一、什么是棧幀(Stack Frame)
當一個函數被調用時,會在棧上開辟一段空間,叫做 棧幀。
每個棧幀保存了:
-
函數的參數
-
返回地址(從哪里跳回來)
-
上一個棧幀的棧底指針(保存調用者的 EBP / FP)
-
局部變量
-
保存的一些寄存器(可選)
二、函數嵌套調用例子
以 C 為例:
void C() {int c = 3;
}void B() {int b = 2;C();
}void A() {int a = 1;B();
}int main() {A();return 0;
}
三、每次函數調用的棧幀結構(以 x86 為例)
棧增長方向:從高地址向低地址
每次調用棧幀大致如下(從下到上):
高地址
│
│ 上一幀的 EBP(caller 的棧底)
│ 返回地址(RET)
│ 參數(如有)
│ 局部變量(如 int x)
│
↓
低地址
四、函數嵌套時完整棧幀流程圖
以調用鏈 main → A → B → C
為例,假設每層函數內部有 1 個 int
局部變量。
初始狀態:
棧空main() 被調用:
+-------------------+ ← ESP,EBP(main 的棧底)
| 返回地址 |
+-------------------+
| 局部變量 return=0 |
+-------------------+main() → A()
+-------------------+ ← ESP(當前)
| 返回地址(main) |
+-------------------+
| 上一幀的 EBP |
+-------------------+
| 局部變量 a=1 |
+-------------------+A() → B()
+-------------------+
| 返回地址(A) |
+-------------------+
| 上一幀的 EBP |
+-------------------+
| 局部變量 b=2 |
+-------------------+B() → C()
+-------------------+
| 返回地址(B) |
+-------------------+
| 上一幀的 EBP |
+-------------------+
| 局部變量 c=3 |
+-------------------+棧頂 (ESP)
五、函數返回時的棧幀回退過程
函數返回時,會彈出當前棧幀,恢復上一個函數的棧幀(EBP 和 RET 地址)。
C() return → ESP 恢復到 B()
B() return → ESP 恢復到 A()
A() return → ESP 恢復到 main()
main() return → 程序結束
六、流程圖總結
main
│
├── 調用 A()
│ │
│ └── 調用 B()
│ │
│ └── 調用 C()
│
└── 每層函數進棧,棧幀不斷疊加每層函數返回,棧幀依次彈出
七、可視化理解
棧頂
│
│ C 的局部變量
│ 返回地址(B)
│
│ B 的局部變量
│ 返回地址(A)
│
│ A 的局部變量
│ 返回地址(main)
│
│ main 的局部變量
│ 返回地址(操作系統)
↓
棧底
八、匯編視角分析
以 x86 為例,函數調用時常見的指令:
CALL func ; 壓入返回地址 → 跳轉
PUSH EBP ; 保存當前幀
MOV EBP, ESP; 建立新棧幀
SUB ESP, n ; 為局部變量分配空間
...
MOV ESP, EBP; 恢復 ESP
POP EBP ; 恢復上層棧幀
RET ; 彈出返回地址
九、總結
步驟 | 棧幀變化 | 關鍵指令 |
---|---|---|
函數調用 | 新棧幀入棧 | CALL 、PUSH EBP |
建立棧幀 | 保存舊幀 & 分配空間 | MOV EBP, ESP 、SUB ESP, n |
函數返回 | 彈出棧幀 | MOV ESP, EBP 、POP EBP 、RE |
補充:
-
在 x86_64 下,函數參數會用寄存器傳遞(如 RDI、RSI)
-
在 ARM64 下,棧幀也有 FP/LR(Frame Pointer / Link Register)結構
-
調用鏈跟蹤調試可用 gdb、ida、ghidra 中的棧回溯(backtrace)