什么是栈溢出?
栈溢出 是一种程序错误,它发生在程序试图向栈 这个内存区域写入数据,超出了其分配的容量时。

就是你的程序需要存放的临时数据(比如函数调用、局部变量等)太多、太大,导致“放不下”了,数据就“溢出”到了内存的其他区域,从而引发程序崩溃或不可预期的行为。
栈 是什么?它有什么用?
在 C 程序运行时,内存通常被划分为几个区域,其中最重要的两个是栈 和堆。
栈 就像一个叠盘子的小推车,它遵循 LIFO (Last-In, First-Out) 后进先出的原则。
栈的主要作用是:

- 存储函数调用信息:当一个函数被调用时,系统会为这个函数创建一个“栈帧”(Stack Frame),这个栈帧包含了:
- 函数的参数
- 函数的局部变量
- 函数执行完毕后的返回地址(告诉 CPU 回到哪里继续执行)
- 管理程序的控制流:函数调用和返回就是通过压栈和出栈来实现的。
栈的大小是有限的,通常由操作系统在程序启动时分配,比如在 Linux/x86-64 系统上,默认栈大小通常是 8MB。
栈溢出的常见原因
栈溢出最常见的原因就是在函数内部定义了过大的局部变量。
巨大的局部数组
这是最直接、最典型的例子,如果你在函数内部定义一个巨大的数组,而这个数组的大小超过了剩余的栈空间,就会导致溢出。
示例代码:

