理解函數調用底層機制,對調試、優化、編寫彙編接口至關重要。本文基於 16 位/32 位 x86 架構,詳細剖析調用約定、棧幀構建、參數訪問與返回流程。
CALL 指令,壓入返回地址(EIP),跳轉到 CalleeEBP(push ebp),建立新棧幀基址(mov ebp, esp)sub esp, N)EAX/EDX:EAX/ST0mov esp, ebp 或 leave),彈出舊 EBP(pop ebp)RET 返回,Caller 清理參數棧(add esp, N)高地址 +------------------+ | 參數 3 (最右) | <-- [EBP+16] (32位 near call) +------------------+ | 參數 2 | <-- [EBP+12] +------------------+ | 參數 1 (最左) | <-- [EBP+8] +------------------+ | 返回地址 | <-- [EBP+4] +------------------+ | 舊 EBP | <-- [EBP] (Callee 保存) +------------------+ | 局部變量 1 | <-- [EBP-4] +------------------+ | 局部變量 2 | <-- [EBP-8] +------------------+ | ... | 低地址 (ESP)
關鍵點:
EBP 作為幀指針,參數正偏移,局部變量負偏移printf)依賴首參數(格式串)確定後續參數數量/類型,故必須右向左壓棧# fun(p0, p1, p2) # 1. 參數入棧 movl $p2, 8(%esp) movl $p1, 4(%esp) movl $p0, (%esp) # 2. CALL call fun # 壓入返回地址,ESP -4 # 3. 進入 fun push %ebp # 保存舊 EBP movl %esp, %ebp # 建立新幀 # 此時棧佈局: # [EBP] 舊 EBP # [EBP+4] 返回地址 # [EBP+8] p0 # [EBP+12] p1 # [EBP+16] p2 # 4. 分配局部變量 subl $24, %esp # 6 個 4 字節局部變量 # 5. 返回序列 movl %ebp, %esp # 或 addl $24, %esp popl %ebp # 恢復 EBP,銷毀幀 ret # 彈出返回地址跳轉
#include#include void fun(int a, int b) { int local = 12; unsigned int ebp_val, esp_val; long ret_addr; /* 內聯彙編讀取 EBP/ESP */ asm volatile ( "movl %%ebp, %%eax\n\t" "movl %%esp, %%ebx\n\t" : "=a"(ebp_val), "=b"(esp_val) ); printf("ebp = %p; esp = %p\n", (void*)ebp_val, (void*)esp_val); printf("local vars size: %u bytes\n", ebp_val - esp_val); /* 讀取參數與返回地址 */ asm volatile ( "movl 12(%%ebp), %%eax\n\t" // 參數 b "movl 8(%%ebp), %%ebx\n\t" // 參數 a "movl 4(%%ebp), %%ecx\n\t" // 返回地址 : "=a"(ebp_val), "=b"(esp_val), "=c"(ret_addr) ); printf("p2(b) = %x; p1(a) = %x; ret = %p\n", ebp_val, esp_val, (void*)ret_addr); } int main() { fun(1, 2); return 0; }
leave/pop ebp 後棧幀銷毀,局部變量內存失效sub esp, N),動態大小需用 alloca 或堆分配info frame、bt、disas 可觀察棧幀與調用鏈