每個用戶態進程擁有一個調用棧,函數調用即棧幀的壓入/彈出。棧幀包含:參數、返回地址、舊幀基址 (EBP)、局部變量、被調用者保存寄存器。
| 寄存器 | 角色 | 調用約定分類 |
|---|---|---|
| EAX | 返回值、乘除隱式操作數 | 調用者保存 |
| ECX, EDX | 通用、fastcall 參數傳遞 | 調用者保存 |
| EBX, ESI, EDI | 通用 | 被調用者保存 |
| EBP | 幀基址指針 | 被調用者保存(必須恢復) |
| ESP | 棧頂指針 | 被調用者保存(必須恢復) |
| EIP | 指令指針 | 隱式修改 |
+-------------------+ ← 高地址 (棧底)
| 參數 N ... 參數 1 | 調用者壓入 (右→左)
+-------------------+
| 返回地址 (EIP) | CALL 隱式壓入
+-------------------+
| 舊 EBP | 被調用者壓入
+-------------------+ ← EBP (幀基址)
| 被調用者保存寄存器| 可選 (EBX/ESI/EDI)
+-------------------+
| 局部變量區 | SUB ESP 分配
| ... |
+-------------------+ ← ESP (棧頂, 低地址)
; 序言
push ebp ; 保存舊幀基址
mov ebp, esp ; 建立新幀基址
sub esp, N ; 分配局部變量空間 (N 通常 16 字節對齊)
push ebx/esi/edi ; 保存被調用者保存寄存器
; 函數體 ...
; 跋語
pop edi/esi/ebx ; 恢復寄存器
mov esp, ebp ; 釋放局部變量區
pop ebp ; 恢復舊幀基址
ret ; 彈出返回地址跳轉
; leave 等價於 mov esp,ebp / pop ebp
; 方式一:逐個 PUSH
push 4
push 3
push 2
push 1
call func
add esp, 16 ; 調用者清棧
; 方式二:預分配棧空間 + MOV (GCC ≥ 3.4 默認)
sub esp, 16
mov [esp+12], 4
mov [esp+8], 3
mov [esp+4], 2
mov [esp], 1
call func
; 無需 ADD ESP,CALLEE 序言已處理
PUSH reg:ESP -= 4; [ESP] = regPOP reg:reg = [ESP]; ESP += 4CALL addr:PUSH EIP; JMP addrRET [imm]:POP EIP; ESP += imm (stdcall 清棧)LEAVE:MOV ESP, EBP; POP EBP編譯器版本、優化等級、架構均影響局部變量相對位置。若需固定相對佈局,必須用結構體/數組。
void callee(int a, int b, int c) {
int x = 1, y = 2, z = 3;
printf("a@%p b@%p c@%p\n", &a, &b, &c);
printf("x@%p y@%p z@%p\n", &x, &y, &z);
}
int main() {
int p=10,q=20,r=30;
callee(p,q,r);
return 0;
}
觀察輸出地址關係,驗證參數正偏移、局部變量負偏移。
線程切換本質是修改 ESP 指向目標線程棧頂,配合 EIP/EBP 實現上下文切換。