#include <stdio.h>
// 定义一个巨大的局部数组,大小为 10MB
// 1 MB = 1024 * 1024 Bytes
void function_with_large_array() {
char large_array[10 * 1024 * 1024]; // 10MB 的字符数组
printf("函数内部: large_array 的地址是 %p\n", large_array);
// 函数结束时,large_array 所占的栈空间会被自动释放
}
int main() {
printf("main 函数开始\n");
function_with_large_array();
printf("main 函数结束\n");
return 0;
}
会发生什么?
当你运行这段代码时,程序很可能会直接崩溃,打印出类似 Segmentation fault (core dumped) 的错误信息。
为什么?
main函数被调用,一个栈帧被创建在栈顶。function_with_large_array被调用,一个新的栈帧被压入栈。- 在这个新栈帧内部,系统试图分配
10 * 1024 * 1024字节的连续空间给large_array。 - 如果此时栈上剩余的空间不足 10MB,这次分配就会失败,数据被写入到栈之外的区域,破坏了其他重要数据(比如其他函数的栈帧或返回地址),导致程序崩溃。
另一个更“阴险”的原因:无限递归
递归是函数调用自身的一种技术,如果递归没有正确的终止条件,或者终止条件永远无法达到,就会导致函数被无限次地调用。
每次递归调用,都会在栈上创建一个新的栈帧,由于栈的大小是有限的,无限次的递归调用会迅速耗尽栈空间,从而导致栈溢出。
示例代码:
#include <stdio.h>
// 一个没有正确终止条件的递归函数
void infinite_recursion() {
int a = 1; // 每次调用都会创建一个新的局部变量
printf("递归调用中... 地址: %p\n", &a);
infinite_recursion(); // 函数调用自己
}
int main() {
printf("开始无限递归...\n");
infinite_recursion(); // 调用递归函数
printf("这行代码永远不会被执行,\n");
return 0;
}
会发生什么?
运行这个程序,你会看到终端上飞速打印出 递归调用中... 的信息,伴随着内存地址的变化,很快,程序就会因为栈空间耗尽而崩溃,同样报 Segmentation fault。
如何避免和解决栈溢出?
解决方案 1:使用动态内存分配(堆)
当你需要处理大量数据时,不要把它们放在栈上,而是把它们放在堆 上。
堆是程序中另一块巨大的内存区域,它的大小通常比栈大得多(可达 GB 级别),与栈不同,堆是手动管理的,你需要使用 malloc (memory allocate) 来申请内存,使用 free 来释放内存。
修改后的代码(使用堆):
#include <stdio.h>
#include <stdlib.h> // 包含 malloc 和 free 的头文件
void function_with_large_array_on_heap() {
// 使用 malloc 在堆上分配 10MB 的内存
// sizeof(char) 是 1,所以大小就是 10 * 1024 * 1024 字节
char* large_array_on_heap = (char*) malloc(10 * 1024 * 1024);
if (large_array_on_heap == NULL) {
printf("内存分配失败!\n");
return;
}
printf("函数内部: large_array_on_heap 的地址是 %p\n", large_array_on_heap);
// 使用完堆内存后,必须手动释放!
free(large_array_on_heap);
}
int main() {
printf("main 函数开始\n");
function_with_large_array_on_heap();
printf("main 函数结束\n");
return 0;
}
为什么这个版本可以工作?
malloc在堆上寻找一块足够大的连续空间,而不是在栈上。- 只要系统还有足够的物理内存或虚拟内存,
malloc就能成功。 free会将这块内存标记为“可重用”,归还给系统。- 重要提示:使用
malloc后一定要记得free,否则会导致内存泄漏。
解决方案 2:优化递归(或改用循环)
对于无限递归,你需要:
- 检查终止条件:确保递归函数有一个明确的、能够达到的终止条件。
- 改用循环:对于可以用循环解决的问题(比如遍历、累加等),优先使用循环,因为循环不会产生额外的函数调用开销,也不会消耗栈空间。
修改后的递归代码(有终止条件):
#include <stdio.h>
// 一个有终止条件的递归函数,计算阶乘
unsigned long long factorial(int n) {
if (n == 0 || n == 1) { // 终止条件
return 1;
} else {
return n * factorial(n - 1); // 递归调用
}
}
int main() {
int num = 20;
printf("%d 的阶乘是 %llu\n", num, factorial(num));
return 0;
}
这个版本的递归是安全的,因为 n 会逐渐减小,最终达到 0 或 1,递归会停止,如果计算 factorial(100000),它依然会栈溢出,因为递归深度太大了。
解决方案 3:增加栈大小(临时方案)
在某些特定场景下(比如递归算法无法轻易改为循环,且你知道数据量有多大),你可以尝试在编译或运行时增加栈的大小。
-
Linux/macOS (使用
ulimit):# 将栈大小设置为 100MB ulimit -s 100000 # 然后运行你的程序 ./your_program
这是一个临时的修改,只对当前终端会话有效。
-
编译器选项 (GCC):
# 使用 -Wl,-z,stack-size= 设置栈大小 gcc -Wl,-z,stack-size=16777216 your_program.c -o your_program # -z,stack-size= 的单位是字节,16777216 = 16MB
这会将栈大小硬编码到可执行文件中。
注意:增加栈大小只是治标不治本,它推迟了问题的发生,但没有从根本上解决问题,最佳实践仍然是使用堆来处理大数据。
栈溢出的安全风险
栈溢出不仅仅是导致程序崩溃那么简单,在历史上,它曾是最常见的安全漏洞之一,被称为缓冲区溢出攻击。
攻击者可以利用栈溢出来覆盖函数的返回地址,如果他们精心构造输入数据,就可以将返回地址指向一段恶意的代码(shellcode),从而控制整个程序的执行流程,执行任意命令,甚至获取系统的最高权限。
现代操作系统和编译器通过多种技术来缓解这种攻击,
- 栈保护:在栈帧中插入一个随机的“金丝雀”(Canary)值,函数返回前检查这个值是否被修改。
- 地址空间布局随机化:每次程序运行时,栈、库函数等的内存地址都是随机的,使得预测返回地址变得极其困难。
- 数据执行保护:将栈所在的内存页标记为“不可执行”,即使攻击者覆盖了返回地址,指向的恶意代码也无法被执行。
| 特性 | 栈 | 堆 |
|---|---|---|
| 管理方式 | 自动管理(编译器) | 手动管理(malloc/free) |
| 大小 | 小且固定(通常几MB) | 大且灵活(受限于系统虚拟内存) |
| 速度 | 快(压栈/出栈是CPU指令) | 慢(需要系统调用寻找空闲内存) |
| 生命周期 | 函数结束时自动销毁 | free 时销毁,否则一直存在 |
| 用途 | 函数调用、局部变量 | 大型数据结构、动态数据 |
核心要点:
- 栈溢出 是因为向有限的栈空间写入了过多数据。
- 两大元凶:巨大的局部变量 和无限递归。
- 最佳实践:处理大数据时,永远优先使用堆(
malloc/free),而不是栈。 - 安全意识:理解栈溢出的原理有助于编写更安全、更健壮的 C 程序。
