閱讀筆記 摘自 《深刻理解計算機系統》僅記錄了感興趣的內容node
計算機系統是由硬件和軟件組成的,它們共同工做來運行應用程序linux
/*hello.c*/ #include<stdio.h> int main() { printf("hello,word\n"); return 0; }
本次經過追蹤hello程序的生命週期來開始對系統的初步瞭解程序員
位 源程序實際上就是一個由值0和1組成的位(又稱比特)序列web
大部分的現代計算機系統都使用ASCII標準來表示文本字符,這種方式實際上就是用一個惟一的單字節大小的整數值來表示每一個字符算法
對於計算機系統來講,信息就是位,都是由一串比特表示,而區分不一樣數據對象的惟一方法是咱們讀到這些數據對象時的上下文,這裏先簡單理解爲經過上下文的信息識別該數據的類型shell
在Unix系統上,從源文件到目標文件的轉化是由編譯器驅動程序完成的:編程
預處理階段:其實就是預處理C程序文件頭,並把它直接插入程序文本中,結果就獲得另外一個C程序,一般以.i做爲文件拓展名ubuntu
編譯階段:編譯器(ccl)將文本文件hello.i翻譯成文本文件hello.s,彙編語言程序。該程序包含函數main的定義,以下所示:windows
main: subq $8, %rsp movl $.LCO, %edi call puts movl $0, %eax addq $8, %rsp ret
彙編階段:彙編器(as)將hello.s翻譯成機器語言指令,把這些指令打包成一種叫作可重定位目標程序的格式,並將結果保存在目標文件hello.o中。hello.o是個二進制文件,都是指令編碼數組
連接階段:hello程序調用了printf函數,它是每一個C編譯器都提供的標準C庫中的一個函數。printf函數存在於一個名爲printf.o的單獨的預編譯好了的目標文件中,而這個文件必須以某種方式合併到咱們的hello.o程序中那個。連接器(ld)就負責處理這種合併。結果就獲得hello文件,他是一個可執行目標文件(簡稱可執行文件),能夠被加載到內存中,由系統執行
shell是一個命令行解釋器,它輸出一個提示符,等待輸入一個命令行,而後執行這個命令,若是該命令行的第一個單詞不是一個內置的shell命令,那麼shell就會假設這是一個可執行文件的名字,它將加載並執行這個文件
在Unix系統下,咱們要運行hello程序能夠經過在shell中輸入它的文件名:
linux> ./hello hello,word linux>
爲了理解運行hello程序時發送了什麼,咱們須要瞭解一個典型系統的硬件知識
總線
貫穿整個系統的一組電子管道,稱做總線,它攜帶信息字節並負責在各個部件間傳遞。一般總線被設計成傳送定長的字節塊,也就是字。不一樣系統設定的字節數不盡相同,主要分爲32位和64位。
I/O設備
I/O設備是系統與外部世界的聯繫通道。每一個I/O設備都經過一個控制器或者適配器與I/O總線相連。控制器和適配器之間的區別主要在於他們的封裝方式。控制器是I/O設備自己或者系統的主印刷電路板(即主板)上的芯片組。而適配器則是一塊插在主板插槽上的卡。
主存
主存是一個臨時存儲設備,在處理器執行程序時,用來存放程序和程序處理的數據。而存儲器是一個線性的字節數組,每一個字節都有其惟一的地址(數組索引),這些地址都是從零開始的,爲了便於地址分配,進程資源管理等,後期還會引入虛擬內存的機率。
處理器
中央處理單元(CPU),簡稱處理器,是解釋(或執行)存儲在主存中指令的引擎。處理器的核心是一個大小爲一個字的存儲設備(或寄存器),稱爲程序計數器(PC)。在任什麼時候刻,PC都指向主存中的某條機器語言指令(即含有該指令的地址)。從系統通電開始,直到系統斷電,處理器一直在不斷地執行程序計數器指向的指令,再更新程序計數器。
處理器看上去是按照一個很是簡單的指令執行模型來操做的,這個模型是由指令集架構決定的。在這個模型中,指令按照嚴格的順序執行,而執行一條指令包含執行一系列的步驟。
處理器看上去是它的指令集架構的簡單實現,可是實際上現代處理器使用了很是複雜的機制來加速程序的執行。所以,咱們將處理器的指令集架構和處理器的微體系結構區分開來:指令集架構描述的是每條機器代碼的效果;而微體系結構描述的是處理器其實是如何實現的。
瞭解這些基礎硬件,應該可以大概描述出運行一個程序所進行的操做。
爲了加快程序運行的速度,開始出現高速緩存存儲器,存儲設備造成層次結構
程序並無直接訪問鍵盤、顯示器、磁盤或者主存,而是依靠操做系統提供的服務,咱們能夠把操做系統當作是應用程序和硬件之間插入的一層軟件,全部應用程序對硬件的操做的嘗試都必須經過操做系統
操做系統有兩個基本功能:
(1)防止硬件被失控的應用程序濫用;
(2)嚮應用程序提供簡單一致的機制來控制複雜而又一般大不相同的低級硬件設備;
操做系統經過幾個抽象概念來實現這兩個功能:進程、虛擬內存、文件
像hello這樣的程序在現代系統上運行時,操做系統會提供一種假象,就好像系統上只有這個程序在運行。程序看上去是獨佔地使用處理器、主存和I/O設備。
進程其實是操做系統對一個正在運行的程序的一種抽象。在一個系統上能夠同時運行多個程序,而每一個進程都好像在獨佔地使用硬件。而併發運行,則是說一個進程的指令和另外一個進程的指令是交錯執行的。而操做系統實現這種交錯執行的機制稱爲上下文切換。
操做系統保持跟蹤進程運行所需的全部狀態信息。這種狀態,也就是上下文,包括許多信息,好比PC和寄存器文件的當前值,以及主存的內容。
在任何一個時刻,單處理器系統都只能執行一個進程的代碼。當操做系統決定要把控制權從當前進程轉移到某個新進程時,就會進行上下文切換,即保存當前進程的上下文、恢復新進程的上下文,而後將控制權傳遞到新進程。新進程就會從它上次中止的地方開始
現代系統中,一個進程實際上能夠由多個稱爲線程的執行單元組成,每一個線程都運行在進程的上下文中,並共享一樣的代碼和全局數據。因爲網絡服務器對並行處理的需求,線程成爲愈來愈多重要的編程模型,由於多線程之間比多進程之間更容易共享數據,比進程更高效
虛擬內存是一個抽象概念,它爲每一個進程提供了一個假象,即每一個進程都在獨佔地使用主存。每一個進程看到的內存都是一致的,稱爲虛擬地址空間。
虛擬內存的運做須要硬件和操做系統軟件之間精密複雜的交互,包括對處理器生成的每一個地址的硬件翻譯。基本思想是把一個進程虛擬內存的內容存儲在磁盤上,而後用主存做爲磁盤的高速緩存。
文件就是字節序列。每一個外部設備均可以當作是文件,系統中的全部輸入輸出都是經過使用一小組稱爲Unix I/O的系統函數調用讀寫文件來實現的
它嚮應用程序提供了一個統一的試圖,來看待系統中可能含有的全部各式各樣的I/O設備。
系統常常經過網絡和其餘系統鏈接到一塊兒,從一個單獨的系統來看,網絡可視爲一個I/O設備。當系統從主存賦值一串字節到網絡適配器時,數據流通過網絡到達另外一臺機器。類似地,系統能夠讀取其餘機器發送來的數據,並把數據複製到本身的內存
譬如:客戶端和服務器之間交互的類型在全部網絡應用中是最典型的
數字計算機的整個歷史中,有兩個需求是驅動進步的持續動力:一個是咱們想要計算機作得更多,另外一個是咱們想要計算機運行得更快。
併發:指一個同時具備多個活動的系統;
並行:指的是用併發來是一個系統運行得更快
按照系統層次結構中由高到低的順序重點強調三個層次
1.線程級併發
2.指令級並行
3.單指令、多數據並行
計算機系統提供的一些抽象,計算機系統中的一個重大主題就是提供不一樣層次的抽象表示,來隱藏實際實現的複雜性
計算機一般使用8位的塊,或者字節(byte),做爲最小的可尋址的內存單位,而不是訪問內存中單獨的位
機器級程序將內存視爲一個很是大的字節數組,稱爲虛擬內存。內存的每一個字節都由一個惟一的數字來標識,稱它爲地址,全部可能地址的集合稱爲虛擬地址空間。
每一個程序對象能夠簡單地視爲一個字節塊,而程序自己就是一個字節序列。
在C語言中,以0x或0X開頭的數字常量被認爲是十六進制(簡寫爲「hex」)的值。例如 0xFA1D37B
記住十六進制數字A、C和F相應的十進制值 A->10 ; C->12 ; F->15
關於進制間的轉換,熟能生巧,忘了就查資料,一般用腳本或工具會比較多
每臺計算機都有一個字長,也就是處理器一次處理的最大數據塊長度。由於虛擬地址是以這樣的一個字來編碼的,因此字長決定的最重要的系統參數就是虛擬地址空間的最大大小。也就是說,對於一個字長爲w位的機器而言,虛擬地址的範圍就是0~2w-1,程序最多訪問2w個字節
32位字長限制虛擬地址空間爲4GB,擴展到64位字長使得虛擬地址空間爲16EB
"32位程序"或"64位程序"區別在於該程序如何編譯的,而不是其運行的機器類型
基本C數據類型的典型大小(以字節爲單位),分配的字節數受程序是如何編譯的影響而變化
大端法:最高有效字節在最前面
小端法:最低有效字節在最前面
X86體系都是選用小端模式
這種字節順序在單一系統上是沒有什麼區別的,不過有時候,字節順序會成爲問題。
首先是在不一樣類型的機器之間經過網絡傳送二進制數據時,一個常見的問題是當小端法機器產生的數據被髮送到大端法機器或者反過來時,接收程序會發現,字裏的字節成了反序的,這就須要機器在發送時轉化爲網絡標準,而接收方則將網絡標準轉化爲它的內部表示
第二種狀況,當閱讀表示整數數據的字節序列時字節順序也很重要。
第三種.......
每一個字符都由某個標準編碼來表示,最多見的是ASCII字符碼
使用ASCII碼做爲字符碼的任何系統上都將獲得相同的結果,與字節順序和字大小規則無關。所以,文本數據比二進制數據具備更強的平臺獨立性
ASCII字符集適合編碼英文文檔,可是在表達一些特殊字符方面並無太多辦法,所以引入了unicode編碼,稱爲"統一字符集"
關於無符號數、有符號數、小數、浮點數四則運算、規格化、運算溢出等知識,在大學的專業基礎課中都有學過,這裏再也不作補充,理解就好,仍是同樣,須要用的時候忘了就查資料
須要時,爲加深理解再回來從新整理
IA32,x86-64的32位前身,是Intel在1985年提出的
ISA,指令集體系結構或指令集架構,定義機器級程序的格式和行爲,定義處理器狀態、指令的格式,以及每條指令對狀態的影響
目標是利用編譯器[GCC](https://zh.wikipedia.org/wiki/GCC)
展現如何查看彙編代碼,並將它反向映射到高級編程語言中的結構
/*p.c*/ #include <stdio.h> long mult2(long,long); void multstore(long x,long y,long *dest){ long t = mult2(x,y); *dest = t; }
在命令行上使用「-S」選項,就能看到C語言編譯器產生的彙編代碼:
linux> gcc -og -S p.c
這會使GCC運行編譯器,產生一個彙編文件p.s,可是不作其餘進一步的工做(一般狀況下,它還會繼續調用匯編器產生目標代碼文件)
彙編代碼文件包含各類聲明:
.file "p.c" .text .globl multstore .type multstore, @function multstore: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $48, %rsp movq %rdi, -24(%rbp) movq %rsi, -32(%rbp) movq %rdx, -40(%rbp) movq -32(%rbp), %rdx movq -24(%rbp), %rax movq %rdx, %rsi movq %rax, %rdi call mult2 movq %rax, -8(%rbp) movq -40(%rbp), %rax movq -8(%rbp), %rdx movq %rdx, (%rax) nop leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size multstore, .-multstore .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits
上面代碼中,multstore函數聲明下每一個縮進去的行都對應一條機器指令。
好比,pushq指令表示應該將寄存器%rbp的內容壓入程序棧中。
若是咱們使用"-c"命令行選項,GCC會編譯並彙編該代碼:
linux> gcc -og -c p.c
這就會產生目標代碼文件p.o,它是二進制格式的,因此沒法直接查看
下面是文件中一段16字節的序列,它的十六進制表示爲:
55 48 89 e5 48 83 ec 30 48 89 7d e8 48 89 75 e0
機器執行的程序只是一個字節序列,它是對一系列指令的編碼。機器對產生這些指令的源代碼幾乎一無所知
要查看機器代碼文件(可重定位文件)的內容,有一類稱爲反彙編器(disassembler)的程序很是有用
這些程序根據機器代碼產生一種相似於彙編代碼的格式。在Linux中,帶-d
命令行標誌的程序OBJDUMP(表示"object dump")能夠充當這個角色:
linux> objdump -d p.o
結果以下:
Disassembly of section .text: 0000000000000000 <multstore>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 30 sub $0x30,%rsp 8: 48 89 7d e8 mov %rdi,-0x18(%rbp) c: 48 89 75 e0 mov %rsi,-0x20(%rbp) 10: 48 89 55 d8 mov %rdx,-0x28(%rbp) 14: 48 8b 55 e0 mov -0x20(%rbp),%rdx 18: 48 8b 45 e8 mov -0x18(%rbp),%rax 1c: 48 89 d6 mov %rdx,%rsi 1f: 48 89 c7 mov %rax,%rdi 22: e8 00 00 00 00 callq 27 <multstore+0x27> 27: 48 89 45 f8 mov %rax,-0x8(%rbp) 2b: 48 8b 45 d8 mov -0x28(%rbp),%rax 2f: 48 8b 55 f8 mov -0x8(%rbp),%rdx 33: 48 89 10 mov %rdx,(%rax) 36: 90 nop 37: c9 leaveq 38: c3 retq
在左邊,咱們看到按照前面給出的字節順序排列的16個十六進制字節值,它們分紅了若干組,每組有1~5個字節。每組都是一條指令,右邊是等價的彙編語言。
其中一些關於機器代碼和它的反彙編表示的特性值得注意:
- x86-64 的指令長度從1到15字節不等,經常使用的指令以及操做數較少的指令所需的字節數少,而那些不太經常使用或操做數較多的指令所需字節數較多
- 設計指令格式的方式是,從某個給定位置開始,能夠將字節惟一地解碼成機器指令。例如,只有質量pushq %rbp是以字節值55開頭的
- 反彙編器只是基於機器代碼文件中的字節序列來肯定彙編代碼。它不須要訪問該程序的源代碼或彙編代碼
- 反彙編器使用的指令命名規則與GCC生成的彙編代碼使用的有寫細微的差異,好比pushq,‘q'徹底能夠省略
生成實際可執行文件的代碼須要對一組目標代碼文件運行連接器,而這一組目標代碼文件中必須含有一個main函數:
/* pp.c */ #include <stdio.h> void multstore(long,long,long *); int main(){ long d; multstore(2,3,&d); printf("2 * 3--> %ld\n",d ); return 0; } long mult2(long a,long b){ long s = a * b; return s; }
而後,用以下方法生成可執行文件prog
linux> gcc -og -o prog pp.c p.c
文件prog變成了8655個字節,由於它不只包含了兩個過程的代碼,還包含了用來啓動和終止程序的代碼,以及用來與操做系統交互的代碼。咱們也能夠反彙編prog文件:
linux> objdump -d prog
反彙編會抽取出各類代碼序列:
Disassembly of section .init: 0000000000400428 <_init>: 400428: 48 83 ec 08 sub $0x8,%rsp 40042c: 48 8b 05 c5 0b 20 00 mov 0x200bc5(%rip),%rax # 600ff8 <_DYNAMIC+0x1d0> 400433: 48 85 c0 test %rax,%rax 400436: 74 05 je 40043d <_init+0x15> 400438: e8 53 00 00 00 callq 400490 <__libc_start_main@plt+0x10> 40043d: 48 83 c4 08 add $0x8,%rsp 400441: c3 retq Disassembly of section .plt: 0000000000400450 <__stack_chk_fail@plt-0x10>: 400450: ff 35 b2 0b 20 00 pushq 0x200bb2(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8> 400456: ff 25 b4 0b 20 00 jmpq *0x200bb4(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10> 40045c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000400460 <__stack_chk_fail@plt>: 400460: ff 25 b2 0b 20 00 jmpq *0x200bb2(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18> 400466: 68 00 00 00 00 pushq $0x0 40046b: e9 e0 ff ff ff jmpq 400450 <_init+0x28> 0000000000400470 <printf@plt>: 400470: ff 25 aa 0b 20 00 jmpq *0x200baa(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20> 400476: 68 01 00 00 00 pushq $0x1 40047b: e9 d0 ff ff ff jmpq 400450 <_init+0x28> 0000000000400480 <__libc_start_main@plt>: 400480: ff 25 a2 0b 20 00 jmpq *0x200ba2(%rip) # 601028 <_GLOBAL_OFFSET_TABLE_+0x28> 400486: 68 02 00 00 00 pushq $0x2 40048b: e9 c0 ff ff ff jmpq 400450 <_init+0x28> Disassembly of section .plt.got: 0000000000400490 <.plt.got>: 400490: ff 25 62 0b 20 00 jmpq *0x200b62(%rip) # 600ff8 <_DYNAMIC+0x1d0> 400496: 66 90 xchg %ax,%ax Disassembly of section .text: 00000000004004a0 <_start>: 4004a0: 31 ed xor %ebp,%ebp 4004a2: 49 89 d1 mov %rdx,%r9 4004a5: 5e pop %rsi 4004a6: 48 89 e2 mov %rsp,%rdx 4004a9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 4004ad: 50 push %rax 4004ae: 54 push %rsp 4004af: 49 c7 c0 c0 06 40 00 mov $0x4006c0,%r8 4004b6: 48 c7 c1 50 06 40 00 mov $0x400650,%rcx 4004bd: 48 c7 c7 96 05 40 00 mov $0x400596,%rdi 4004c4: e8 b7 ff ff ff callq 400480 <__libc_start_main@plt> 4004c9: f4 hlt 4004ca: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) 00000000004004d0 <deregister_tm_clones>: 4004d0: b8 47 10 60 00 mov $0x601047,%eax 4004d5: 55 push %rbp 4004d6: 48 2d 40 10 60 00 sub $0x601040,%rax 4004dc: 48 83 f8 0e cmp $0xe,%rax 4004e0: 48 89 e5 mov %rsp,%rbp 4004e3: 76 1b jbe 400500 <deregister_tm_clones+0x30> 4004e5: b8 00 00 00 00 mov $0x0,%eax 4004ea: 48 85 c0 test %rax,%rax 4004ed: 74 11 je 400500 <deregister_tm_clones+0x30> 4004ef: 5d pop %rbp 4004f0: bf 40 10 60 00 mov $0x601040,%edi 4004f5: ff e0 jmpq *%rax 4004f7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 4004fe: 00 00 400500: 5d pop %rbp 400501: c3 retq 400502: 0f 1f 40 00 nopl 0x0(%rax) 400506: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 40050d: 00 00 00 0000000000400510 <register_tm_clones>: 400510: be 40 10 60 00 mov $0x601040,%esi 400515: 55 push %rbp 400516: 48 81 ee 40 10 60 00 sub $0x601040,%rsi 40051d: 48 c1 fe 03 sar $0x3,%rsi 400521: 48 89 e5 mov %rsp,%rbp 400524: 48 89 f0 mov %rsi,%rax 400527: 48 c1 e8 3f shr $0x3f,%rax 40052b: 48 01 c6 add %rax,%rsi 40052e: 48 d1 fe sar %rsi 400531: 74 15 je 400548 <register_tm_clones+0x38> 400533: b8 00 00 00 00 mov $0x0,%eax 400538: 48 85 c0 test %rax,%rax 40053b: 74 0b je 400548 <register_tm_clones+0x38> 40053d: 5d pop %rbp 40053e: bf 40 10 60 00 mov $0x601040,%edi 400543: ff e0 jmpq *%rax 400545: 0f 1f 00 nopl (%rax) 400548: 5d pop %rbp 400549: c3 retq 40054a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) 0000000000400550 <__do_global_dtors_aux>: 400550: 80 3d e9 0a 20 00 00 cmpb $0x0,0x200ae9(%rip) # 601040 <__TMC_END__> 400557: 75 11 jne 40056a <__do_global_dtors_aux+0x1a> 400559: 55 push %rbp 40055a: 48 89 e5 mov %rsp,%rbp 40055d: e8 6e ff ff ff callq 4004d0 <deregister_tm_clones> 400562: 5d pop %rbp 400563: c6 05 d6 0a 20 00 01 movb $0x1,0x200ad6(%rip) # 601040 <__TMC_END__> 40056a: f3 c3 repz retq 40056c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000400570 <frame_dummy>: 400570: bf 20 0e 60 00 mov $0x600e20,%edi 400575: 48 83 3f 00 cmpq $0x0,(%rdi) 400579: 75 05 jne 400580 <frame_dummy+0x10> 40057b: eb 93 jmp 400510 <register_tm_clones> 40057d: 0f 1f 00 nopl (%rax) 400580: b8 00 00 00 00 mov $0x0,%eax 400585: 48 85 c0 test %rax,%rax 400588: 74 f1 je 40057b <frame_dummy+0xb> 40058a: 55 push %rbp 40058b: 48 89 e5 mov %rsp,%rbp 40058e: ff d0 callq *%rax 400590: 5d pop %rbp 400591: e9 7a ff ff ff jmpq 400510 <register_tm_clones> 0000000000400596 <main>: 400596: 55 push %rbp 400597: 48 89 e5 mov %rsp,%rbp 40059a: 48 83 ec 10 sub $0x10,%rsp 40059e: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 4005a5: 00 00 4005a7: 48 89 45 f8 mov %rax,-0x8(%rbp) 4005ab: 31 c0 xor %eax,%eax 4005ad: 48 8d 45 f0 lea -0x10(%rbp),%rax 4005b1: 48 89 c2 mov %rax,%rdx 4005b4: be 03 00 00 00 mov $0x3,%esi 4005b9: bf 02 00 00 00 mov $0x2,%edi 4005be: e8 50 00 00 00 callq 400613 <multstore> 4005c3: 48 8b 45 f0 mov -0x10(%rbp),%rax 4005c7: 48 89 c6 mov %rax,%rsi 4005ca: bf d4 06 40 00 mov $0x4006d4,%edi 4005cf: b8 00 00 00 00 mov $0x0,%eax 4005d4: e8 97 fe ff ff callq 400470 <printf@plt> 4005d9: b8 00 00 00 00 mov $0x0,%eax 4005de: 48 8b 4d f8 mov -0x8(%rbp),%rcx 4005e2: 64 48 33 0c 25 28 00 xor %fs:0x28,%rcx 4005e9: 00 00 4005eb: 74 05 je 4005f2 <main+0x5c> 4005ed: e8 6e fe ff ff callq 400460 <__stack_chk_fail@plt> 4005f2: c9 leaveq 4005f3: c3 retq 00000000004005f4 <mult2>: 4005f4: 55 push %rbp 4005f5: 48 89 e5 mov %rsp,%rbp 4005f8: 48 89 7d e8 mov %rdi,-0x18(%rbp) 4005fc: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400600: 48 8b 45 e8 mov -0x18(%rbp),%rax 400604: 48 0f af 45 e0 imul -0x20(%rbp),%rax 400609: 48 89 45 f8 mov %rax,-0x8(%rbp) 40060d: 48 8b 45 f8 mov -0x8(%rbp),%rax 400611: 5d pop %rbp 400612: c3 retq 0000000000400613 <multstore>: 400613: 55 push %rbp 400614: 48 89 e5 mov %rsp,%rbp 400617: 48 83 ec 30 sub $0x30,%rsp 40061b: 48 89 7d e8 mov %rdi,-0x18(%rbp) 40061f: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400623: 48 89 55 d8 mov %rdx,-0x28(%rbp) 400627: 48 8b 55 e0 mov -0x20(%rbp),%rdx 40062b: 48 8b 45 e8 mov -0x18(%rbp),%rax 40062f: 48 89 d6 mov %rdx,%rsi 400632: 48 89 c7 mov %rax,%rdi 400635: e8 ba ff ff ff callq 4005f4 <mult2> 40063a: 48 89 45 f8 mov %rax,-0x8(%rbp) 40063e: 48 8b 45 d8 mov -0x28(%rbp),%rax 400642: 48 8b 55 f8 mov -0x8(%rbp),%rdx 400646: 48 89 10 mov %rdx,(%rax) 400649: 90 nop 40064a: c9 leaveq 40064b: c3 retq 40064c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000400650 <__libc_csu_init>: 400650: 41 57 push %r15 400652: 41 56 push %r14 400654: 41 89 ff mov %edi,%r15d 400657: 41 55 push %r13 400659: 41 54 push %r12 40065b: 4c 8d 25 ae 07 20 00 lea 0x2007ae(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry> 400662: 55 push %rbp 400663: 48 8d 2d ae 07 20 00 lea 0x2007ae(%rip),%rbp # 600e18 <__init_array_end> 40066a: 53 push %rbx 40066b: 49 89 f6 mov %rsi,%r14 40066e: 49 89 d5 mov %rdx,%r13 400671: 4c 29 e5 sub %r12,%rbp 400674: 48 83 ec 08 sub $0x8,%rsp 400678: 48 c1 fd 03 sar $0x3,%rbp 40067c: e8 a7 fd ff ff callq 400428 <_init> 400681: 48 85 ed test %rbp,%rbp 400684: 74 20 je 4006a6 <__libc_csu_init+0x56> 400686: 31 db xor %ebx,%ebx 400688: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40068f: 00 400690: 4c 89 ea mov %r13,%rdx 400693: 4c 89 f6 mov %r14,%rsi 400696: 44 89 ff mov %r15d,%edi 400699: 41 ff 14 dc callq *(%r12,%rbx,8) 40069d: 48 83 c3 01 add $0x1,%rbx 4006a1: 48 39 eb cmp %rbp,%rbx 4006a4: 75 ea jne 400690 <__libc_csu_init+0x40> 4006a6: 48 83 c4 08 add $0x8,%rsp 4006aa: 5b pop %rbx 4006ab: 5d pop %rbp 4006ac: 41 5c pop %r12 4006ae: 41 5d pop %r13 4006b0: 41 5e pop %r14 4006b2: 41 5f pop %r15 4006b4: c3 retq 4006b5: 90 nop 4006b6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 4006bd: 00 00 00 00000000004006c0 <__libc_csu_fini>: 4006c0: f3 c3 repz retq Disassembly of section .fini: 00000000004006c4 <_fini>: 4006c4: 48 83 ec 08 sub $0x8,%rsp 4006c8: 48 83 c4 08 add $0x8,%rsp 4006cc: c3 retq
抽出一段:
0000000000400613 <multstore>: 400613: 55 push %rbp 400614: 48 89 e5 mov %rsp,%rbp 400617: 48 83 ec 30 sub $0x30,%rsp 40061b: 48 89 7d e8 mov %rdi,-0x18(%rbp) 40061f: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400623: 48 89 55 d8 mov %rdx,-0x28(%rbp) 400627: 48 8b 55 e0 mov -0x20(%rbp),%rdx 40062b: 48 8b 45 e8 mov -0x18(%rbp),%rax 40062f: 48 89 d6 mov %rdx,%rsi 400632: 48 89 c7 mov %rax,%rdi 400635: e8 ba ff ff ff callq 4005f4 <mult2> 40063a: 48 89 45 f8 mov %rax,-0x8(%rbp) 40063e: 48 8b 45 d8 mov -0x28(%rbp),%rax 400642: 48 8b 55 f8 mov -0x8(%rbp),%rdx 400646: 48 89 10 mov %rdx,(%rax) 400649: 90 nop 40064a: c9 leaveq 40064b: c3 retq 40064c: 0f 1f 40 00 nopl 0x0(%rax)
這段代碼與以前p.c反彙編產生的代碼幾乎徹底同樣。其中主要的區別是左邊列出的地址不一樣-----連接器將這段代碼的地址移到了一段不一樣的地址範圍中
第二個不一樣之處在於連接器填上了callq指令調用函數mult2須要使用的地址;連接器的任務之一就是爲函數調用找到匹配的函數的可執行代碼的位置。
最後一個區別是多了兩行代碼,插入這些指令是爲了使函數代碼變爲16字節,對程序並無影響,使得就存儲器系統性能而言,能更好地放置下一個代碼塊
大多數GCC生成的彙編代碼指令都有一個字符的後綴,代表操做數的大小。例如,數據傳送指令由四個變種:movb(傳送字節)、movw(傳送字)、movl(傳送雙字)、和movq(傳送四字)
一個x86-64的中央處理單元(CPU)包含一組16個存儲64位值得通用目的寄存器,用來存儲整數數據和指針
一般有一組標準的編程規範控制着如何使用寄存器來管理棧、傳遞函數參數、從函數的返回值,以及存儲局部和臨時數據。
大多數指令有一個或多個操做數,指示出執行一個操做中要使用的源數據值,以及放置結果的目的位置。
源數據能夠以常數形式給出,或是從寄存器或內存中讀出。結果能夠存放在寄存器或內存中。所以,各類不一樣的操做數的可能性被分爲三種類型:當即數 、寄存器、內存引用
也就是三種基本的尋址方式,能夠在學習彙編語言的時候接觸到
最頻繁使用的指令是將數據從一個位置複製到另外一個位置的指令。
四種不一樣的指令都執行相同的操做;主要區別在於它們操做的數據大小不一樣
源操做數指定的值是一個當即數,存儲在寄存器中或者內存中。目的操做數指定一個位置,要麼是一個寄存器或者,要麼是一個內存地址。
x86-64加了一條限制,傳送指令的兩個操做數不能都指向內存位置。將一個值從一個內存位置複製到另外一個內存位置須要兩條指令:
第一條指令將源值加載到寄存器中,第二條將該寄存器值寫入目的的位置,也就是必須通過寄存器
mov指令只會更新目的操做數指定的那些寄存器字節或內存位置,惟一例外的是movl指令以寄存器做爲目的時,會把該寄存器的高位4字節設置爲0
在將較小的源值複製到較大的目的時使用雙大小指示符:
第一個字符指定源的大小,第二個指名目的的大小
數據擴展方式
①零擴展:MOVZ類中的指令把目的中剩餘的字節填充爲0
②符號擴展:MOVS類中的指令經過符號擴展來填充,把源操做的最高位進行復制
/* C語言代碼*/ long exchange(long *xp,long y) { long x = *xp; *xp = y; return x; }
/*彙編代碼*/ exchange: movq (%rdi),%rax movq %rsi,(%rdi) ret
函數exchange由三條指令實現:兩條數據傳送(movq),加上一條返回函數被調用點的指令(ret)
可知參數經過寄存器傳遞給函數,函數經過把值存儲在寄存器%rax或者寄存器的某個低位部分中返回
執行過程描述: 過程參數xp和y分別存儲在寄存器%rdi和%rsi中。而後,指令2從內存中讀出x,把它存放到寄存器%rax中,直接實現了C程序中的操做x=xp。稍後,用寄存器%rax從這個函數返回一個值,於是返回值就是x。指令3將y寫入到寄存器%rdi中的xp指向的內存位置,直接實現了xp=y。
棧是一種數據結構,能夠經過添加或者刪除值,不過要遵循「後進先出」的原則。經過push操做把數據壓入棧中,經過pop操做刪除數據;它具備一個屬性:彈出的值永遠是最近被壓入並且仍然在棧中的值。在x86-64中,程序棧存放在內存中某個區域,棧向下增加,這樣一來,棧頂元素的地址是全部棧中元素地址最低的,棧指針%rsp保存着棧頂元素的地址
分爲四類:加載有效地址、一元操做、二元操做和移位
加載有效地址(load effective address)指令leaq其實是movq指令的變形。它的指令形式是從內存讀數據到寄存器,但實際上它根本就沒有引用內存
它的第一個操做數看上去是一個內存引用,但該指令並非從指定的位置讀入數據,而是將有效地址寫入到目的操做數。
leaq指令能執行加法和有限形式的乘法,例如:若是寄存器%rdx的值爲x,那麼指令leaq 7(%rdx,%rdx,4),%rax 將設置寄存器%rax的值爲5x+7.
一元操做,只有一個操做數,既是源又是目的。這個操做數能夠是一個寄存器,也能夠是一個內存位置
二元操做,其中,第二個操做數既是源又是目的。即源操做數是第一個,目的操做數是第二個,最終的結果是放回目的,第二個操做數不能是當即數
先給出移位量,而後第二項給出的是要移位的數。能夠進行算術和邏輯移位。移位量能夠是一個當即數,或者放在單字節寄存器%cl中。原則上1字節的移位量是的移位量的編碼範圍能夠達到28-1=255。x86-64中,移位操做對w位長的數據值進行操做,移位量又%cl寄存器的**低m位決定的,這裏2m=w。高位會被忽略**。
例如:寄存器%cl的十六進制值爲0xFF時,指令salb會移7位,salw會移15位,sall會移21位,而salq會移63位
左移指令有兩個名字:SAL和SHL。二者的效果同樣的,都是將右邊填上0
右移指令不一樣,SAR執行算術移位(填上符號位,其實就是移位後符號不能變),而SHR執行邏輯移位(填上0)
移位操做數的目的操做數能夠是一個寄存器或者一個內存位置;只有右移位須要區分符號和無符號數
對兩個64位有符號或無符號整數相乘獲得的乘積須要128位來表示。x86-64指令集對128位數的操做提供有限的支持
擴展方式是高64位放在%rdx,低64位放在%rax
到目前爲止,咱們只考慮了直線代碼的行爲,也就是指令一條接着一條順序地執行。但若是要實現條件控制、循環等操做,該怎麼辦呢
除了整數寄存器,CPU還維護着一組單個位的條件碼寄存器,它們描述了最近的算術或邏輯操做的屬性。還能夠檢測這些寄存器來執行條件分支指令,常見的條件碼有:
CF:進位標誌。最近的操做使最高位產生了進位。可用來檢查無符號操做的溢出
ZF:零標誌。最近的操做得出的結果爲0
SF:符號標誌。最近的操做獲得的結果爲負數
OF:溢出標誌。最近的操做致使一個補碼溢出----正溢出或負溢出
leaq指令不改變任何條件碼,由於它是用來進行地址計算的
與條件碼相關的指令(只設置條件碼而不更新目的寄存器):CMP(比較大小)、TEST(與AND指令相同操做)
大多數運算符都會更新目的寄存器,同時設置對應的條件碼
條件碼一般不會直接讀取,經常使用的使用方法有三種:
①能夠根據條件碼的某種組合,將一個字節設置爲0或1
SET指令;指令名字的不一樣後綴指明瞭他們所考慮的條件碼的組合,必須明確指令的後綴表示不一樣的條件而不是操做數大小
②能夠條件跳轉到程序的某個其餘的部分
③能夠有條件地傳送數據
正常執行的狀況下,指令按照它們出現的順序一條條地執行。跳轉(jump)指令會致使執行切換到程序中一個全新的位置
跳轉指令jmp能夠是無條件跳轉;也能夠是直接跳轉;或者是間接跳轉,區別只是跳轉目標是否做爲指令的一部分編碼,是否來源於寄存器或內存位置讀出
瞭解跳轉指令的目標如何編碼,對研究連接很是有幫助,也能幫助理解反彙編器的輸出
①條件控制
用跳轉指令jmp、jge,結合有條件和無條件跳轉
當條件知足時,程序沿一條直線路徑執行,當條件不知足時就走另外一條
②條件傳送(數據)
用comv指令
這種方法計算一個條件操做的兩種結果,而後再根據條件是否知足從中選取一個
瞭解常見的幾種循環在彙編代碼的表示:do、while、for、switch
逆向工程:理解產生的彙編代碼與原始代碼之間的關係,關鍵是找到程序值和寄存器之間的映射關係
x86-64 的過程實現包括一組特殊的指令和一些對機器資源(例如寄存器和程序內存)使用的約定規則
要提供對過程的機器級支持,必需要處理許多不一樣的屬性。假設過程p調用過程Q,Q執行後返回p。這些動做包括下面一個或多個機制:
傳遞控制 在進入過程Q的時候,程序計數器必須被設置爲Q的代碼的起始地址,而後再返回時,要把程序計數器設置爲p中調用Q後面那條指令的地址
傳遞數據 p必須可以向Q提供一個或多個參數,Q必須可以向p返回一個值
分配和釋放內存 在開始時,Q可能須要爲局部變量分配空間,而在返回前,又必須釋放這些存儲空間
棧數據結構提供了後進先出的內存管理原則;程序能夠用棧來管理它的過程所須要的存儲空間,棧和程序寄存器存放着傳遞控制和數據、分配內存所需的信息。當p調用Q時,控制和數據信息添加到棧尾,當p返回時,這些信息會釋放掉
當x86-64過程須要的存儲空間超出寄存器可以存放的大小時,就會在棧上分配空間。這個部分稱爲過程的棧幀。當前正在執行的過程的幀總在棧頂,大多數過程的棧幀都是定長的,在過程的開始就分配好了,而實際上,許多函數甚至根本就不須要棧幀。
將控制從函數p轉移到函數Q只須要簡單地把程序計數器(PC)設置爲Q的代碼起始位置。不過,當稍後從Q返回的時候,處理器必須記錄好它須要繼續p的執行的代碼位置。在x86-64機器中,這個信息是用指令call Q調用過程Q來記錄的。該指令會把地址A壓入棧中,並將PC設置爲Q的起始地址。壓入的地址A被稱爲返回地址,是緊跟着call指令後面的那條指令的地址。對應的指令ret會從棧中彈出地址A,並把PC設置爲A
當調用一個過程時,除了要把控制傳遞給它並在過程返回時再傳遞回來以外,過程調用還可能包括把數據做爲參數傳遞,而從過程返回還可能包括一個返回值。x86-64中,大部分過程間的數據傳送是經過寄存器實現。
寄存器不足夠存放全部的本地數據;
對一個局部變量使用地址運算符‘&’,所以必須可以爲它產生一個地址;
某些局部變量是數組或結構,所以必須可以經過數組或結構引用被訪問到
通常來講,過程經過減少棧指針在棧上分配空間。分配的結果做爲棧幀的一部分,標號爲「局部變量」
寄存器組是惟一被全部過程共享的資源
必須確保當一個過程(調用者)調用另外一個過程時,被調用者不會覆蓋調用者稍後會使用的寄存器值
壓入寄存器的值會在棧幀中建立標號「保存的寄存器」
寄存器和棧的慣例使得過程可以遞歸地調用它們自身。每一個過程調用在棧中都有它本身的私有空間,所以多個未完成調用的局部變量不會相互影響。此外,棧的原則很天然地就提供了適當的策略,當過程被調用時分配局部存儲,當返回時釋放存儲
C語言的不一樣尋常的特色是能夠產生指向數組中元素的指針,並對這些指針進行運算。在機器代碼中,這些指針會被翻譯成地址計算
x86-64的內存引用指令能夠用來簡化數組訪問。例如,假設E是一個int型的數組,而咱們想計算E[i],在此,E的地址存放在寄存器%rdx中,而i存放在寄存器%rcx中,而後,指令movl (%rdx,%rcx,4),%eax
會執行地址計算Xe+4i,讀這個內存位置的值,並將結果存放到寄存器%eax中
經過下面的命令行來啓動GDB:
linux> gdb prog
commands | result |
---|---|
quit | 退出GDB |
run | 運行程序(在此給出命令行參數) |
kill | 中止程序 |
break multstore | 在函數multstore入口處設置斷點 |
break * 0x400540 | 在地址0x400540 |
delete 1 | 刪除斷點1 |
delete | 刪除全部斷點 |
stepi | 執行1條指令 |
stepi 4 | 執行4條指令 |
nexti | 相似stepi,但以函數調用爲單位 |
continue | 繼續執行 |
finish | 運行到當前函數返回 |
disas | 反彙編當前函數 |
disas multstore | 反彙編函數multstore |
disas 0x400544 | 反彙編位於地址0x400544附件的函數 |
disas 0x400540,0x40054d | 反彙編指定地址範圍內的代碼 |
print /x $rip | 以十六進制輸出程序計數器的值 |
print $rax | 以十進制輸出%rax的內容 |
print /x $rax | 以十六進制輸出%rax的內容 |
print /t $rax | 以二進制輸出%rax的內容 |
print 0x100 | 輸出0x100的十進制表示 |
print /x 555 | 輸出555的十六進制表示 |
print /x ($rsp+8) | 以十六進制輸出%rsp的內容加上8 |
print *(long *) 0x7fffffffe818 | 輸出位於地址0x7fffffffe818的長整數 |
print *(long *)($rsp+8) | 輸出位於地址%rsp+8處的長整數 |
x/2g 0x7fffffffe818 | 檢查從地址0x7fffffffe818開始的雙字 |
x20bmultstore | 檢查函數multstore的前20個字節 |
info frame | 有關當前棧幀的信息 |
info registers | 全部寄存器的值 |
help |
對越界的數組元素的寫操做會破壞存儲在棧中的狀態信息
一種特別常見的狀態破壞稱爲緩衝區溢出(buffer overflow)。一般,在棧中分配某個字符數組來保存一個字符串,可是字符串的長度超出了爲數組分配的空間
緩衝區溢出
的一個更加致命的使用就是讓程序執行它原本不肯意執行的函數
一般,輸入給程序一個字符串,這個字符串包含一些可執行代碼的字節編碼,稱爲攻擊編碼(exploit code),另外,還有一些字節會用一個指向攻擊代碼的指針覆蓋返回地址。那麼,執行ret指令的效果就是跳轉到攻擊代碼
在一種攻擊形式中,攻擊代碼會使用系統調用啓動一個shell程序,給攻擊者提供一組操做系統函數。另外一種攻擊形式中,攻擊代碼會執行一些未受權的任務,修復棧的破壞,而後第二次執行ret指令,(表面上)正常返回到調用者
在1988年11月,著名的Internet蠕蟲病毒經過Internet以四種不一樣的方法獲取對多計算機的訪問。一種是對finger守護進程fingerd的緩衝區溢出攻擊,fingerd服務FINGER命令請求。經過以一個適當的字符串調用FINGER,蠕蟲能夠遠程的守護進程緩衝區並執行一段代碼,讓蠕蟲訪問遠程系統。一旦蠕蟲得到了對系統的訪問,它就能自我複製,幾乎徹底地消耗掉機器上全部的計算機資源。
1.棧隨機化
爲了在系統中插入攻擊代碼,攻擊者既要插入代碼,也要插入指向這段代碼的指針,這個指針也是攻擊字符串的一部分。產生這個指針須要知道這個字符串放置的棧地址。
棧隨機化的思想使得棧的位置在程序每次運行時都有變化。所以,即便許多機器都運行一樣的代碼,他們的棧地址都是不一樣的
然而一個執著的攻擊者老是可以用蠻力克服隨機化,他能夠反覆用不一樣的地址進行攻擊。一種常見的把戲就是在實際的攻擊代碼前插入很長一段nop(讀做「no op」,no operatioin的縮寫)指令。執行這種指令除了對程序計數器加一,使之指向下一條指令以外,沒有任何效果。只要攻擊者可以猜中這段序列中的某個地址,程序就會通過這個序列,到達攻擊代碼。這個序列經常使用的術語是「空操做雪橇(nop sled)」,意思是程序會「滑過」這個序列。若是咱們創建一個256字節的nop sled,那麼枚舉215=32768個起始地址,就能破解n=223的隨機化,這對於一個頑固的攻擊者來講,是徹底可行的。若是是對於64位的系統,就要嘗試枚舉2^24=16777216
2.棧破壞檢測
在產生的代碼中加入了一種棧保護者機制,來檢測緩衝區越界,其思想是在棧幀中任何局部緩衝區與棧狀態之間存儲一種特殊的金絲雀值,也稱哨兵值
,是在程序每次運行時隨機產生的;在恢復寄存器狀態和從函數返回以前,程序檢查這個金絲雀值是否杯該函數的某個操做或者該函數調用的某個函數的某個操做改變了,若是是,那麼程序異常停止
3.限制可執行代碼區域
消除攻擊者想系統中插入可執行代碼的能力,限制哪些內存區域可以存放可執行代碼。
爲了管理變長棧幀,x86代碼使用寄存器%rbp做爲幀指針(frame pointer)(有時稱爲基指針(base pointer),這也是%rbp中bp兩個字母的由來)。當使用幀指針時,棧幀的組織結構與圖中函數vframe的狀況同樣。能夠看到代碼必須把%rbp以前的值保存到棧中,由於它是一個被調用者保存寄存器。而後在函數的整個執行過程當中,都使得%rbp指向那個時刻棧的位置,而後用固定長度的局部變量相對於%rbp的偏移量來引用他們
程序中的每條指令都會讀取或修改處理器狀態的某些部分。這稱爲程序員可見狀態。
Y86-64程序員可見狀態。同x86-64 同樣,Y86-64的程序能夠訪問和修改程序寄存器、狀態碼、程序計數器(PC)和內存。狀態碼指明程序是否容許正常,或者發生了某個特殊事件
內存從概念上來講就是一個很大的字節數組,保存着程序和數據。Y86-64程序用虛擬地址來引用內存位置。硬件和操做系統軟件聯合起來將虛擬地址翻譯成實際物理地址,指明數據實際存在內存中哪一個地方。
程序狀態的最後一部分是狀態碼Stat,它代表程序執行的整體狀態
指令的字節級編碼。每條指令須要1~10個字節不等,這取決於須要哪些字段。每條指令的第一個字節代表指令的類型,這個字節分爲兩部分,每部分4位:高4位是代碼部分,低4位是功能部分
功能值只有在一組相關指令共用一個代碼時纔有用,區分不一樣功能。
Y86-64 15個程序寄存器中每一個都有一個相對應的範圍在0到0xE之間的寄存器標識符,程序寄存器存在CPU中的一個寄存器文件中,這個寄存器文件就是一個小的、以寄存器ID爲地址的隨機訪問存儲器。在指令編碼中以及在硬件設計中,當須要指明不該訪問任何寄存器時,就用ID值0xF來表示
有的指令只有一個字節長,而有的須要操做數的指令編碼就更長一些。指令集的一個重要性質就是字節編碼必須惟一的解釋。任意一個字節序列要麼是一個惟一的指令序列的編碼,要麼就不是一個合法的字節序列。
例如:用十六進制來表示指令rmmovq %rsp,0x123456789abcd(%rdx)的字節編碼。
rmmovq的第一個字節爲40.源寄存器%rsp應該編碼放在rA字段中,而基址寄存器%rdx應該編碼放在rB字段中。兩個寄存器的ID分別爲4和2.最後,偏移量編碼放在8字節的常數字中。首先在0x123456789abcd的前面填充0變成8個字節,變成字節序列00 01 23 45 67 89 ab cd。寫成按字節反序就是4042cdab896745230100
比較x86-64和Y86-64的指令編碼
同x86-64中的指令編碼相比,Y86-64的編碼簡單得多,可是沒有那麼緊湊。在全部的Y86-64指令中,寄存器字段的位置都是固定的,而在不一樣的x86-64指令中,它們的位置是不同的。x86-64能夠將常數值編碼成一、二、4或8個字節,而Y86-64老是將常數值編碼成8字節
RISC和CISC指令集
對Y86-64來講,程序員可見的狀態包括狀態碼Stat,它描述程序執行的整體狀態
當遇到這些異常的時候,咱們簡單地讓處理器中止執行指令。在更完整的設計中,處理器一般會調用一個異常處理程序,這個過程被指定用來處理遇到的某種類型的異常
SEQ處理器,每一個時鐘週期時間,SEQ執行處理一條完整指令所需的全部步驟。不過,這須要一個很長的時鐘週期時間,所以時鐘週期頻率會低到不可接受,而最終要實現的是流水線化的處理器
一般,處理一條指令包括不少操做。將它們組織成某個特殊的階段序列,即便指令的動做差別很大,但全部的指令都遵循統一的序列。每一步的具體處理取決於正在執行的指令。
取指(fetch):取指階段從內存讀取指令字節,地址爲程序計數器(PC)的值。從指令中抽取出指令指示符字節的兩個四位部分,稱爲icode(指令代碼)和ifun(指令功能)。它可能取出一個寄存器指示符字節(寄存器ID),指明一個或兩個寄存器操做數
譯碼(decode):譯碼階段從寄存器文件讀入最多兩個操做數
執行(execute):在執行階段,算術/邏輯單元(ALU)要麼執行指令指明的操做(根據ifun的值),計算內存引用的有效地址,要麼增長或減小棧指針。在此可能設置條件碼,對一條條件指令來講,這個階段會檢驗條件碼和傳送條件,若是條件成立,則更新目標寄存器。一樣,對一條跳轉指令來講,這個階段會決定是否是應該選擇分支
訪存(memory):訪存階段能夠將數據寫入內存,或者從內存讀出數據。
寫回(write back):寫回階段最多能夠寫兩個結果到寄存器文件
更新PC(PC update):將PC設置成下一條指令的地址
----SEQ硬件結構
----亂序處理器框圖
本章作了一個大概的瀏覽,若是已經上過相關計算機基礎課程,我想已經對存儲器技術及在計算機中的地位有了必定的瞭解,這本書講的很詳細,這裏只作個小結,方便往後須要時再去翻閱
基於存儲技術包括隨機存儲器(RAM)、非易失性存儲器(ROM)和磁盤。RAM有兩種基本類型。靜態RAM(SRAM)快一些,可是也貴一些,它便可以用做CPU芯片上的高速緩存,也能夠也用做芯片下的高速緩存。動態RAM(DRAM)慢一些,也便宜一些,用做主存和圖形幀緩衝區。即便是在關電的時候,ROM也能保持他們的信息,能夠用來存儲固件。旋轉磁盤是機械的非易失性存儲設備,以每一個位很低的成本保存大量的數據,可是其訪問時間比DRAM長不少。固態硬盤(SSD)基於非易失性的閃存,對某些應用來講,愈來愈成爲旋轉磁盤的具備吸引力的替代產品.
通常而言,較快的存儲技術每一個位會更貴,並且容量更小。這些技術的價格和性能屬性正在以顯著不一樣的速度變化着。特別的,DRAM和磁盤訪問時間遠遠大於CPU週期時間。系統經過將存儲器組織成存儲設備的層次結構來彌補這些差別,在這個層次結構中,較小、較快的設備在頂部,較大、較慢的設備在底部。由於編寫良好的程序有好的局部性,大多數數據均可以較高層獲得服務,若是就是存儲系統能以較高層的速度運行,但卻有較低層的成本和容量.
連接(linking)是將各類代碼和數據片斷收集並組合成爲一個單一文件的過程,這個文件可被加載(複製)到內存並執行。
在早期的計算機系統中,連接是手動執行的。在現代系統中,連接是由叫連接器(linker)的程序自動執行的。連接器在軟件開發中扮演着一個關鍵的角色,由於它們使得分離編譯成爲可能,不用擔憂牽一髮動全身。
大多數編譯系統提供編譯器驅動程序,它表明用戶在須要時調用語言預處理器、編譯器、彙編器和連接器
舉例:
linux> gcc -og -o add main.c sum.c
執行完該指令後,ASCII碼源文件翻譯成可執行目標文件過程以下
這個過程在前面已經詳述過了 --> hello.c經歷的四個階段
可重定位目標文件由各類不一樣的代碼和數據節(section)組成,每一節都是一個連續的字節序列
爲了構造可執行文件,連接器必須完成兩個主要任務:
目標文件是字節塊的集合。這些塊包含程序代碼,有些包含程序數據,而其餘的則包含引導連接器和加載器的數據結構。
目標文件有三種形式:
編譯器和彙編器生成可重定位目標文件(包括共享目標文件)。連接器生成可執行目標文件。從技術上來講,一個目標模塊就是一個字節序列,而一個目標文件就是一個以文件形式存放在磁盤中的目標模塊。
如圖是一個典型的ELF可重定位目標文件格式。ELF頭以一個16字節的序列開始,這個序列描述了生成該文件的系統的字的大小和字節順序。夾在ELF頭和節頭部表之間的都是節,節頭部表描述中間各個不一樣節的位置和大小,其中目標文件中每一個節都有一個固定的條目。
.text:已編譯程序的機器代碼
.rodata:只讀數據
.data:已初始化的全局和靜態C變量。局部C變量在運行時被保存在棧中,即不出如今.data節中,也不出如今.bss節(表示未初始化的數據)中
.symtab:一個符號表,它存放在程序中定義和引用的函數和全局變量的信息
.rel.text:一個.text節中位置的列表,當連接器把這個目標文件和其餘文件組合時,須要修改這些位置
.rel.data:被模塊引用或定義的全部全局變量的重定位信息
.debug:一個調試符號表,其條目是程序中定義的局部變量和類型定義,程序中定義和引用的全局變量,以及原始的C的源文件
.line:原始C源程序中行號和.text節中機器指令之間的映射
.strtab:一個字符串表,其內容包括.symtab和.debug節中的符號表,以及節頭部表的節名字。字符串表就是以null結尾的字符串的序列
在符號解析階段,連接器從左到右按照它們在編譯器驅動程序命令行上出現的順序來掃描可重定位目標文件和存檔文件,將掃描到的符號分爲三類,其中一類就是未解析的符號,即須要利用靜態庫來解析的引用符號
連接器一般會利用靜態庫來解析引用:全部的編譯系統都有提供一種機制,將全部相關的目標模塊打包成爲一個單獨的文件,它能夠用做連接器的輸入。當連接器構造一個輸出的可執行文件時,它只複製靜態庫裏被應用程序引用的目標模塊,例如libc.a中的printf.o模塊
重定位節和符號定義 使程序中的每條指令和全局變量都有惟一的運行時內存地址
重定位節中的符號引用 連接器修改代碼節和數據節中對每一個符號的引用,使得它們指向正確的運行時的地址。要執行這一步,連接器依賴於可重定位目標模塊中的重定位條目(rel.text、rel.data)
當彙編器生成一個目標模塊時,它並不知道數據和代碼最終將放在內存中的什麼位置。它也不知道這個模塊引用的任何外部定義的函數或者全局變量的位置。因此,不管什麼時候彙編器遇到對最終位置未知的目標引用,它就會生成一個重定位條目,告訴連接器在將目標文件合併成可執行文件時如何修改這個引用。代碼重定位條目放在.rel.text中,已初始化數據的重定位條目放在.rel.data中
ELF定義了32種不一樣的重定位類型,其中有兩種最基本的重定位類型:
R_X86_64_PC32 重定位一個使用32位PC相對地址的引用
R_X86_64_32 重定位一個使用32位絕對地址的引用,CPU直接在指令中編碼的32值做爲有效地址
舉例:反編譯一個可重定位文件,其中一個指令有以下的重定位的形式
指令地址 指令編碼 指令 重定位地址 <符號引用> 4004de: e8 05 00 00 00 callq 4004e8 <sum>
一個典型的ELF可執行文件包含加載程序到內存並運行它所需的全部信息
ELF頭描述文件的整體格式
起始地址,也就是程序的入口點(entry point)也就是當程序運行時要執行的第一條指令的地址
.text、.rodata和.data節與可重定位目標文件中的節是相識的,除了這些節已經被重定位到它們最終的運行時內存地址外
.init節定義了一個叫作_init的函數,程序的初始化代碼會調用它。由於可執行文件是徹底連接的(已被重定位),因此它不須要rel節
ELF可執行文件被設計得很容易加載到內存,可執行文件的連續的片(chunk)被映射到連續的內存段。程序頭部表描述了這種映射關係
off:目標文件中的偏移;vaddr/paddr:內存地址;align:對齊要求;filesz:目標文件中的段大小;memsz:內存中的段大小;flags:運行時訪問權限
這裏先只關注可執行目標文件的內容初始化兩個內存段。
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21 filesz 0x000000000000085c memsz 0x000000000000085c flags r-x LOAD off 0x0000000000000e10 vaddr 0x0000000000600e10 paddr 0x0000000000600e10 align 2**21 filesz 0x0000000000000230 memsz 0x0000000000000238 flags rw-
一、2兩行告訴咱們第一段(代碼段)有讀/執行 訪問權限,開始於內存地址0x400000處,總共的內存大小是0x85c字節,而且被初始化爲可執行目標文件的頭0x85c字節,其中包括ELF頭、程序頭部表以及.init、.text和.rodata節
三、4行告訴咱們第二段(數據段)有讀/寫 訪問權限,開始於內存地址0x600e10處,總的內存大小爲0x238字節,並用從目標文件中偏移0xe10處開始的.data節中的0x230個字節初始化.該段中剩下的8個字節對應於運行時將被初始化爲0的.bss數據
對於任何段s,連接器必須選擇一個起始地址vaddr,使得vaddr mod align=off mod align
這裏,off是目標文件中段的第一個節的偏移量,align是程序頭部中指定的對齊(2^21=0x200000)
舉例:
vaddr mod align = 0x600e10 mod 0x200000 = 0xe10
off mod align = 0xe10 mod 0x200000 = 0xe10
這個對齊要求是一種優化,使得當程序執行時,目標文件中的段可以頗有效率地傳送到內存中
首先shell會認爲prog是一個可執行目標文件,經過調用某個駐留在存儲器中稱爲加載器(loader)的操做系統代碼來運做它。任何Linux程序均可以經過調用execve
函數來調用加載器。
加載器將可執行目標文件中的代碼和數據從磁盤複製到內存中,而後經過跳轉到程序的第一條指令或入口點來運行該程序,這個將程序複製到內存並運行的過程叫作加載
每一個Linux程序都有一個運行時內存映像。在Linux x86-64系統中,代碼段老是從地址0x400000處開始,32位系統從0x08048000處開始,後面是數據段。運行時堆在數據段以後,經過調用malloc庫往上增加。堆後面的區域是爲共享模塊保留的。用戶棧老是從最大的合法用戶地址(248-1)開始,向較小內存地址增加。棧上的區域,從地址248開始,是爲內核中的代碼和數據保留的,所謂內核就是操做系統駐留在內存的部分
棧頂放在最大的合法用戶地址處
加載器實際是如何工做的?這裏作個概述
Linux系統中每一個程序都運行在一個進程上下文中,有本身的虛擬地址空間。當shell運行一個程序時,父shell進程生成一個子進程,它是父進程的一個複製。子進程經過execve系統調用啓動加載器。加載器刪除子進程現有的虛擬內存段,並建立一組新的代碼、數據、堆和棧段。新的棧和堆段被初始化爲零。經過將虛擬地址空間中的頁映射到可執行文件的頁大小的片,新的代碼和數據被初始化爲可執行文件的內容。最後,加載器跳轉到_start地址,它最終會調用應用程序的main函數。除了一些頭部信息,在加載過程當中沒有任何從磁盤到內存的數據複製。直到CPU引用一個被映射的虛擬頁時纔會進行復制,此時,操做系統利用它的頁面調度機制自動將頁面從磁盤傳送到內存
共享庫(shared library)是致力於解決靜態庫缺陷的一個現代創新產物。共享庫是一個目標模塊,在運行或加載時,能夠加載到任意的內存地址,並和一個內存中的程序連接起來。這個過程爲動態連接(dynamic linking),是由一個叫作動態連接器(dynamic linker)的程序執行的。共享庫也成爲共享目標(shared object),在Linux系統中一般用.so後綴
來表示。微軟的操做系統大量地使用了共享庫,它們稱爲DLL(動態連接庫)
沒有任何libvector.so的代碼和數據節真的被複制到可執行文件prog21中。反之,連接器複製了一些重定位和符號表信息,它們使得運行時能夠解析對libvector.so中代碼和數據的引用。
當加載器加載和運行可執行文件prog21時,加載部分連接的可執行文件prog21.接着,prog21中包含一個.interp節
,這一節包含動態連接器的路徑名,動態連接器自己就是一個共享目標,加載器不會像它一般所作的那樣將控制傳遞給應用,而是加載和運行這個動態連接器。而後,動態連接器經過執行重定位完成連接任務:重定位共享庫文本和數據到某個內存段,重定位共享庫定義的符號的引用
最後,動態連接器將控制傳遞給應用程序,從這一時刻開始,共享庫的位置就固定了,而且在程序執行的過程當中都不會改變
這是共享庫的一種使用情景,無需在編譯時將那些庫連接到庫中。如:微軟windows應用開發者分發軟件;構建高性能web服務器
共享庫的主要目的就是運行多個正在運行的進程共享內存中相同的庫代碼
爲解決多個進程共享程序的一個副本時,形成了庫在內存中的分配管理問題
現代系統以這樣一種方式編譯共享模塊的代碼段,使得能夠把它們加載到內存的任何位置而無需連接器修改。能夠加載而無需重定位的代碼稱爲位置無關代碼
在x86-64系統中,對同一個目標模塊中符號的引用是不須要特殊處理使之成爲PIC,能夠用PC相對尋址來編譯這些引用,構造目標文件時由靜態連接器重定位。
ldd:列出一個可執行文件在運行時所須要的共享庫
objdunp:全部二進制工具之母。可以顯示一個目標文件中全部的信息。它最大的做用是反彙編.text節中的二進制指令
size:列出目標文件中節的名字和大小
readelf:顯示一個目標文件的完整結構,包括ELF頭中編碼的全部信息
異常控制流(ECF)發生在計算機系統的各個層次,是計算機系統中提供併發的基本機制。
在硬件層,異常是由處理器中的事件觸發的控制流中的突變。控制流傳遞給一個軟件處理程序,該處理程序進行一些處理,而後返回控制給被中斷的控制流
有四種不一樣類型的異常:中斷
、故障
、終止
和陷阱
。當一個外部I/O設備設置了處理器芯片上的中斷管腳時,中斷會異步的發生。控制返回到故障指令後面的那條指令,一條指令的執行可能致使故障和終止同步發生。故障處理程序會從新啓動故障指令,而終止處理程序從不將控制返回給被中斷的流。最後,陷阱就像是用來實現嚮應用提供操做系統代碼的受控的入口點的系統調用的函數調用
在操做系統層,內核用ECF提供進程的基本概念。進程提供給應用兩個重要的抽象:1)邏輯控制流,它提供給每一個程序一個假象,好像它是在獨佔地使用處理器,2)私有地址空間,它提供給每一個程序一個假象,好像它是在獨佔地使用主存
在操做系統和應用程序之間的接口處,應用程序能夠建立子進程,等待它們的子進程中止或者終止,運行新的程序,以及捕獲來自其餘進程的信號。
最後,在應用層,C程序可使用非本地跳轉來規避正常的調用/返回棧規則,而且直接從一個函數分支到另外一個函數
程序和進程
程序時一堆代碼和數據;程序能夠做爲目標文件存在於磁盤上,或者做爲段存在於地址空間中。進程是執行中程序的一個具體的實例;程序老是運行在某個進程的上下文中。
fork函數在新的子進程中運行相同的程序,新的子進程是父進程的複製品。execve函數在當前進程的上下文中加載並執行一個新的程序。它會覆蓋當前進程的地址空間,但並無建立一個新進程。新的程序仍然有相同的PID,而且繼承了調用execve函數時已達開的全部文件描述符
虛擬內存是對主存的一個抽象。支持虛擬內存的處理器經過使用一種叫作虛擬尋址的間接形式來引用主存。處理器產生一個虛擬地址,在被髮送到主存以前,這個地址被翻譯成一個物理地址。從虛擬地址空間到物理地址空間的地址翻譯要求硬件和軟件緊密合做。專門的硬件經過使用頁表來翻譯虛擬地址,而頁表的內容是由操做系統提供的。
虛擬內存提供三個重要的功能。第一,它在主存中自動緩存最近使用的存放磁盤上的虛擬地址空間的內容。虛擬內存緩存中的塊叫作頁。對磁盤上頁的引用會觸發缺頁,缺頁將控制轉移到操做系統中的一個缺頁處理程序。缺頁處理程序將頁面從磁盤複製到主存緩存,若是必要,將寫回被驅逐的頁。第二,虛擬內存簡化了內存管理,進而又簡化了連接、在進程間共享數據、進程的內存分配以及程序加載。最後,虛擬內存經過在每條頁表條目中加入保護位,從而了簡化了內存保護。
地址翻譯的過程必須和系統中全部的硬件緩存的操做集成在一塊兒。大多數頁表條目位於L1高速緩存中,可是一個稱爲TLB的頁表條目的片上高速緩存,一般會消除訪問在L1上的頁表條目的開銷。
現代系統經過將虛擬內存片和磁盤上的文件片關聯起來,來初始化虛擬內存片,這個過程稱爲內存映射
。內存映射爲共享數據、建立新的進程以及加載程序提供了一種高效的機制。應用可使用mmap函數來手工地建立和刪除虛擬地址空間的區域。然而,大多數程序依賴於動態內存分配器,例如malloc,它管理虛擬地址空間區域內一個稱爲堆的區域。動態內存分配器是一個感受像系統級程序的應用級程序,它直接操做內存,而無需類型系統的不少幫助。分配器有兩種類型。顯式分配器
要求應用顯式地釋放它們的內存塊。隱式分配器
(垃圾收集器)自動釋聽任何未使用的和不可達的塊。
對於C程序員來講,管理和使用虛擬內存是一件困難和容易出錯的任務。常見的錯誤示例包括:間接引用壞指針
,讀取未初始化的內存
,容許棧緩衝區溢出
,假設指針和它們指向的對象大小相同,引用指針而不是它所指向的對象,誤解指針運算
,引用不存在的變量
,以及引發內存泄漏
。
後續內容不太想記錄,根據感興趣的點進行補充,便於查詢,其餘直接閱讀書籍理解下了解下便可,網絡編程和併發編程貌似不是我讀這本書太關注的重點,並且也將在其餘書籍研究這塊內容,讀到這附近我以爲我已經彌補了不熟悉的部分
虛擬內存是單機系統最重要的幾個底層原理之一,它由底層硬件和操做系統二者軟硬件結合來實現,是硬件異常,硬件地址翻譯,主存,磁盤文件和內核的完美交互。它主要提供了3個能力:
給全部進程提供一致的地址空間,每一個進程都認爲本身是在獨佔使用單機系統的存儲資源
保護每一個進程的地址空間不被其餘進程破壞,隔離了進程的地址訪問
根據緩存原理,上層存儲是下層存儲的緩存,虛擬內存把主存做爲磁盤的高速緩存,在主存和磁盤之間根據須要來回傳送數據,高效地使用了主存
包括幾塊內容
虛擬地址和物理地址
頁表
地址翻譯
虛擬內存相關的數據結構
內存映射
對於每一個進程來講,它使用到的都是虛擬地址,每一個進程都看到同樣的虛擬地址空間,對於32位計算機系統來講,它的虛擬地址空間是 0 - 2^32,也就是0 - 4G。對於64位的計算機系統來講,理論的虛擬地址空間是 0 - 2^64,遠高於目前常見的物理內存空間。虛擬地址空間不須要和物理地址空間同樣大小。
Linux內核把虛擬地址空間分爲兩部分: 用戶進程空間和內核進程空間,二者的比例通常是3:1,好比4G的虛擬地址空間,3G用戶用戶進程,1G用於內核進程。
在說CPU高速緩存的時候說過CPU只直接和寄存器和高速緩存打交道,CPU在執行進程的指令時要取一個實際的物理地址的值的時候主要有幾步:
把進程指令使用的虛擬地址經過MMU轉換成物理地址
把物理地址映射到高速緩存的緩存行
若是高速緩存命中就返回
若是不命中,就產生一個緩存缺失中斷,從主存相應的物理地址取值,並加載到高速緩存中。CPU從中斷中恢復,繼續執行中斷前的指令
因此高速緩存是和物理地址相映射的,進程指令中使用到的是虛擬地址。
在緩存原理中,數據都是按塊來進行邏輯劃分的,一次換入/換出的數據都是以塊爲最小單位,這樣提升了數據處理的性能。一樣的原理應用到具體的內存管理時,使用了頁(page)來表示塊,虛擬地址空間劃分爲多個固定大小的虛擬頁(Virtual Page, VP),物理地址空間劃分爲多個固定大小的物理頁(Physical Page, PP), 一般虛擬頁大小等於物理頁大小,這樣簡化了虛擬頁和物理頁的映射。虛擬頁的大小一般在4KB - 2MB之間。在JVM調優的時候有時候會使用2MB的大內存頁來提升GC的性能。
要明白一個重要的概念:
對於CPU來講,它的目標存儲器是物理內存,使用高速緩存作物理內存的緩存
一樣,對於虛擬內存來講,它的目標存儲器是磁盤空間,使用物理內存作磁盤的緩存
因此,從緩存原理的角度來理解,在任什麼時候刻,虛擬頁的集合都分爲3個不相交的子集:
未分配的頁,即沒有任何數據和這些虛擬頁關聯,不佔用任何磁盤空間
緩存的頁,即已經分配了的虛擬頁,而且已經緩存在具體的物理頁中
未緩存的頁,即已經爲磁盤文件分配了虛擬頁,可是尚未緩存到具體的物理頁中
虛擬內存系統和高速緩存系統同樣,須要判斷一個虛擬頁面是否緩存在DRAM(主存)中,若是命中,就直接找到對應的物理頁。若是不命中,操做系統須要知道這個虛擬頁對應磁盤的哪一個位置,而後根據相應的替換策略從DRAM中選擇一個犧牲的物理頁,把虛擬頁從磁盤中加載到DRAM物理主存中
虛擬內存的這種緩存管理機制是經過操做系統內核,MMU(內存管理單元)中的地址翻譯硬件和每一個進程存放在主存中的頁表(page table)數據結構來實現的。
頁表(page table)是存放在主存中的,每一個進程維護一個單獨的頁表。它是一種管理虛擬內存頁和物理內存頁映射和緩存狀態的數據結構。它邏輯上是由頁表條目(Page Table Entry, PTE)爲基本元素構成的數組。
數組的索引號對應着虛擬頁號
數組的值對應着物理頁號
數組的值能夠留出幾位來表示有效位,權限控制位。有效位爲1的時候表示虛擬頁已經緩存。有效位爲0,數組值爲null時,表示未分配。有效位爲0,數組值不爲null,表示已經分配了虛擬頁,可是還未緩存到具體的物理頁中。權限控制位有可讀,可寫,是否須要root權限
SRAM緩存表示位於CPU和主存之間的L一、L2和L3高速緩存。
DARM緩存的命中稱爲頁命中,不命中稱爲缺頁。舉個例子來講,
CPU要訪問的一個虛擬地址在虛擬頁3上(VP3),經過地址翻譯硬件從頁表的3號頁表條目中取出內容,發現有效位0,即沒有緩存,就產生一個缺頁異常
缺頁異常調用內核的缺頁異常處理程序,它會根據替換算法選擇一個DRAM中的犧牲頁,好比PP3。PP3中已經緩存了VP4對應的磁盤文件的內容,若是VP4的內容有改動,就刷新到磁盤中去。而後把VP3對應的磁盤文件內容加載到PP3中。而後更新頁表條目,把PTE3指向PP3,並修改PTE4,再也不指向PP3.
缺頁異常處理程序返回後從新啓動缺頁異常前的指令,這時候虛擬地址對應的內容已經緩存在主存中了,頁命中也可讓地址翻譯硬件正常處理了
磁盤和主存之間傳送頁的活動叫作交換(swapping)或者頁面調度(頁面調入,頁面調出)。現代操做系統都採用按需調度的策略,即不命中發生時才調入頁面。操做系統都會在主存中分配一塊交換區(swap)來做緩衝區,加速頁面調度。
因爲頁的交換會引發磁盤流量,因此具備好的局部性的程序能夠大大減小磁盤流量,提升性能。而若是局部性很差產生大量缺頁,從而致使不斷地在磁盤和主存交換頁,這種現象叫緩存顛簸。能夠用Unix的函數getrusage來統計缺頁的次數
現代操做系統都採用多級頁表的方式來壓縮頁表的大小。舉個例子,
對於32位的機器來講,支持4G的虛擬內存大小,若是每一個頁是4KB大小,那麼採用一級頁表的話,須要10^6個頁表條目PTE。32位機器的頁表條目是4個字節,那麼頁表須要4MB大小的空間。
假設使用4MB大小的頁,那麼只須要103個頁表項。假設每一個4MB大小的頁又分爲4KB大小的子頁,那麼每一個4MB大小的頁須要103個的頁表項來指向子頁。也就是說能夠分爲兩級頁表,第一級頁表項只須要4KB大小的頁表項,每一個一級頁表項又指向一個4KB大小的二級頁表,二級頁表項則指向實際的物理頁。
頁表項加載是按需加載的,沒有分配的虛擬頁不須要創建頁表項, 因此能夠一開始只創建一級頁表項,而二級頁表項按需建立,這樣大大壓縮了頁表的空間。
地址翻譯就是把N個元素的虛擬地址空間(VAS)映射到M個元素的物理地址空間(PAS)的過程。
總結一下地址翻譯的過程:
CPU拿到一個虛擬地址,分爲兩步,先經過頁表機制肯定該地址所在虛擬頁的內容是否從磁盤加載到物理內存頁中,而後經過高速緩存機制從該物理地址中取到數據
地址翻譯硬件要把這個虛擬地址翻譯成一個物理地址,從而能夠再根據高速緩存的映射關係,把這個物理地址對應的值找到
地址翻譯硬件利用頁表數據結構,TLB硬件緩存等技術,目的只是把一個虛擬地址映射到一個物理地址。要記住DRAM緩存是全相聯的,因此一個虛擬地址和一個物理地址是動態關聯的,不能直接根據虛擬地址推導出物理地址,必須根據DRAM從磁盤把數據緩存到DRAM時存到頁表時存的實際物理頁才能獲得實際的物理地址,用物理頁PPN + VPO就能算出實際的物理地址 (VPO = PPO,因此直接用VPO便可)。 PPN的值是存在頁表條目PTE中的。地址翻譯作了一堆工做,就是爲了找到物理頁PPN,而後根據VPO頁面偏移量,就能定位到實際的物理地址。
獲得實際物理地址後,根據高速緩存的原理,把一個物理地址映射到高速緩存具體的組,行,塊中,找到實際存儲的數據。
Linux把虛擬內存劃分紅區域area的集合,每一個存在的虛擬頁面都屬於一個area。一個area包含了連續的多個頁。Linux經過area相關的數據結構來靈活地管理虛擬內存。
內核爲每一個進程維護了一個單獨的任務結構 task_struct
task_struct的mm指針指向了mm_struct,該結構描述了虛擬內存的運行狀態
mm_struct的pgd指針指向該進程的一級頁表的基地址。mmap指針指向了vm_area_struct鏈表
vm_area_struct是描述area結構的一個鏈表,鏈表節點的幾個重要屬性以下:vm_start表示area的開始位置,vm_end表示area的結束位置,vm_prot描述了area內的頁的讀寫權限,vm_flags描述該area內的頁面是與其餘進程共享仍是進程私有, vm_next指向下一個area節點
在Linux系統中,當MMU翻譯一個虛擬地址發生缺頁異常時,跳轉到內核的缺頁異常處理程序。
Linux的缺頁異常處理程序會先檢查一個虛擬地址是哪一個area內的地址。只須要比較全部area結構的vm_start和vm_end就能夠知道。area都是一個連續的塊。若是這個虛擬地址不屬於任何一個area,將發生一個段錯誤,終止進程
要訪問的目標地址是否有相應的讀寫權限,若是沒有,將觸發一個保護異常,終止進程
選擇一個犧牲頁,若是犧牲頁被修改過,那麼把它交換出去。從磁盤加載虛擬頁內容到物理頁,更新頁表
虛擬內存的目標存儲器是磁盤,因此虛擬內存區域是和磁盤中的文件對應的。初始化虛擬內存區域的內容時,會把虛擬內存區域和一個磁盤文件對象對應起來,這個過程叫內存映射(memory mapping)。虛擬內存能夠映射的磁盤文件對象包括兩種:
一個普通的磁盤文件,文件中的內容被分紅頁大小的塊。由於按需進行頁面調度,只有真正須要讀取這些虛擬頁時,纔會交換到主存
一個匿名文件,匿名文件是內核建立的,內容全是二進制0,它至關於一個佔位符,不會產生實際的磁盤流量。映射到匿名文件中的頁叫作請求二進制零的頁(demand zero page)
一旦一個虛擬頁面被初始化了,它就在一個由內核維護的專門的交換區(swap area)之間換來換去。
因爲內存映射機制,因此一個磁盤文件對象能夠被多個進程共享訪問,也能夠被多個進程對象私有訪問。若是是共享訪問,那麼一個進程對這個對象的修改會顯示到其餘進程。若是是私有訪問,內核會採用寫時拷貝copy on write的方式,若是一個進程要修改一個私有的寫時拷貝的對象,會產生一個保護故障,內核會拷貝這個私有對象,寫進程會在新的私有對象上修改,其餘進程仍指向原來的私有對象。
理解了內存映射機制就能夠理解幾個重要的函數:
fork函數會建立帶有獨立虛擬地址空間的新進程,內核會爲新進程建立各類數據結構,分配一個惟一的PID,把當前進程的mm_struct, area結構和頁表都複製給新進程。兩個進程的共享一樣的區域,這些區域包括共享的內存映射和私有的內存映射。私有的內存映射區域都被標記爲私有的寫時拷貝。若是新建的進程對這些虛擬頁作修改,那麼會觸發寫時拷貝,爲新的進程維護私有的虛擬地址空間。
mmap函數能夠建立新的虛擬內存area,並把磁盤對象映射到新建的area。
mmap能夠用做高效的操做文件的方式,直接把一個文件映射到內存,經過修改內存就至關於修改了磁盤文件,減小了普通文件操做的一次拷貝操做。普通文件操做時會先把文件內容從磁盤複製到內核空間管理的一塊虛擬內存區域area,而後內核再把內容複製到用戶空間管理的虛擬內存area。 mmap至關於建立了一個內核空間和用戶空間共享的area,文件的內容只須要在這個area對應的物理內存和磁盤文件之間交換便可。
mmap也能夠經過映射匿名文件的方式來分配內存空間。好比malloc當要求分配的內存大小超過了MMAP_THRESHOLD(默認128kb)時,會使用mmap私有的,匿名文件的方式來分配大塊的內存空間。
動態內存分配器維護者一個進程的虛擬內存區域,稱爲堆(heap)。向上生長(向更高地址),對每一個進程,內核維護着一個變量brk,它指向堆的頂部
程序使用動態內存分配的最重要的緣由是常常直到程序實際運行時才知道某些數據結構的大小
Linux提供了少許的基於Unix I/O模型的系統級函數,它們容許應用程序打開、關閉、讀和寫文件,提取文件的元數據,以及執行I/O重定向。Linux 的讀和寫操做會出現不足值,應用程序必須能正確地預計和處理這種狀況。應用程序不該直接調用UnixI/O函數,而應該使用RIO包,RIO包經過反覆執行讀寫操做,直到傳送完全部的請求數據,自動處理不足值。
Linux內核使用三個相關的數據結構來表示打開的文件。描述符表中的表項指向打開文件表中的表,項,而打開文件表中的表項又指向v-node表中的表項。每一個進程都有它本身單獨的描述符表,而全部的進程共享同一個打開文件表和v-node表。理解這些結構的通常組成就能使咱們清楚地理解文件共享和1/O重定向。
標準I/O庫是基於Unix I/O實現的,並提供了一組強大的高級I/O例程。對於大多數應用程序而言,標準I/O更簡單,是優於Unix I/O的選擇。然而,由於對標準I/O和網絡文件的一些相互不兼容的限制,UnixI/O比之標準I/O更該適用於網絡應用程序。
每一個網絡應用都是基於客戶端一服務器模型的。根據這個模型,一個應用是由一個服務器和一個或多個客戶端組成的。服務器管理資源,以某種方式操做資源,爲它的客戶端提供服務。客戶端-服務器模型中的基本操做是客戶端-服務器事務,它是由客戶端請求和跟隨其後的服務器響應組成的。
客戶端和服務器經過因特網這個全球網絡來通訊。從程序員的觀點來看,咱們能夠把因特網當作是一個全球範圍的主機集合,具備如下幾個屬性: 1)每一個因特網主機都有一個惟-一的32位名字,稱爲它的IP地址。2)IP地址的集合被映射爲一個因特網域名的集合。3)不一樣因特網主機上的進程可以經過鏈接互相通訊。
客戶端和服務器經過使用套接字接口創建鏈接。一個套接字是鏈接的一個端點,鏈接以文件描述符的形式提供給應用程序。套接字接口提供了打開和關閉套接字描述符的函數。客戶端和服務器經過讀寫這些描述符來實現彼此間的通訊。
Web服務器使用HTTP協議和它們的客戶端(例如瀏覽器)彼此通訊。瀏覽器向服務器請求靜態或者動態的內容。對靜態內容的請求是經過從服務器磁盤取得文件並把它返回給客戶端來服務的。對動態內容的請求是經過在服務器上一個子進程的上下文中運行一個程序並將它的輸出返回給客戶端來服務的。CGI標準提供了一.組規則,來管理客戶端如何將程序參數傳遞給服務器,服務器如何將這些參數以及其餘信息傳遞給子進程,以及子進程如何將它的輸出發送回客戶端。只用幾百行C代碼就能實現一個簡單可是有功效的Web服務器,它既能夠提供靜態內容,也能夠提供動態內容。
一個併發程序是由在時間上重疊的一組邏輯流組成的。三種不一樣的構建併發程序的機制:進程、I/O 多路複用和線程。咱們以一個併發網絡服務器做爲貫穿全章的應用程序。
進程是由內核自動調度的,並且由於它們有各自獨立的虛擬地址空間,因此要實現共享數據,必需要有顯式的IPC機制。事件驅動程序建立它們本身的併發邏輯流,這些邏輯流被模型化爲狀態機,用I/O多路複用來顯式地調度這些流。由於程序運行在一個單一進程中,因此在流之間共享數據速度很快並且很容易。線程是這些方法的混合。同基於進程的流同樣,線程也是由內核自動調度的。同基於I/O 多路複用的流同樣,線程是運行在一個單一進程的上下文中的,所以能夠快速而方便地共享數據。
不管哪一種併發機制,同步對共享數據的併發訪問都是一個困難的問題。提出對信號量的P和V操做就是爲了幫助解決這個問題。信號量操做能夠用來提供對共享數據的互斥訪問,也對諸如生產者-消費者程序中有限緩衝區和讀者-寫者系統中的共享對象這樣的資源訪問進行調度。一個併發預線程化的echo服務器提供了信號量使用場景的很好的例子。
併發也引人了其餘一些困難的問題。被線程調用的函數必須具備一種稱爲線程安全的屬性。咱們定義了四類線程不安全的函數,以及- -些將它們變爲線程安全的建議。可重人函數是線程安全函數的一個真子集,它不訪問任何共享數據。可重人函數一般比不可重人函數更爲有效,由於它們不須要任何同步原語。競爭和死鎖是併發程序中出現的另外一些困難的問題。當程序員錯誤地假設邏輯流該如何調度時,就會發生競爭。當一個流等待一個永遠不會發生的事件時,就會產生死鎖。