計算機組成原理 — 指令系統

目錄

 

 

前文列表

《計算機組成的基本硬件設備》
《計算機組成原理 — 馮諾依曼體系結構》
《計算機組成原理 — 中央處理器》程序員

指令系統

指令系統

指令系統決定了計算機的基本功能。計算機的性能與它所設置的指令系統有很大的關係,而指令系統的設置又與機器的硬件結構密切相關。指令系統的改進主要圍繞縮小指令與高級語言的語義差別以及有利於操做系統的優化而進行的,例如:高級語言中常常用到的 if 語句、do 語句,因此設置了功能較強的條件跳轉指令;爲了操做系統的實現和優化,設置了有控制系統狀態的特權指令、管理多道程序和多處理機系統的專用指令。編程

而後指令系統太過於複雜並不徹底是好事,在大多數場景中實際上只有算術邏輯運算、數據傳輸、跳轉和程序調用等幾十條指令會被頻繁的使用到,而須要大量硬件支持的大多數複雜的指令卻並不經常使用,由此會形成硬件資源的浪費。爲了解決這個問題,指令系統被分爲 精簡指令系統計算機(RISC) 和 複雜指令系統計算機(CISC)。數組

計算機指令集與程序指令

計算機指令集是 CPU 提供的可執行指令的集合;而程序所描述的指令是程序員但願在 CPU 上執行的指令,這些指令的範圍不會超出計算機指令集的範圍。不一樣類型的 CPU 可以理解不一樣的 「語言」,即具備不一樣的計算機指令集(Instruction Set)。同一個程序能夠在相同型號的 CPU 中運行,反之則沒法運行。sass

CPU 運行一個計算機程序的本質就是運行這個程序所描述的指令,有如咱們在 Linux 操做系統上執行指令通常,只是前者由 CPU 硬件支持。一個計算機程序一般由成千上萬條指令組成,CPU 顯然不能存放全部的指令,而是將這些指令存放在存儲器中,只有在運行時纔會被 CPU 讀取。又由於現代計算機被設計爲能夠運行多種程序,存儲器被各類各樣的程序共享着,因此存儲器也不會持久化的保存它們。而是當咱們要運行(啓動)某個程序時,纔會將其加載到存儲器中,最終再由 CPU 從存儲器中逐一讀取其指令。咱們常見的內部存儲器多爲 RAM(隨機存儲器),這是一種被設計成掉電就會自動重置的存儲設備。性能優化

以上就是馮·諾依曼機的典型特性,因此又稱之爲 「存儲程序計算機」。馮·諾依曼體系結構解決了計算機實現領域的一個重要難題:如何可以動態加載程序指令。解決了這個問題,「計算器」 才得以成爲 「計算機」,咱們今天才得以在計算機上運行各類各樣的應用程序。markdown

注:計算器的 「程序」 是焊死在主板上的。架構

指令格式

計算機是經過執行指令來處理各類數據的,爲了瞭解數據的來源、操做結果的去向及所執行的操做類型,一條計算機指令通常包含如下信息。編程語言

  1. 操做碼:說明操做的性質和功能(e.g. 加減乘除、數據傳輸),長度有指令系統的指令條數決定(e.g. 256 條指令,則操做碼須要 8 位長度)。
  2. 操做數的地址:CPU 經過該地址讀取到所須要的操做數,多是存儲器的地址,也多是寄存器的地址。
  3. 操做結果的儲存地址:保存操做結果數據的地址。
  4. 下一條指令的地址:當程序順序執行時,下一條指令的地址又程序計數器(PC)給出,僅當改變程序的執行順序時(e.g. 跳轉、函數調用),下一條指令的支持纔會有指令自己給出。

綜上,指令格式主要有 操做碼 和 地址碼 組成。須要注意的是,在指令字長較長的計算機中,操做碼的長度通常是固定的,而且由指令集的數量決定。但在指令字較短的計算機中,爲了可以充分利用指令字的位數,在有限的長度中實現更多的指令集數目,因此其操做碼長度被設計成是可變的,即把它們的操做碼在必要的時候擴充到地址碼字段。這就是所謂的 指令操做碼擴展技術。指令字的長度與 CPU 的位數密切相關。函數

指令類型

平常使用的 Intel CPU 大概有 2000 多條 CPU 指令。能夠分爲如下 5 大類型:性能

  • 算術類指令:加減乘除。
  • 數據傳輸類指令:變量賦值、讀寫內存數據。
  • 邏輯類指令:與或非。
  • 條件分支類指令:條件判斷語句。
  • 無條件跳轉指令:方法、函數的調用跳轉。

