每個用戶態進程擁有一個調用棧,支撐函數參數傳遞、返回地址保存、上下文保護、局部變數存儲。理解棧幀機制是掌握 C 語言運行時行為、除錯優化、逆向分析的基石。
| 寄存器 | 角色 | 呼叫慣例分類 |
|---|---|---|
%eax | 返回值、乘除法隱含操作數 | Caller-saved (主調保存) |
%edx | 高 32 位返回值、除法被除數高位 | Caller-saved |
%ecx | 循環計數、移位量 | Caller-saved |
%ebx | 基址、Callee-saved (被調保存) | Callee-saved |
%esi | 源索引、字串操作 | Callee-saved |
%edi | 目標索引、字串操作 | Callee-saved |
%ebp | 幀基指針 (Frame Pointer) | Callee-saved (必須恢復) |
%esp | 棧頂指針 (Stack Pointer) | Callee-saved (必須恢復) |
%eip | 指令指針 (不可直接訪問) | 由 call/ret/jmp 隱含修改 |
Caller-saved:主調函數若需保留值,調用前必須自行入棧。
Callee-saved:被調函數若使用,必須在序言保存、結語恢復。
棧幀 = 單次函數調用在棧上佔用的連續區域。邊界由 %ebp (棧底/高地址) 和 %esp (棧頂/低地址) 界定。
[主調函數棧幀] ... [參數 N] ← 最後壓入 (最右參數) ... [參數 2] [參數 1] ← 最先壓入 (最左參數) [返回地址] ← call 指令隱含壓入 (下一條指令地址) [舊 %ebp] ← 序言壓入 (主調函數的幀基指針) [局部變數 1] ← 被調函數分配 [局部變數 2] ... [暫存/寄存器保存區] (可選) [被調函數棧幀]
push 使 %esp 減小,pop 使其增大%eax,結構體/聯合體另行約定 (通常由主調分配空間傳隱含指針)call callee:壓入返回地址 (%eip),跳轉入口
push %ebp ; 保存舊幀基指針
mov %esp, %ebp ; 建立新幀基指針 (指向舊 %ebp 位置)
sub $N, %esp ; 分配局部變數空間 (N 通常為 16 的倍數,對齊優化)
push %ebx/%esi/%edi ; 可選:保存 Callee-saved 寄存器
8(%ebp) 為第 1 參數,12(%ebp) 為第 2 參數... (正偏移)-4(%ebp), -8(%ebp)... (負偏移)%eax
pop %edi/%esi/%ebx ; 恢復 Callee-saved 寄存器
mov %ebp, %esp ; 釋放局部變數空間 (等價 leave)
pop %ebp ; 恢復主調函數幀基指針
ret ; 彈出返回地址至 %eip,跳轉繼續執行
add $N, %esp 彈出參數 (Cdecl 由主調清理)。
| 指令 | 操作 | 棧指針變化 |
|---|---|---|
push %reg | %esp -= 4; [*%esp] = %reg | -4 |
pop %reg | %reg = [*%esp]; %esp += 4 | +4 |
call addr | push %eip; jmp addr | -4 |
leave | mov %ebp, %esp; pop %ebp | 恢復到序言前 |
ret | pop %eip; jmp %eip | +4 (再由主調清參數) |
GCC 3.4+ 採用 subl $16, %esp 一次性預留最大參數空間,後續調用直接 mov 存入對應偏移,避免重複 push/pop 修改 %esp,效率更高。
文章提供完整 C 代碼、編譯輸出、棧內存佈局圖解,展示:
memset 清零鄰近變數風險 (按地址連續操作)每個線程擁有獨立棧區。線程切換核心動作:保存當前 %esp,載入目標線程 %esp,即切換棧上下文。
函數調用棚是編譯器、ABI、CPU 協作的精密機制。掌握棧幀佈局、寄存器約定、序言結語模式,能讓你:
下一篇將深入探討參數傳遞細節、可變參數實現、結構體返回值約定及 x86_64 架構差異。