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 字符串拷贝越界

这个场景太经典了。用strcpysprintf这类函数,如果不检查目标缓冲区的大小,很容易写穿栈。

void copy_string(const char *input) {
    char dest[64];
    strcpy(dest, input);  // 如果input长度超过63,就出事了
}

为什么会这样?strcpy不会检查目标缓冲区的大小,它只管往内存里写,直到遇到\0。如果输入字符串很长,它就会一路写下去,覆盖掉栈上的返回地址、局部变量,甚至其他线程的数据。

我记得有一次,一个同事用sprintf拼接字符串,没注意格式化的长度。结果程序在客户现场崩溃,日志里全是乱码。最后定位到,是栈上的返回地址被覆盖了。

我的建议:永远用strncpysnprintf这类带长度限制的函数。别嫌麻烦,安全第一。

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
变长数组 大小由变量决定 谨慎使用,优先堆分配

下一节,我会带你看看如何通过工具和日志,快速定位栈溢出的根因。到时候,我会分享一些我常用的调试技巧,保证实用。