3. 栈溢出常见场景:递归调用过深、大局部变量、字符串拷贝越界、alloca使用不当、变长数组
栈溢出,说白了就是程序在栈上“越界”了。我调试过不少系统崩溃的问题,十有八九都能在栈上找到根源。这一节,我带你看看最常见的五种场景。每一种我都亲手踩过坑,有些甚至让我熬了好几个通宵。
3.1 递归调用过深
递归本身不是问题,问题在于递归深度不可控。每次函数调用,都会在栈上分配一个新的栈帧。如果递归深度太大,栈空间迟早被吃光。
核心原因:每次递归调用都会消耗栈帧,深度过大时栈空间耗尽。
举个例子,你写了一个简单的递归函数:
void recurse(int depth) {
char buf[1024]; // 每次调用分配1KB
printf("depth: %d\n", depth);
recurse(depth + 1);
}
这个函数看起来人畜无害,但运行起来,栈会迅速膨胀。我记得有一次,同事在嵌入式设备上跑了一个类似的递归,结果系统直接黑屏。嗯,连日志都没来得及打印。
我的建议:递归深度超过1000次,就要警惕了。能用迭代就别用递归,实在要用,加一个深度上限检查。
3.2 大局部变量
局部变量是分配在栈上的。如果你在函数里声明了一个巨大的数组,比如:
void process_data() {
char big_buffer[1024 * 1024]; // 1MB的局部数组
// ...
}
这1MB的数组,直接塞进栈里。默认的栈大小,Linux上是8MB,Windows上是1MB。你想想看,一个函数就吃掉1MB,再嵌套几层调用,栈很快就爆了。
我个人习惯,超过几KB的局部变量,就改用堆分配。用malloc或者new,虽然麻烦一点,但安全得多。
避坑指南:我曾经接手过一个项目,代码里到处都是大数组局部变量。上线后,系统在高峰期频繁崩溃。排查了三天,最后发现是栈溢出。从那以后,我定了个规矩:局部数组超过4KB,必须走堆分配。
3.3 字符串拷贝越界
这个场景太经典了。用strcpy、sprintf这类函数,如果不检查目标缓冲区的大小,很容易写穿栈。
void copy_string(const char *input) {
char dest[64];
strcpy(dest, input); // 如果input长度超过63,就出事了
}
为什么会这样?strcpy不会检查目标缓冲区的大小,它只管往内存里写,直到遇到\0。如果输入字符串很长,它就会一路写下去,覆盖掉栈上的返回地址、局部变量,甚至其他线程的数据。
我记得有一次,一个同事用sprintf拼接字符串,没注意格式化的长度。结果程序在客户现场崩溃,日志里全是乱码。最后定位到,是栈上的返回地址被覆盖了。
我的建议:永远用strncpy、snprintf这类带长度限制的函数。别嫌麻烦,安全第一。
3.4 alloca使用不当
alloca这个函数,很多人觉得它方便——在栈上动态分配内存,函数返回时自动释放。但方便背后藏着大坑。
void handle_request(int size) {
char *buffer = (char*)alloca(size);
// 如果size很大,或者被恶意控制,栈就爆了
}
alloca分配的内存,不受栈大小限制检查。如果你传入一个很大的size,比如几MB,栈直接撑爆。更可怕的是,如果size来自用户输入,攻击者可以故意传一个超大值,触发栈溢出,甚至执行恶意代码。
避坑指南:我曾经在一个网络服务里看到有人用alloca处理请求数据。上线后,只要有人发一个超大的请求包,服务就崩溃。后来我全部改成了malloc,问题才解决。我个人建议,除非你非常清楚栈的剩余空间,否则别用alloca。
3.5 变长数组(VLA)
C99标准引入了变长数组(Variable Length Array),允许在栈上声明大小可变的数组。听起来很灵活,但用起来要小心。
void process(int n) {
int array[n]; // 变长数组,大小由n决定
// 如果n很大,栈就爆了
}
变长数组和alloca类似,都是在栈上动态分配。如果n的值不可控,比如来自用户输入或者计算错误,栈溢出就是分分钟的事。
嗯,这里要注意:变长数组在C++中不是标准特性,很多编译器不支持。即使你用C语言,也建议谨慎使用。我个人的习惯是,能用堆分配就用堆分配,别在栈上玩这种“动态”的把戏。
总结一下:这五种场景,本质上都是对栈空间的不当使用。递归过深、大局部变量、字符串越界、alloca、变长数组,每一个我都亲手调试过。记住一句话:栈是宝贵的资源,别把它当堆用。
| 场景 | 根本原因 | 我的建议 |
|---|---|---|
| 递归调用过深 | 栈帧不断累积 | 加深度限制,或用迭代 |
| 大局部变量 | 栈上分配过大 | 超过几KB改用堆 |
| 字符串拷贝越界 | 未检查缓冲区大小 | 用带长度限制的函数 |
| alloca使用不当 | 动态分配不受控 | 尽量不用,改用malloc |
| 变长数组 | 大小由变量决定 | 谨慎使用,优先堆分配 |
下一节,我会带你看看如何通过工具和日志,快速定位栈溢出的根因。到时候,我会分享一些我常用的调试技巧,保证实用。