从栈溢出到获取栈大小
Author: ChrisZZ [email protected]
Create Time: 2024-05-14 23:22:38
Update Time: 2024-05-17 22:39:39
1. 栈溢出是一个运行时报错
在谷歌搜索 "Stack Overflow", 靠前的结果是一个问答网站。它的本意是“栈溢出”, 是一种运行时错误,例如编写的 C/C++ 代码,编译后运行的时在某个函数的第一句还没执行就挂了。
2. 为什么会出现栈溢出
2.1 运行时的栈大小被限定了
生成可执行程序时, 链接器可以指定运行时栈大小, 超过这个尺寸就发生栈溢出。
对于 MSVC 编译器, 默认栈大小是 1MB; 对于 Linux GCC, 默认栈大小是 8MB。
2.2 栈是怎么被消耗的
栈,是栈内存的简称, 是区别于堆的内存。函数里定义局部变量,就消耗栈内存,这是单个函数内的情况。 函数之间相互调用, 被调用者的栈内存开销, 累加到调用者身上, 构成了总的栈内存开销。
直观理解: C/C++ 程序运行时,入口函数通常为 main()
, 假设它调用函数 f1()
, f1()
里面调用 f2()
, 那么在执行 f2()
时的栈开销就是 main + f1 + f2.
如果 main()
先调用 f1()
, 调用完毕再调用 f2()
, 那么在执行 f2()
时的栈开销就是 main + f2.
当然,这里假设了 f2()
是叶子函数, 不会调用函数,也不会调用 f2()
自身。
2.3 栈溢出的几种典型情况
-
函数递归调用,在到达递归终止条件之前,栈的开销超过了限定值。
-
函数调用链路过长,callstack 呈现为超长链表,累计的栈内存开销较大。
这种情况不是递归,但和递归很像。 好的程序的 callstack 应当是有分叉, 呈现树状, 而不是链表状。
- 局部变量size过大,尽管函数调用层次可能很浅(甚至只有 main 函数), 仍然栈溢出。 具体又包含如下典型情况:
- 在函数内,直接定义了超大数组:解决方法是改用new/malloc申请
- 定义了size很大的结构体或class,并且在函数内创建了实例:
- 例如直观的: 单个结构体里有一个超大数组的成员(e.g. Visual Studio 鼠标悬停,Intellisense会提示栈大小)
- 或者间接的: 结构体的成员也是结构体,内层结构体的数组成员M, 乘以外层结构体的数组元素数量N, 乘积很大
- 解决方法是改为new/malloc申请
- 平台或编译工具链限定, 例如 HVX cDSP 平台的 Hexagon-clang, 限定了14000 bytes 左右。
3. 确定栈大小
3.1 暴力法
- 编写代码,创建大数组
- 编译
- 运行
- 判断运行结果
a) 如果运行时没有 crash,则回到步骤1), 并增加数组大小, 否则进入5)
b) 运行 crash 了,说明超过栈的最大大小,则回到步骤1), 减小数组大小
c) 如果恰好到了临界值,多了会crash,少了不会crash,那就停止
int main()
{
int a[1024*1024*8];
return 0;
}
优点是直观,容易操作, 缺点是需要多次尝试, 在交叉编译和推送设备运行的情况下比较低效率。
3.2 递归法
get_stack_size.c:
#include <stdio.h>
enum { unit_size = 1024 };
const char* unit_str = "KB";
void f(int depth)
{
char a[unit_size];
printf("depth = %d, stacksize = %d %s\n", depth, depth * unit_size, unit_str);
f(depth+1);
}
int main()
{
int depth = 1;
f(depth);
return 0;
}
编译运行:
depth = 7577, stacksize = 7758848 KB
depth = 7578, stacksize = 7759872 KB
[1] 43864 segmentation fault ./a.out
优点:一次编译、一次运行,比较省事,粗略得到了栈大小。
缺点:打印出的 stacksize 并不准确,单个函数的栈开销并不是 1024 字节。
3.3 更准确的获取栈大小
-fstack-usage
编译选项
GCC/Clang 提供了 -fstack-usage
编译选项, 生成 .c/.cpp 源文件同名的 .su
文件,标注出了每个函数的栈开销大小.
$ gcc get_stack_size.c -fstack-usage
$ cat get_stack_size.su
get_stack_size.c:6:f 1104 static
get_stack_size.c:13:main 32 static
因此,更准确一点的栈大小是 8564864 字节:
>>> 32 + 1104*7758
8564864
-stack_size
链接选项
gcc -Wl,-stack_size,0x1000000 -o my_program my_program.c
当有函数的栈开销超过这一数值时就报警.
4. 避免栈溢出
4.1 修改最大栈大小
不改代码的情况下,修改链接选项,增大栈大小。
Linux/macOS: ulimit 命令。
Windows MSVC: 给链接器指定 /stack:<栈大小>
参数, 例如 /stack:10485760
, 10MB。
可在 CMakeLists.txt 中设定
if(MSVC)
target_link_options(your_target_name PRIVATE "/STACK:10485760")
endif()
4.2 减小栈开销
- 使用 new/malloc
使用 new/malloc 替代直接定义大size的栈对象。
#include <stdlib.h>
struct Engine
{
int id;
int data[1024];
};
typedef struct Engine Engine;
int main()
{
//Engine engine; // 这种方式下,总的栈大小是 4128 字节
Engine* engine = (Engine*)malloc(sizeof(Engine)); // 这种方式下,总的栈大小是 48 字节
return 0;
}
gcc -c main.c -fstack-usage
查看 .su 文件验证。
- 使用柔性数组
使用柔性数组替代结构体中定义的大数组。e.g. https://github.com/jonhoo/pthread_pool/blob/master/pthread_pool.c#L21-L27
struct pool {
char cancelled;
void *(*fn)(void *);
unsigned int remaining;
unsigned int nthreads;
struct pool_queue *q;
struct pool_queue *end;
pthread_mutex_t q_mtx;
pthread_cond_t q_cnd;
pthread_t threads[1];// 柔性数组声明
};
void * pool_start(void * (*thread_func)(void *), unsigned int threads)
{
struct pool *p = (struct pool *) malloc(sizeof(struct pool) + (threads-1) * sizeof(pthread_t)); //柔性数组填充
int i;
...
}
5. Python 中的栈溢出
Python 默认限定递归次数不能超过1000次. 虽然不是直接的超过了栈大小,但是间接避免了栈开销过大的问题.
def f(depth: int):
print(depth)
f(depth+1)
f(1)
995
996
997
Traceback (most recent call last):
File "/Users/zz/work/immitate/Cpp-homework/test2.py", line 5, in <module>
f(1)
File "/Users/zz/work/immitate/Cpp-homework/test2.py", line 3, in f
f(depth+1)
File "/Users/zz/work/immitate/Cpp-homework/test2.py", line 3, in f
f(depth+1)
File "/Users/zz/work/immitate/Cpp-homework/test2.py", line 3, in f
f(depth+1)
[Previous line repeated 993 more times]
File "/Users/zz/work/immitate/Cpp-homework/test2.py", line 2, in f
print(depth)
RecursionError: maximum recursion depth exceeded while calling a Python object
使用 sys.getrecursionlimit()和sys.setrecursionlimit()来观察和修改这个限制.
6. 总结
TODO