函式呼叫過程與棧幀結構
程序記憶體佈局
32 位保護模式下 Linux 中程序的記憶體佈局如下:
0xFFFFFFFF ------> +----------------------+ <--+ | | | | OS Kernel | | 1GB | | | 0xC0000000 ------> +----------------------+ <--+ | | | | | | stack | | v | | | +----------------------+ | | | | +----------------------+ | | Memory Mapping Region| | 0x40000000 ------> +----------------------+ | | | | +----------------------+ | ^ | | | | | heap | | 3GB | | | | +----------------------+ | | Read/Write Segments | | | .bss | | | .data | | +----------------------+ | | Read-only Segments | | | .init .text .rodata | | | | | 0x08048000 ------> +----------------------+ | | reserved | | 0x00000000 ------> +----------------------+ <--+
32 位下定址空間為 4GB,其中高地址的 1GB 是給作業系統核心使用的(Windows 中預設為 2GB),稱為核心空間(Kernel Space),剩餘的 3GB 分配給應用程式使用,稱為使用者空間(User Space)。當程序執行在核心空間時就處於核心態(Kernel Mode),當程序執行在使用者空間時就處於使用者態(User Mode)。
區分使用者態和核心態是為了提高系統的穩定性和安全性,使作業系統能夠控制資源的訪問,能夠防止應用程式執行一些危險的指令。計算機體系結構中,在硬體上提供不同的特權態,即 Rings Protection,如 Intel CPU 有 4 個特權級:Ring0、Ring1、Ring2、Ring3,一般作業系統只使用 Ring0 和 Ring3,Ring0 具有最高許可權,能夠訪問任何資源,Ring3 訪問受限,需要陷入(trap)到核心態才能訪問特權資源。
棧(stack)從虛擬地址 0xC0000000
往低地址增長。堆(heap)正好相反,從低地址往高地址增長。棧用於函式呼叫以及存放區域性變數等,堆用於動態記憶體分配,如 C 語言中的 malloc() 函式。棧與作業系統核心之間有一個隨機的 offset,堆與讀/寫段之間也有一個隨機的 offset。
記憶體對映段(Memory Mapping Segment)用於將檔案內容對映到記憶體,用於載入動態連結庫,可以通過 mmap() 系統呼叫實現記憶體對映。
bss 段放的是未初始化的全域性變數和靜態變數,預設都初始化為 0,data 段存放的是初始化的全域性變數和靜態變數。
text 段存放的是程式程式碼,除了可讀還具有可執行許可權。
棧及其操作
棧是一種先進後出的結構,包含兩種操作:push
和 pop
。
在 IA-32 體系結構中,通常用 ESP
和 EBP
維護一個棧,EBP
指向棧底,ESP
指向棧頂。
IA-32 中 PUSH
指令先減少 ESP
的值,再將源運算元複製到堆中。
如 PUSH EAX
等價於
SUB ESP, 4
MOV [ESP], EAX
POP
指令正好相反,如 POP EBX
等價於
MOV EBX, [ESP]
ADD ESP, 4
棧幀
棧幀(stack frame)用於函式呼叫,每一次函式呼叫都會有一個獨立的棧幀,包含了返回地址、區域性變數、上下文等資訊。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
+------> | previous EBP | |
| +--------------------+ |
| | saved registers | |
| +--------------------+ |
| | local variables | | caller's frame
| +--------------------+ |
| | callee's args | |
| +--------------------+ |
| | return address | |
| +--------------------+ <--+
+------- | previous EBP | |
+--------------------+ |
| local variables | |
+--------------------+ |
| | | callee's frame
| | |
| | |
| | |
| | |
+--------------------+ <--+
low address
呼叫者(Caller) 呼叫 被呼叫者,每次呼叫都會有新的棧幀壓棧,所以一般深度優先搜尋可以用棧來代替遞迴,以達到更深的搜尋深度。
ESP
和 EBP
只維護當前的棧幀,因此之前的棧幀都要儲存下來,棧幀的頂部儲存了上一個棧幀的 EBP
指向的位置,函式返回時 EBP
能夠恢復到上一個棧幀的 EBP
。
函式呼叫過程
呼叫過程
呼叫者 呼叫 被呼叫者 前,先儲存返回地址,即下一條指令的地址,用於返回後繼續執行,然後進入被呼叫者函式。
被呼叫者開闢了新的棧幀,因此需要儲存呼叫者的棧幀,通常使用以下兩條指令:
PUSH EBP
MOV EBP, ESP
先把舊的 EBP
入棧,然後讓 EBP
指向舊的 EBP
,此時 EBP
已經作為新的棧幀的棧底了。
函式呼叫時,為了防止暫存器被覆蓋,有時需要將暫存器內容也儲存到棧中。
引數的傳遞
IA-32 下,在呼叫者函式中,引數從右往左入棧。進入被呼叫者函式時,引數以及區域性變數的訪問以 EBP
為基址,通過 EBP
加上偏移量訪問。
x86-64 下引數傳遞略有不同。如果函式呼叫引數少於 7 個,則用暫存器傳遞引數,引數從左到右依次放入暫存器 RDI
,RSI
,RDX
,RCX
,R8
,R9
。引數超過 7 個時,前 6 個引數同樣通過暫存器傳參,之後的引數與 32 位一樣從右往左壓入棧中。
返回過程
返回值儲存在 EAX 暫存器中。
返回時先將 EBP
恢復至上一個棧幀的棧底,通常使用以下兩條指令:
MOV ESP, EBP
POP EBP
然後將儲存的返回地址放入 EIP
暫存器。
最後將儲存的引數出棧。
涉及的指令
CALL
指令
先將當前 EIP(即下一條指令的地址作為返回地址)壓入棧中,然後 EIP 轉移到被呼叫者的入口地址。
RET
指令
從棧頂彈出原來儲存的地址至 EIP。
LEAVE
指令
通常將返回時涉及的兩條指令用 LEAVE
指令代替,也就是說 LEAVE
等價於上述提到的兩條指令:
MOV ESP, EBP
POP EBP
函式的呼叫約定
上述引數從右往左入棧、返回值存入 EAX
暫存器等不是硬性規定的,而是遵守函式的呼叫約定(calling convention)。函式的呼叫約定規定了引數的傳遞順序、引數和返回值放置的位置、呼叫前後設定的工作由呼叫者完成還是被呼叫者完成等。上文提到的都是 C 呼叫約定(cdecl呼叫約定)。其他的呼叫約定有 stdcall呼叫約定、fastcall呼叫約定、thiscall呼叫約定等。
一個簡單的例子
以一個加法函式為例:
// file: add.c
#include <stdio.h>
int add(int a, int b) {
int c = a + b;
return c;
}
int main() {
int a = 1, b = 2;
int c = add(a, b);
printf("%d\n", c);
return 0;
}
編譯成 32 位程式:
gcc add.c -o add -m32
檢視彙編程式碼(彙編程式碼可以用 gdb、objdump、IDA 等工具檢視,也可以直接用 gcc 編譯成彙編程式碼)。
輸入 layout asm
檢視彙編程式碼:
進入 main() 函式後,先儲存上一個棧幀的 EBP
(main 函式不是第一個被呼叫的函式)。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
ESP,EBP ------> | previous EBP | | main()'s frame
+--------------------+ <--+
low address
然後儲存暫存器。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ | main()'s frame
ESP ------> | saved registers | |
+--------------------+ <--+
low address
開闢一定的空間儲存區域性變數。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ | main()'s frame
| b=2 | |
+--------------------+ |
| a=1 | |
+--------------------+ |
ESP ------> | | |
+--------------------+ <--+
low address
壓入 add() 函式的引數。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ |
| b=2 | | main()'s frame
+--------------------+ |
| a=1 | |
+--------------------+ |
| 1 | |
+--------------------+ |
ESP ------> | 2 | |
+--------------------+ <--+
low address
call add() 函式,先儲存下一條指令的地址,然後跳轉到 add() 函式。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ |
| b=2 | |
+--------------------+ | main()'s frame
| a=1 | |
+--------------------+ |
| 1 | |
+--------------------+ |
| 2 | |
+--------------------+ |
ESP ------> | 0x122b | |
+--------------------+ <--+
low address
進入 add() 函式,先儲存 main() 函式的 EBP
。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
| previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ |
| b=2 | |
+--------------------+ | main()'s frame
| a=1 | |
+--------------------+ |
| 1 | |
+--------------------+ |
| 2 | |
+--------------------+ |
| 0x122b | |
+--------------------+ <--+
ESP,EBP ------> | previous EBP | | add()'s frame
+--------------------+ <--+
low address
開闢空間用以儲存區域性變數。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
| previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ |
| b=2 | |
+--------------------+ | main()'s frame
| a=1 | |
+--------------------+ |
| 1 | |
+--------------------+ |
| 2 | |
+--------------------+ |
| 0x122b | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ |
| ...... | | add()'s frame
+--------------------+ |
ESP ------> | | |
+--------------------+ <--+
low address
通過 EBP
加上偏移獲取 add() 函式的兩個引數,然後求和,結果儲存在 EAX
暫存器(EAX
是累加暫存器)。
累加結果放入區域性變數 c。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
| previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ |
| b=2 | |
+--------------------+ | main()'s frame
| a=1 | |
+--------------------+ |
| 1 | |
+--------------------+ |
| 2 | |
+--------------------+ |
| 0x122b | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ |
| c=3 | |
+--------------------+ | add()'s frame
| ...... | |
+--------------------+ |
ESP ------> | | |
+--------------------+ <--+
low address
將 c 的值放入 EAX
暫存器作為返回值。
然後 add() 函式返回,先執行 LEAVE
指令,恢復 main() 函式的 EBP
。
MOV ESP, EBP
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
| previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ |
| b=2 | |
+--------------------+ | main()'s frame
| a=1 | |
+--------------------+ |
| 1 | |
+--------------------+ |
| 2 | |
+--------------------+ |
| 0x122b | |
+--------------------+ <--+
ESP,EBP ------> | previous EBP | |
+--------------------+ |
| c=3 | |
+--------------------+ | add()'s frame
| ...... | |
+--------------------+ |
| | |
+--------------------+ <--+
low address
POP EBP
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ |
| b=2 | |
+--------------------+ | main()'s frame
| a=1 | |
+--------------------+ |
| 1 | |
+--------------------+ |
| 2 | |
+--------------------+ |
ESP ------> | 0x122b | |
+--------------------+ <--+
| previous EBP | |
+--------------------+ |
| c=3 | |
+--------------------+ | add()'s frame
| ...... | |
+--------------------+ |
| | |
+--------------------+ <--+
low address
然後執行 RET
指令,將返回地址賦給 EIP
,從 add() 函式返回至 main() 函式,EIP
的值為 0x122b
,即下一條指令為 main() 函式的 ADD ESP, 0x8
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | |
+--------------------+ |
| b=2 | | main()'s frame
+--------------------+ |
| a=1 | |
+--------------------+ |
| 1 | |
+--------------------+ |
ESP ------> | 2 | |
+--------------------+ <--+
low address
執行 ADD ESP, 0x8
,清除 add() 函式的兩個引數。
high address
+--------------------+ <--+
| | |
| ...... | | previous frame
| | |
+--------------------+ <--+
EBP ------> | previous EBP | |
+--------------------+ |
| saved registers | |
+--------------------+ |
| ...... | | main()'s frame
+--------------------+ |
| b=2 | |
+--------------------+ |
ESP ------> | a=1 | |
+--------------------+ <--+
low address
至此 add() 函式的呼叫完成,可以在 main() 函式繼續執行之後的指令了。
系統呼叫
應用程式無法呼叫核心函式,需要通過系統呼叫陷入到核心態,由作業系統執行。
系統呼叫通過 INT 0x80
中斷實現,不同的系統呼叫有不同的系統呼叫號,系統呼叫號需放在 EAX
暫存器中。
系統呼叫號在 /usr/include/asm/unistd.h 中定義。
檢視 i386 體系結構下的系統呼叫號:
陷入到核心態前需要保護現場,將暫存器、程式計數器壓入核心棧,然後呼叫相應的核心函式(系統呼叫)。核心函式執行完成後,將返回值放入 EAX
暫存器,然後恢復現場,將暫存器、程式計數器從核心棧恢復,
然後恢復到使用者態。保護現場和恢復現場均由中斷處理程式完成。
系統呼叫引數傳遞與函式呼叫引數傳遞略有不同,當系統呼叫引數不超過 6 個時,引數從左到右放到暫存器 EBX
,ECX
,EDX
,ESI
,EDI
,EBP
中,如果引數超過 6 個,所有引數應該放在一塊連續的記憶體區域裡(C 結構體),用暫存器 EBX
儲存指向該記憶體區域的指標。
應用程式一般通過 API 去完成系統呼叫。API 與 系統呼叫 不同,一個 API 可能呼叫多個系統呼叫,不同的 API 也可能呼叫同一個系統呼叫。
基本的流程可以概括為:
- 應用程式呼叫 API
- API 將系統呼叫號存入
EAX
,然後通過中斷呼叫系統呼叫 - 中斷處理程式儲存暫存器至核心棧,陷入核心態,根據系統呼叫號呼叫對應的核心函式
- 核心函式完成工作,儲存返回值至
EAX
- 中斷處理程式從核心棧恢復暫存器,恢復到使用者態,返回到 API
- API 將
EAX
返回給應用程式
系統呼叫的跟蹤可用 strace 工具。