在這裏插入圖片描述
繼續細分的話,具備以下指令類型:

  • 算術邏輯運算指令
  • 移位操做指令
    • 算術移位
    • 邏輯移位
    • 循環移位
  • 矢量運算指令(矩陣運算)
  • 浮點運算指令
  • 十進制運算指令
  • 字符串處理指令
    • 字符串傳送
    • 字符串比較
    • 字符串查詢
    • 字符串轉換
  • 數據傳輸指令
    • 寄存器與寄存器傳輸
    • 寄存器與主存儲器單元傳輸
    • 存儲器單元與存儲器單元傳輸
    • 數據交換(源操做數與目的操做下互換)
  • 轉移指令
    • 條件轉移
    • 無條件轉移
    • 過程調用與返回
    • 陷阱
  • 堆棧及堆棧操做指令
  • I/O 指令
  • 特權指令
  • 多處理機指令(在多處理器系統中保證共享數據的一致性等)
  • 控制指令

指令尋址

指令尋址,便是根據指令字的地址碼來獲取到實際的數據,尋址的方式跟硬件關係密切,不一樣的計算機有不一樣的尋址方式。有的計算機尋址方式種類少,因此會直接在操做碼上表示尋址方式;有些計算機的尋址方式種類多,就會在指令字中添加一個特別用於標記尋址方式的字段,例如:假設該字段具備 3 位,那麼就能夠表示 8 種尋址方式。

NOTE:尋址方式與 CPU 內的寄存器設計密切相關。

在這裏插入圖片描述

直接尋址:指令字的地址碼直接給出了操做數在存儲器中的地址,是最簡單的尋址方式。

間接尋址:指令字的地址碼所指向的寄存器或存儲器的內容並非真實的操做數,而是操做數的地址。間接尋址經常使用於跳轉指令,只要修改寄存器或存儲器的地址就能夠實現跳轉到不一樣的操做數上。

相對尋址:把程序計數器(PC)的內容,即當前執行指令的地址與地址碼部分給出的偏移量(Disp)之和做爲操做數的地址。這種尋址方式一樣經常使用於跳轉(轉移)指令,當程序執行到本條指令後,跳轉到 PC+Disp。

當即數尋址:即地址碼自己就是一個操做數的尋址方式,該方式的特色就是數據塊(由於實際上沒有尋址),但操做數固定。經常使用於爲某個寄存器或存儲器單元賦初值,或提供一個常數。

通用寄存器尋址:CPU 中大概會有幾個到幾十個通用寄存器用於臨時儲存操做數、操做數的地址或中間結果,指令字的地址碼能夠指向這些寄存器。通用寄存器具備地址短,存取速度快的特性,因此地址碼指向通用寄存器的指令的長度也會更短,節省存儲空間,執行效率更快。常被用於執行速度要求嚴格的指令中。

基址寄存器尋址:基址,即基礎地址,基址寄存器就是存放基址的寄存器,能夠是一個專用寄存器,也可使用通用寄存器來充當基址寄存器。執行指令時,須要將基址與指令字的地址碼結合獲得完成的地址,此時的地址碼充當着偏移量(位移量)的角色。當存儲器容量較大時,直接尋址方式是沒法存取到全部存儲單元的,因此一般會採用 分段 或 分頁 的內存管理方式。此時,段或頁的首地址就會存放於基址寄存器中,而指令字的地址碼就做爲段或頁的長度,這樣只要修改基址寄存器的內容就能夠訪問到存儲器的任意單元了。這種尋址方式常被用於爲程序或數據分配存儲區,與虛擬地址實現密切相關。基址寄存器尋址方式解決了程序在存儲器中的定位存儲單元和擴大 CPU 尋址空間的問題。

變址寄存器尋址:變址寄存器內的地址與指令字地址之和獲得了實際的有效地址,若是 CPU 中存在基址寄存器,那麼就還得加上基址地址。這種尋址方式經常使用於處理須要循環執行的程序,例如:循環處理數組,此時變址寄存器所改變的就是數組的下標了。

堆棧尋址:堆棧是有若干個連續的存儲器單元組成的先進後出(FILO)存儲區。堆棧是用於提供操做數和保存運算結果的主要存儲區,同時還主要用於暫存中斷和子程序調用時的線程數據及返回地址。

經過 MIPS 感覺指令字的設計

