问题:
昨天,有同学遇到栈溢出的问题。在做大三小学期项目时,需要一个750x750的矩阵。于是在栈中定义了一个二维数组。为了说明问题,做如下简化:
/*
测试环境:
window平台 vs2013
*/
int main()
{
//占用栈内存,局部变量,太大,栈溢出
double test[750][750];
return 0;
}
这看似没有问题,定义了一个变量,不大,才4.5M左右。可是,当运行时出现了栈溢出。什么情况?
编译器和操作系统背后的原理:
操作系统为例实现对用户程序的管理,使用进程+线程来运行我们的程序。涉及到线程,必须得考虑操作系统和编译器。
操作系统,例如Linux,使用vm_area_struct
结构体来管理用户空间,在加载一个elf或其他格式的可执行文件时总是会参考文件中给出的信息来设置这个结构体中的内容。其中代码和只读数据段可能就由同一个vm_area_struct
来管理、栈区由另一个vm_area_struct
来管理、堆区也有一个vm_area_struct
、共享区有一个vm_area_struct
、映射文件有自己vm_area_struct
。所有的vm_area_struct
按地址大小连成一个vm_area_struct
链表。如果vm_area_struct
过多,貌似超过32个时,操作系统就会为其建立一个红黑树,加快查找过程。
在task_struct
中有一个数组rlim
用来记录一个对进程分配资源的限制,数组的第RLIMIT_STRACK
项记录了栈的大小限制。
struct task_struct { ... struct signal_struct *signal; ... } struct signal_struct { ... /* * We don't bother to synchronize most readers of this at all, * because there is no reader checking a limit that actually needs * to get both rlim_cur and rlim_max atomically, and either one * alone is a single word that can safely be read normally. * getrlimit/setrlimit use task_lock(current->group_leader) to * protect this instead of the siglock, because they really * have no need to disable irqs. */ struct rlimit rlim[RLIM_NLIMITS];//记录了对RLIM_NLIMITS份资源的限制 }
struct rlimit
struct rlimit {
unsigned long rlim_cur; //软限制。用户可以更改,但上限是硬限制。
unsigned long rlim_max; //硬限制。除了超级用户之外,其他用户不可更改。
};
init_task
那么,所有进程的
rlimit
数组什么时候被设置呢?由用户创建的每个进程都继承其父进程rlimit
数组的内容。在系统静态的创建第一个内核进程时,为
idle[init_task]
进程静态的初始化了一个rlimit
数组:
#define INIT_RLIMITS \
{ \
[RLIMIT_CPU] = { RLIM_INFINITY, RLIM_INFINITY }, \
[RLIMIT_FSIZE] = { RLIM_INFINITY, RLIM_INFINITY }, \
[RLIMIT_DATA] = { RLIM_INFINITY, RLIM_INFINITY }, \
[RLIMIT_STACK] = { _STK_LIM, _STK_LIM_MAX }, \
[RLIMIT_CORE] = { 0, RLIM_INFINITY }, \
[RLIMIT_RSS] = { RLIM_INFINITY, RLIM_INFINITY }, \
[RLIMIT_NPROC] = { 0, 0 }, \
[RLIMIT_NOFILE] = { INR_OPEN, INR_OPEN }, \
[RLIMIT_MEMLOCK] = { MLOCK_LIMIT, MLOCK_LIMIT }, \
[RLIMIT_AS] = { RLIM_INFINITY, RLIM_INFINITY }, \
[RLIMIT_LOCKS] = { RLIM_INFINITY, RLIM_INFINITY }, \
[RLIMIT_SIGPENDING] = { MAX_SIGPENDING, MAX_SIGPENDING }, \
[RLIMIT_MSGQUEUE] = { MQ_BYTES_MAX, MQ_BYTES_MAX }, \
}
/*
* Limit the stack by to some sane default: root can always
* increase this limit if needed.. 8MB seems reasonable.
*/
#define _STK_LIM (8*1024*1024)
#ifndef RLIM_INFINITY
#define RLIM_INFINITY (~0UL)//0取反,自然是能表示的最大值。
#endif
#ifndef _STK_LIM_MAX
#define _STK_LIM_MAX RLIM_INFINITY
#endif
如果超级用户不做更改,所有其他进程的
rlimit
最初的祖先就是这个进程的rlimit
【在创建一个进程时,在do_fork
中会调用copy_signal
函数将父进程的rlimit
拷贝到子进程】。
分配栈内存
应该可以这么理解:进程的栈有一个
vm_area_struct
,在编译器中可以设置他的大小:
可以看到指定虚拟内存中栈分配的合计大小默认为1MB。不过,我们可以在
堆栈保留大小
选项中调整大小。对于linux
操作系统,栈的vm_area_struct
的大小由rlim[RLIMIT_STACK].rlim_cur
限制。在代码中可以更改rlim[RLIMIT_STACK].rlim_cur
的大小,甚至可以将其值设置为无穷大,完全不对栈的大小设置限制。
栈溢出
以下内容参考自《Linux内核代码情景分析》.2.5节:
假设在进程运行过程中,栈已经增长到了
vm_area_struct
的边界,在下一次压栈操作时,访问的虚拟地址落在了空洞
中,正常情况下就会产生栈溢出,报出段错误。但是栈区是个特例。这个落在空洞中的push访存操作导致缺页异常,缺页异常发现地址就在栈区附近。所以,异常处理程序会检查
rlim[RLIMIT_STACK].rlim_cur
的限制,如果对这个地址的访问不超出这个限制,就使用expand_stack()
扩展vm_area_struct
的边界,将这个地址包含进去。否则,访问出错,报出段错误。好了,说明白了原理?下面分析出错原因。
我们看到上面的代码只是定义了一个局部变量,并没有显示的进程内存访问。按理说没有访存就不会有段错误啊?
其实是有访存的,可以参考动态分配栈内存之alloca内幕。 在代码需要一块局部栈变量时,他会在代码中调用
__alloca_probe
在栈中申请内存,最后还要对申请的内存测试test dword ptr [eax],eax
,这里就存在访存操作,如果内存访问不合理,程序就会报出栈溢出错误。这样就能在代码测试的早期定位出错位置,而不至于运行到某个某名奇妙的地方才报出错误。
怎么解决
明白是栈内存不够,接下来怎么办呢?
- 增大栈大小,默认可以无限制的增大。
- 使用堆内存。
- 使用静态区内存【全局变量或者静态变量】。
Change stack size for a C++ application in Linux during compilation with GNU compiler
动态分配栈内存之alloca内幕