共计 1210 个字符,预计需要花费 4 分钟才能阅读完成。
从 CPU 寄存器看函数调用
当我们在 C 语言中调用一个函数时,CPU 会通过两个关键寄存器来管理堆栈:

- ESP(Extended Stack Pointer):始终指向栈顶
-
EBP(Extended Base Pointer):标记当前栈帧的基地址
-
调用函数时,CPU 会依次执行以下操作:
- 按从右到左顺序压入参数
- 压入返回地址(call 指令的下一条指令)
- 保存调用者的 EBP 值(形成调用链)
-
移动 ESP 分配局部变量空间
-
函数返回时逆向操作:
- 恢复 ESP 到 EBP 位置
- 弹出保存的 EBP 值
- 通过 ret 指令跳转回返回地址
GDB 实战演示栈帧结构
准备以下测试代码(保存为 stack.c):
#include <stdio.h>
int add(int a, int b) {
int sum = a + b; // 局部变量
return sum;
}
int main() {
int x = 3, y = 4;
int result = add(x, y);
printf("%d\n", result);
return 0;
}
调试步骤:
-
编译时加入调试信息:
gcc -g stack.c -o stack -
启动 GDB 并设置断点:
gdb ./stack (gdb) break add (gdb) run -
查看栈帧关键信息:
(gdb) info frame # 显示当前栈帧 (gdb) x/8x $esp # 查看栈内存
典型场景代码示例
递归调用栈消耗
#include <stdio.h>
void recursive(int depth) {char buffer[1024]; // 每次递归消耗 1KB 栈空间
printf("Depth %d at %p\n", depth, &buffer);
if(depth > 0) recursive(depth-1);
}
int main() {recursive(5);
return 0;
}
缓冲区溢出示例
#include <string.h>
void vulnerable() {char name[4];
strcpy(name, "ABCDEFGH"); // 故意越界写入
printf("Hello %s\n", name);
}
int main() {vulnerable();
return 0;
}
生产环境避坑指南
-
检查栈大小限制:
ulimit -s # 显示当前栈大小(默认 8MB)ulimit -s 20480 # 设置为 20MB(临时生效) -
安全编码规范:
- 避免大局部变量(改用堆分配)
- 对数组访问进行边界检查
-
慎用递归(特别是深度不可控时)
-
编译器保护选项:
gcc -fstack-protector-strong test.c # GCC 的栈保护原理:在栈中插入 canary 值,在返回前校验是否被修改
思考与进阶
- 调用约定验证 :通过
objdump -d比较不同调用约定下参数清理方式的差异 - 可变参数影响 :观察
va_start如何通过栈指针访问不定参数 - 尾递归优化:满足哪些条件时编译器会把递归转为循环(需 -O2 优化)
理解堆栈机制不仅能帮助调试,更是写出健壮代码的基础。建议用 gdb -tui 开启图形模式观察栈变化,会有更直观的认识。
正文完