MIPS(Millions of Instructions Per Second)是一種最簡單的精簡指令集架構,由 MIPS 科技公司設計。MIPS 指令具備 32 位(最新版本爲 64 位),高 6 位爲操做碼(OPCODE),描述了指令的操做類型。其他 26 位具備 3 種格式:R、I 和 J。不一樣的指令類型和操做碼組合可以完成多種功能實現,以下:

在這裏插入圖片描述

加法算數指令 add $t0,$s2,$s1 的指令字及其對應的機器碼以下:
在這裏插入圖片描述

  • opcode:0
  • rs:表明第一個寄存器 s1 的地址是 17
  • rt:表明第二個寄存器 s2 的地址是 18
  • rd:表明目標臨時寄存器 t0 的地址是 8
  • shamt:0,表示不位移

最終加法算數指令 add $t0,$s2,$s1 的二進制機器碼錶示爲 000000 10001 10010 01000 00000 1000000(0X02324020)。能夠看見,機器碼中沒有保存任何實際的程序數據,而是保存了程序數據的儲存的地址,這也算是存儲程序計算機指令集設計的一大特色。

將高級語言翻譯成彙編代碼

爲何要保留彙編語言

彙編語言是與機器語言最接近的高級編程語言(或稱爲中級編程語言),彙編語言基本上與機器語言對應,即彙編指令和計算機指令是相對匹配的。雖然彙編語言具備與硬件的關係密切,佔用內存小,運行速度快等優勢,但也具備可讀性低、可重用性差,開發效率低下等問題。高級語言的出現是爲了解決這些問題,讓軟件開發變得更加簡單高效,易於協做。但高級語言也存在本身的缺陷,例如:難以編寫直接操做硬件設備的程序等。

因此爲了權衡上述的問題,最終彙編語言被做爲中間的狀態保留了下來。一些高級語言(e.g. C 語言)提供了與彙編語言之間的調用接口,彙編程序可做爲高級語言的外部過程或函數,利用堆棧在二者之間傳遞參數或參數的訪問地址。二者的源程序經過編譯或彙編生成目標文件(OBJ)以後再利用鏈接程序(linker)把它們鏈接成爲可執行文件即可在計算機上運行了。保留彙編語言還爲程序員提供一種調優的手段,不管是 C 程序仍是 Python 程序,當咱們要進行代碼性能優化時,瞭解程序的彙編代碼是一個不錯的切入點。

順序程序流

計算機指令是一種邏輯上的抽象設計,而機器碼則是計算機指令的物理表現。機器碼(Machine Code),又稱爲機器語言,本質是由 0 和 1 組成的數字序列。一條機器碼就是一條計算機指令。程序由指令組成,但讓人類使用機器碼來編寫程序顯然是不人道的,因此逐步發展了對人類更加友好的高級編程語言。這裏咱們須要瞭解計算機是如何將高級編程語言編譯爲機器碼的。

Step 1. 編寫高級語言程序。

// test.c int main() { int a = 1; int b = 2; a = a + b; } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Step 2. 編譯(Compile),將高級語言編譯成彙編語言(ASM)程序。

$ gcc -g -c test.c
  • 1

Step 3. 使用 objdump 命令反彙編目標文件,輸出可閱讀的二進制信息。下述左側的一堆數字序列就是一條條機器碼,右側 push、mov、add、pop 一類的就是彙編代碼。

$ objdump -d -M intel -S test.o test.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: int main() { 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp int a = 1; 4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1 int b = 2; b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 a = a + b; 12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 15: 01 45 fc add DWORD PTR [rbp-0x4],eax } 18: 5d pop rbp 19: c3 ret 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

NOTE:這裏的程序入口是 main() 函數,而不是第 0 條彙編代碼。
在這裏插入圖片描述

條件程序流

值得注意的是,某些特殊的指令,好比跳轉指令,會主動修改 PC 的內容,此時下一條地址就不是從存儲器中順序加載的了,而是到特定的位置加載指令內容。這就是 if…else 條件語句,while/for 循環語句的底層支撐原理。

Step 1. 編寫高級語言程序。

// test.c #include <time.h> #include <stdlib.h> int main() { srand(time(NULL)); int r = rand() % 2; int a = 10; if (r == 0) { a = 1; } else { a = 2; } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

Step 2. 編譯(Compile),將高級語言編譯成彙編語言。

$ gcc -g -c test.c
  • 1

Step 3. 使用 objdump 命令反彙編目標文件,輸出可閱讀的二進制信息。咱們主要分析 if…else 語句。

