一、什么是栈帧(Stack Frame)
当一个函数被调用时,会在栈上开辟一段空间,叫做 栈帧 。
每个栈帧保存了:
-
函数的参数
-
返回地址(从哪里跳回来)
-
上一个栈帧的栈底指针(保存调用者的 EBP / FP)
-
局部变量
-
保存的一些寄存器(可选)
二、函数嵌套调用例子
以 C 为例:
cpp
void C() {
int c = 3;
}
void B() {
int b = 2;
C();
}
void A() {
int a = 1;
B();
}
int main() {
A();
return 0;
}
三、每次函数调用的栈帧结构(以 x86 为例)
栈增长方向:从高地址向低地址
每次调用栈帧大致如下(从下到上):
bash
高地址
│
│ 上一帧的 EBP(caller 的栈底)
│ 返回地址(RET)
│ 参数(如有)
│ 局部变量(如 int x)
│
↓
低地址
四、函数嵌套时完整栈帧流程图
以调用链**main → A → B → C
** 为例,假设每层函数内部有 1 个**int
** 局部变量。
bash
初始状态:
栈空
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 地址)。
bash
C() return → ESP 恢复到 B()
B() return → ESP 恢复到 A()
A() return → ESP 恢复到 main()
main() return → 程序结束
六、流程图总结
bash
main
│
├── 调用 A()
│ │
│ └── 调用 B()
│ │
│ └── 调用 C()
│
└── 每层函数进栈,栈帧不断叠加
每层函数返回,栈帧依次弹出
七、可视化理解
bash
栈顶
│
│ C 的局部变量
│ 返回地址(B)
│
│ B 的局部变量
│ 返回地址(A)
│
│ A 的局部变量
│ 返回地址(main)
│
│ main 的局部变量
│ 返回地址(操作系统)
↓
栈底
八、汇编视角分析
以 x86 为例,函数调用时常见的指令:
Clojure
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)