共计 1374 个字符,预计需要花费 4 分钟才能阅读完成。
目录

1. 函数调用栈的基本原理
每次函数调用时,CPU 都会在内存中创建一个 ” 栈帧 ” 来保存上下文。x86 架构下关键寄存器:
- ESP (栈指针):始终指向栈顶
- EBP (基址指针):标记当前栈帧的起始位置
典型栈帧结构如下(地址从高到低生长):
| 高地址 |
|---------|
| 参数 N |
| ... |
| 参数 1 |
| 返回地址 |
| 旧 EBP | ← EBP 指向这里
| 局部变量 |
| ... | ← ESP 指向这里
| 低地址 |
调用函数时 CPU 自动完成:
- 按从右到左顺序压入参数(cdecl 约定)
- 压入返回地址(call 指令的下一条指令)
- 跳转到函数入口
2. 实战:用 GDB 解剖栈帧结构
示例代码(保存为 stack.c):
#include <stdio.h>
int sum(int a, int b) {
int c = a + b; // 局部变量存储在栈中
return c;
}
int main() {int x = sum(3, 4);
printf("Result: %d\n", x);
return 0;
}
编译并调试:
gcc -g -o stack stack.c # - g 生成调试符号
gdb ./stack
关键 GDB 命令演示:
(gdb) break sum # 在 sum 函数设断点
(gdb) run # 运行程序
(gdb) info frame # 查看当前栈帧信息
(gdb) x/8xw $esp # 查看栈内存(显示 8 个 4 字节字)(gdb) bt # 查看调用栈回溯
实际调试时会看到:
- 参数 3 和 4 按从右到左顺序压栈
- sum 函数的返回地址紧随其后
- EBP 保存着 main 函数的栈基址
- 局部变量 c 存放在 EBP- 4 的位置
3. 危险的栈溢出漏洞
经典漏洞示例:
void vulnerable() {char buf[8];
gets(buf); // 无边界检查!}
当输入超过 7 字节时(需留 1 字节给 \0):
- 字符会覆盖相邻的栈空间
- 继续溢出将覆盖保存的 EBP
- 最终覆盖返回地址→程序控制流被劫持
现代防护机制:
- Stack Canary:编译器插入随机值在返回地址前,函数返回时校验
- ASLR:随机化内存布局增加攻击难度
- NX:标记栈为不可执行
4. 防护与最佳实践
安全编码
- 永远不要使用 gets(),改用 fgets():
fgets(buf, sizeof(buf), stdin); - 使用 strncpy 替代 strcpy
- 对用户输入进行长度校验
编译选项
gcc -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2
检测工具
Valgrind 内存检测示例:
valgrind --tool=memcheck ./your_program
5. 延伸思考
递归深度问题
每次递归调用都会消耗栈空间,可用以下方法估算极限深度:
int check_depth() {
static int count = 0;
char dummy[1024]; // 每次分配 1KB
count++;
return check_depth();}
栈空间不足处理
优雅方案包括:
- 改用迭代算法
- 动态分配大内存时使用堆(malloc)
- 通过 setrlimit()调整栈大小
结语
理解调用栈不仅有助于调试,更是编写安全代码的基础。建议用 objdump -d 查看编译器生成的实际汇编代码,配合 GDB 单步跟踪,会有更直观的体会。当你下次遇到 Segmentation Fault 时,不妨先看看调用栈回溯——它可能正在告诉你问题根源。
正文完