if (r == 0) 33: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0 37: 75 09 jne 42 <main+0x42> { a = 1; 39: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1 40: eb 07 jmp 49 <main+0x49> } else { a = 2; 42: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

首先進入條件判斷,彙編代碼爲 cmp 比較指令,比較數 1:DWORD PTR [rbp-0x4] 表示變量 r 是一個 32 位整數,數據在寄存器 [rbp-0x4] 中;比較數 2:0x0 表示常量 0 的十六進制。比較的結果會存入到 條件碼寄存器,等待被其餘指令讀取。當判斷條件爲 True 時,ZF 設置爲 1,反正設置爲 0。

條件碼寄存器(Condition Code)是一種單個位寄存器,它們的值只能爲 0 或者 1。當有算術與邏輯操做發生時,這些條件碼寄存器當中的值就隨之發生變化。後續的指令經過檢測這些條件碼寄存器來執行條件分支指令。經常使用的條件碼類型以下:

  • CF:進位標誌寄存器。最近的操做是最高位產生了進位。它能夠記錄無符號操做的溢出,當溢出時會被設爲 1。
  • ZF:零標誌寄存器,最近的操做得出的結果爲 0。當計算結果爲 0 時將會被設爲 1。
  • SF:符號標誌寄存器,最近的操做獲得的結果爲負數。當計算結果爲負數時會被設爲 1。
  • OF:溢出標誌寄存器,最近的操做致使一個補碼溢出(正溢出或負溢出)。當計算結果致使了補碼溢出時,會被設爲 1。

回到正題,PC 繼續自增,執行下一條 jnp 指令。jnp(jump if not equal)會查看 ZF 的內容,若爲 0 則跳轉到地址 42 <main+0x42>(42 表示彙編代碼的行號)。前文提到,當 CPU 執行跳轉類指令時,PC 就再也不經過自增的方式來得到下一條指令的地址,而是直接被設置了 42 行對應的地址。由此,CPU 會繼續將 42 對應的指令讀取到 IR 中並執行下去。

42 行執行的是 mov 指令,表示將操做數 2:0x2 移入到 操做數 1:DWORD PTR [rbp-0x8] 中。就是一個賦值語句的底層實現支撐。接下來 PC 恢復如常,繼續以自增的方式獲取下一條指令的地址。

在這裏插入圖片描述

循環程序流

  • C 語言代碼
// test.c int main() { int a = 0; int i; for (i = 0; i < 3; i++) { a += i; } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 計算機指令與彙編代碼
for (i = 0; i < 3; i++) b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0 12: eb 0a jmp 1e <main+0x1e> { a += i; 14: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 17: 01 45 fc add DWORD PTR [rbp-0x4],eax for (i = 0; i < 3; i++) 1a: 83 45 f8 01 add DWORD PTR [rbp-0x8],0x1 1e: 83 7d f8 02 cmp DWORD PTR [rbp-0x8],0x2 22: 7e f0 jle 14 <main+0x14> } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在這裏插入圖片描述

函數調用棧的工做原理

與普通的跳轉程序(e.g. if…else、while/for)不一樣,函數調用的特色在於具備迴歸(return)的特色,在調用的函數執行完以後會再次回到執行調用的 call 指令的位置,繼續往下執行。可以實現這個效果,徹底依賴堆棧(Stack)存儲區的特性。 首先咱們須要瞭解幾個概念。

  • 堆棧(Stack):是有若干個連續的存儲器單元組成的先進後出(FILO)存儲區,用於提供操做數、保存運算結果、暫存中斷和子程序調用時的線程數據及返回地址。經過執行堆棧的 Push(壓棧)和 Pop(出棧)操做能夠將指定的數據在堆棧中放入和取出。堆棧具備棧頂和棧底之分,棧頂的地址最低,而棧底的地址最高。堆棧的 FILO 的特性很是適用於函數調用的場景:父函數調用子函數,父函數在前,子函數在後;返回時,子函數先返回,父函數後返回。

  • 棧幀(Stack Frame):是堆棧中的邏輯空間,每次函數調用都會在堆棧中生成一個棧幀,對應着一個未運行完的函數。從邏輯上講,棧幀就是一個函數執行的環境,保存了函數的參數、函數的局部變量以及函數執行完後返回到哪裏的返回地址等等。棧幀的本質是兩個指針寄存器: EBP(基址指針,又稱幀指針)和 ESP(棧指針)。其中 EBP 指向幀底,而 ESP 指向棧頂。當程序運行時,ESP 是能夠移動的,大多數信息的訪問都經過移動 ESP 來完成,而 EBP 會一直處於幀低。EBP ~ ESP 之間的地址空間,就是當前執行函數的地址空間。

NOTE:EBP 指向當前位於系統棧最上邊一個棧幀的底部,而不是指向系統棧的底部。嚴格說來,「棧幀底部」 和 「系統棧底部」 不是同一個概念,而 ESP 所指的棧幀頂部和系統棧頂部是同一個位置。

在這裏插入圖片描述
簡單歸納一下函數調用的堆棧行爲,ESP 隨着當前函數的壓棧和出棧會不斷的移動,但因爲 EBP 的存在,因此當前執行函數棧幀的邊界是始終清晰的。當一個當前的子函數調用完成以後,EBP 就會跳到父函數棧幀的底部,而 ESP 也會隨其天然的來到父函數棧幀的頭部。因此,理解函數調用堆棧的運做原理,主要要掌握 EBP 和 ESP 的動向。下面以一個例子來講明。

NOTE:咱們習慣將將父函數(調用函數的函數)稱爲 「調用者(Caller)」,將子函數(被調用的函數)稱爲 「被調用者(Callee)」。

  • C 程序代碼
#include <stdio.h> int add(int a, int b) { int result = 0; result = a + b; return result; } int main(int argc, char *argv[]) { int result = 0; result = add(1, 2); printf("result = %d \r\n", result); return 0; } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 使用gcc編譯,而後gdb反彙編main函數,看看它是如何調用add函數的
(gdb) disassemble main Dump of assembler code for function main: 0x08048439 <+0>: push %ebp 0x0804843a <+1>: mov %esp,%ebp 0x0804843c <+3>: and $0xfffffff0,%esp 0x0804843f <+6>: sub $0x20,%esp 0x08048442 <+9>: movl $0x0,0x1c(%esp) # 給 result 變量賦 0 值 0x0804844a <+17>: movl $0x2,0x4(%esp) # 將第 2 個參數 argv 壓棧(該參數偏移爲esp+0x04) 0x08048452 <+25>: movl $0x1,(%esp) # 將第 1 個參數 argc 壓棧(該參數偏移爲esp+0x00) 0x08048459 <+32>: call 0x804841c <add> # 調用 add 函數 0x0804845e <+37>: mov %eax,0x1c(%esp) # 將 add 函數的返回值地址賦給 result 變量,做爲子函數調用完以後的迴歸點 0x08048462 <+41>: mov 0x1c(%esp),%eax 0x08048466 <+45>: mov %eax,0x4(%esp) 0x0804846a <+49>: movl $0x8048510,(%esp) 0x08048471 <+56>: call 0x80482f0 <printf@plt> 0x08048476 <+61>: mov $0x0,%eax 0x0804847b <+66>: leave 0x0804847c <+67>: ret End of assembler dump. (gdb) disassemble add Dump of assembler code for function add: 0x0804841c <+0>: push %ebp # 將 ebp 壓棧(保存函數調用者的棧幀基址) 0x0804841d <+1>: mov %esp,%ebp # 將 ebp 指向棧頂 esp(設置當前被調用函數的棧幀基址) 0x0804841f <+3>: sub $0x10,%esp # 分配棧空間(棧向低地址方向生長) 0x08048422 <+6>: movl $0x0,-0x4(%ebp) # 給 result 變量賦 0 值(該變量偏移爲ebp-0x04) 0x08048429 <+13>: mov 0xc(%ebp),%eax # 將第 2 個參數的值賦給 eax 寄存器(準備運算) 0x0804842c <+16>: mov 0x8(%ebp),%edx # 將第 1 個參數的值賦給 edx 寄存器(準備運算) 0x0804842f <+19>: add %edx,%eax # 運算器執行加法運算 (edx+eax),結果保存在 eax 寄存器中 0x08048431 <+21>: mov %eax,-0x4(%ebp) # 將運算結果 eax 賦給 result 變量 0x08048434 <+24>: mov -0x4(%ebp),%eax # 將 result 變量的值賦給 eax 寄存器(eax 的地址將做爲函數返回值) 0x08048437 <+27>: leave # 恢復函數調用者的棧幀基址(pop %ebp) 0x08048438 <+28>: ret # 返回(準備執行下條指令) End of assembler dump. 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 示意圖
    在這裏插入圖片描述
    可見,每一次函數調用,都會對調用者的棧幀基址 EBP 進行壓棧操做(爲了調用迴歸),而且因爲子函數的棧幀基址 EBP 來自於棧指針 ESP 而來(生成新的子函數的棧幀),因此各層函數的棧幀基址很巧妙的構成了一個鏈,即當前的棧幀基址指向下一層函數棧幀基址所在的位置。

在這裏插入圖片描述
由此當子函數執行完成時,ESP 依舊在棧頂,但 EBP 就跳轉到父函數的棧幀底部了,而且堆棧下一個彈出的就是子函數的調用迴歸點,最終程序流回到調用點並繼續往下執行。

經過函數調用堆棧的工做原理咱們能夠看出,不管程序中具備多少層的函數調用,或遞歸調用,只須要維護好每一個棧幀的 EBP 和 ESP 就能夠管理還函數之間的跳轉。但堆棧也是由容量限制的,若是函數調用的層級太多就會出現棧溢出的錯誤(Stack Overflow)。

程序在操做系統中的裝載與運行

一個程序在操做系統上運行須要經歷如下階段:

第一階段:獲得可執行文件

  1. 編譯(Compile)
  2. 彙編(Assemble)
  3. 連接(Link)

第二階段:裝載運行

  1. 裝載器(Loader)將可執行文件載入到內存
  2. CPU 從內存中可執行文件的程序入口開始讀取指令和數據,開始真正執行程序。

在這裏插入圖片描述
編譯和彙編的過程在上文中已經提到了,下面再繼續介紹連接的過程。

  • 子程序
// add_lib.c int add(int a, int b) { return a+b; } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 主函數
// link_example.c #include <stdio.h> int main() { int a = 10; int b = 5; int c = add(a, b); printf("c = %d\n", c); } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 編譯 C 程序獲得 Object 文件
$ gcc -g -c add_lib.c link_example.c
  • 1
  • 連接上述兩個 Object 文件獲得一個可執行文件
$ gcc -o link-example add_lib.o link_example.o

$ ./link-example
c = 15
  • 1
  • 2
  • 3
  • 4

區別於 Object 文件,真正的可執行文件的內容以下:

$ objdump -d -M intel -S link-example link-example: file format elf64-x86-64 Disassembly of section .init: 00000000004003c8 <_init>: 4003c8: 48 83 ec 08 sub rsp,0x8 4003cc: 48 8b 05 25 0c 20 00 mov rax,QWORD PTR [rip+0x200c25] # 600ff8 <__gmon_start__> 4003d3: 48 85 c0 test rax,rax 4003d6: 74 05 je 4003dd <_init+0x15> 4003d8: e8 43 00 00 00 call 400420 <.plt.got> 4003dd: 48 83 c4 08 add rsp,0x8 4003e1: c3 ret Disassembly of section .plt: 00000000004003f0 <.plt>: 4003f0: ff 35 12 0c 20 00 push QWORD PTR [rip+0x200c12] # 601008 <_GLOBAL_OFFSET_TABLE_+0x8> 4003f6: ff 25 14 0c 20 00 jmp QWORD PTR [rip+0x200c14] # 601010 <_GLOBAL_OFFSET_TABLE_+0x10> 4003fc: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 0000000000400400 <printf@plt>: 400400: ff 25 12 0c 20 00 jmp QWORD PTR [rip+0x200c12] # 601018 <printf@GLIBC_2.2.5> 400406: 68 00 00 00 00 push 0x0 40040b: e9 e0 ff ff ff jmp 4003f0 <.plt> 0000000000400410 <__libc_start_main@plt>: 400410: ff 25 0a 0c 20 00 jmp QWORD PTR [rip+0x200c0a] # 601020 <__libc_start_main@GLIBC_2.2.5> 400416: 68 01 00 00 00 push 0x1 40041b: e9 d0 ff ff ff jmp 4003f0 <.plt> Disassembly of section .plt.got: 0000000000400420 <.plt.got>: 400420: ff 25 d2 0b 20 00 jmp QWORD PTR [rip+0x200bd2] # 600ff8 <__gmon_start__> 400426: 66 90 xchg ax,ax Disassembly of section .text: 0000000000400430 <_start>: 400430: 31 ed xor ebp,ebp 400432: 49 89 d1 mov r9,rdx 400435: 5e pop rsi 400436: 48 89 e2 mov rdx,rsp 400439: 48 83 e4 f0 and rsp,0xfffffffffffffff0 40043d: 50 push rax 40043e: 54 push rsp 40043f: 49 c7 c0 f0 05 40 00 mov r8,0x4005f0 400446: 48 c7 c1 80 05 40 00 mov rcx,0x400580 40044d: 48 c7 c7 31 05 40 00 mov rdi,0x400531 400454: e8 b7 ff ff ff call 400410 <__libc_start_main@plt> 400459: f4 hlt 40045a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0] 0000000000400460 <deregister_tm_clones>: 400460: b8 37 10 60 00 mov eax,0x601037 400465: 55 push rbp 400466: 48 2d 30 10 60 00 sub rax,0x601030 40046c: 48 83 f8 0e cmp rax,0xe 400470: 48 89 e5 mov rbp,rsp 400473: 77 02 ja 400477 <deregister_tm_clones+0x17> 400475: 5d pop rbp 400476: c3 ret 400477: b8 00 00 00 00 mov eax,0x0 40047c: 48 85 c0 test rax,rax 40047f: 74 f4 je 400475 <deregister_tm_clones+0x15> 400481: 5d pop rbp 400482: bf 30 10 60 00 mov edi,0x601030 400487: ff e0 jmp rax 400489: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0] 0000000000400490 <register_tm_clones>: 400490: b8 30 10 60 00 mov eax,0x601030 400495: 55 push rbp 400496: 48 2d 30 10 60 00 sub rax,0x601030 40049c: 48 c1 f8 03 sar rax,0x3 4004a0: 48 89 e5 mov rbp,rsp 4004a3: 48 89 c2 mov rdx,rax 4004a6: 48 c1 ea 3f shr rdx,0x3f 4004aa: 48 01 d0 add rax,rdx 4004ad: 48 d1 f8 sar rax,1 4004b0: 75 02 jne 4004b4 <register_tm_clones+0x24> 4004b2: 5d pop rbp 4004b3: c3 ret 4004b4: ba 00 00 00 00 mov edx,0x0 4004b9: 48 85 d2 test rdx,rdx 4004bc: 74 f4 je 4004b2 <register_tm_clones+0x22> 4004be: 5d pop rbp 4004bf: 48 89 c6 mov rsi,rax 4004c2: bf 30 10 60 00 mov edi,0x601030 4004c7: ff e2 jmp rdx 4004c9: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0] 00000000004004d0 <__do_global_dtors_aux>: 4004d0: 80 3d 55 0b 20 00 00 cmp BYTE PTR [rip+0x200b55],0x0 # 60102c <_edata> 4004d7: 75 11 jne 4004ea <__do_global_dtors_aux+0x1a> 4004d9: 55 push rbp 4004da: 48 89 e5 mov rbp,rsp 4004dd: e8 7e ff ff ff call 400460 <deregister_tm_clones> 4004e2: 5d pop rbp 4004e3: c6 05 42 0b 20 00 01 mov BYTE PTR [rip+0x200b42],0x1 # 60102c <_edata> 4004ea: f3 c3 repz ret 4004ec: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 00000000004004f0 <frame_dummy>: 4004f0: 48 83 3d 28 09 20 00 cmp QWORD PTR [rip+0x200928],0x0 # 600e20 <__JCR_END__> 4004f7: 00 4004f8: 74 1e je 400518 <frame_dummy+0x28> 4004fa: b8 00 00 00 00 mov eax,0x0 4004ff: 48 85 c0 test rax,rax 400502: 74 14 je 400518 <frame_dummy+0x28> 400504: 55 push rbp 400505: bf 20 0e 60 00 mov edi,0x600e20 40050a: 48 89 e5 mov rbp,rsp 40050d: ff d0 call rax 40050f: 5d pop rbp 400510: e9 7b ff ff ff jmp 400490 <register_tm_clones> 400515: 0f 1f 00 nop DWORD PTR [rax] 400518: e9 73 ff ff ff jmp 400490 <register_tm_clones> 000000000040051d <add>: // add_lib.c int add(int a, int b) { 40051d: 55 push rbp 40051e: 48 89 e5 mov rbp,rsp 400521: 89 7d fc mov DWORD PTR [rbp-0x4],edi 400524: 89 75 f8 mov DWORD PTR [rbp-0x8],esi return a+b; 400527: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 40052a: 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 40052d: 01 d0 add eax,edx } 40052f: 5d pop rbp 400530: c3 ret 0000000000400531 <main>: // link_example.c #include <stdio.h> int main() { 400531: 55 push rbp 400532: 48 89 e5 mov rbp,rsp 400535: 48 83 ec 10 sub rsp,0x10 int a = 10; 400539: c7 45 fc 0a 00 00 00 mov DWORD PTR [rbp-0x4],0xa int b = 5; 400540: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x5 int c = add(a, b); 400547: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8] 40054a: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 40054d: 89 d6 mov esi,edx 40054f: 89 c7 mov edi,eax 400551: b8 00 00 00 00 mov eax,0x0 400556: e8 c2 ff ff ff call 40051d <add> 40055b: 89 45 f4 mov DWORD PTR [rbp-0xc],eax printf("c = %d\n", c); 40055e: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc] 400561: 89 c6 mov esi,eax 400563: bf 10 06 40 00 mov edi,0x400610 400568: b8 00 00 00 00 mov eax,0x0 40056d: e8 8e fe ff ff call 400400 <printf@plt> } 400572: c9 leave 400573: c3 ret 400574: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 40057b: 00 00 00 40057e: 66 90 xchg ax,ax 0000000000400580 <__libc_csu_init>: 400580: 41 57 push r15 400582: 41 89 ff mov r15d,edi 400585: 41 56 push r14 400587: 49 89 f6 mov r14,rsi 40058a: 41 55 push r13 40058c: 49 89 d5 mov r13,rdx 40058f: 41 54 push r12 400591: 4c 8d 25 78 08 20 00 lea r12,[rip+0x200878] # 600e10 <__frame_dummy_init_array_entry> 400598: 55 push rbp 400599: 48 8d 2d 78 08 20 00 lea rbp,[rip+0x200878] # 600e18 <__init_array_end> 4005a0: 53 push rbx 4005a1: 4c 29 e5 sub rbp,r12 4005a4: 31 db xor ebx,ebx 4005a6: 48 c1 fd 03 sar rbp,0x3 4005aa: 48 83 ec 08 sub rsp,0x8 4005ae: e8 15 fe ff ff call 4003c8 <_init> 4005b3: 48 85 ed test rbp,rbp 4005b6: 74 1e je 4005d6 <__libc_csu_init+0x56> 4005b8: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0] 4005bf: 00 4005c0: 4c 89 ea mov rdx,r13 4005c3: 4c 89 f6 mov rsi,r14 4005c6: 44 89 ff mov edi,r15d 4005c9: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 4005cd: 48 83 c3 01 add rbx,0x1 4005d1: 48 39 eb cmp rbx,rbp 4005d4: 75 ea jne 4005c0 <__libc_csu_init+0x40> 4005d6: 48 83 c4 08 add rsp,0x8 4005da: 5b pop rbx 4005db: 5d pop rbp 4005dc: 41 5c pop r12 4005de: 41 5d pop r13 4005e0: 41 5e pop r14 4005e2: 41 5f pop r15 4005e4: c3 ret 4005e5: 90 nop 4005e6: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 4005ed: 00 00 00 00000000004005f0 <__libc_csu_fini>: 4005f0: f3 c3 repz ret Disassembly of section .fini: 00000000004005f4 <_fini>: 4005f4: 48 83 ec 08 sub rsp,0x8 4005f8: 48 83 c4 08 add rsp,0x8 4005fc: c3 ret 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222

可見,連接(Link) 不只僅是單純的將多個 Object 文件拼湊起來而已,而是將程序真正的轉換爲一個能夠在操做系統上執行的文件格式,且這個文件中還包含了整個程序全部 Object 文件的內容。在 Linux 上,這個文件格式就是 ELF(Execuatable and Linkable File Format,可執行與可連接文件格式)。

ELF 文件格式:是一種用於二進制文件、可執行文件、目標代碼、共享庫和核心轉儲格式文件。ELF 文件由 4 部分組成,分別是 ELF header、程序頭表(Program Header Table)、節(Section)和節頭表(Section Header Table)。
在這裏插入圖片描述
在連接器把程序轉換爲 ELF 格式的可執行文件以後,裝載器再去處理就會容易得多。由於裝載器再也不須要考慮地址跳轉的問題,只須要解析 ELF 文件,把對應的指令和數據加載到內存裏面供 CPU 執行就能夠了。

一樣,Windows 也有本身的可執行文件格式 PE(Portable Executable Format)。由於 Linux 和 Windows 的可執行文件格式不一樣,因此也就不可以 「一次編譯,跨平臺執行」 了。那麼換句話說:是否是隻要在 Linux 上運行能夠解析 PE 文件的裝載器就能夠解決這個問題呢?答案是確定的,Linux 著名的開源軟件 Wine 正是此類裝載器,國內不少 Linux Desktop 發行版都是基於 Wine 實現了 Windows 經常使用軟件的移植。

相關文章
相關標籤/搜索