一、什么是线程栈溢出
我们都知道,每一个win32线程都会开辟一个空间,用来临时存储线程执行时所调用的一系列函数的参数、返回地址和局部变量及其他上下文信息。这个空间就是线程的栈区。栈区的容量是有限的,在程序编译链接时,就固定下来了。通过VC++编译的程序,默认的栈区大小是1MB。当我们程序执行时,访问超过了这个空间的边界,就叫栈溢出,又叫Stack overflow。这时会产生代码为STATUS_STACK_OVERFLOW(0xC00000FD)的异常,从而导致程序崩溃。注意一定要与栈缓冲区溢出---STATUS_STACK_BUFFER_OVERRUN(0xC0000409)区别开来。
二、栈溢出的原因
栈溢出是用户模式线程可能会遇到的错误。 有三个可能的原因产生此错误:
-
线程使用为其保留的整个堆栈。这通常是由无限递归引起的。
-
线程无法扩展堆栈,因为页文件已最大化,因此无法提交其他页来扩展堆栈。
-
由于系统内使用以扩展页面文件的短时间内,线程不能扩展堆栈。
当一个线程上运行的函数分配的本地变量时,变量放线程的调用堆栈上。 函数所需的堆栈空间量可能大至所有本地变量的大小的总和。 但是,编译器通常会执行优化,降低函数所需的堆栈空间。 例如,如果两个变量,则在不同的作用域中,编译器可以为这两个这些变量使用相同的堆栈内存。 编译器还可能无法完全消除某些本地变量对计算进行优化。优化的量会影响应用在生成时的编译器设置。 例如,调试版本和发布版本具有不同的优化级别。 所需的调试版本中的函数的堆栈空间量可能会大于该发行版中的相同函数所需的堆栈空间量。
在我们编写代码时,如下情况通常引发栈溢出:
- 由于某些原因,导致函数递归深度很深或无穷递归
- 在栈里分配了很大的缓冲区
- 栈里的某缓冲区溢出
三、溢出代码举例
3.1、在栈里分配了很大的缓冲区导致溢出
代码如下:
#include "stdafx.h" int LargeBuffer(void)
{
char buf[1024 * 1024];
int a = 0;
int b = buf[2];
return a+b;
}
int _tmain(int argc, _TCHAR* argv[])
{
int n=LargeBuffer();
printf("n=%d ", n);
return 0;
}
由于在VS里默认线程栈空间为1MB,我们代码里在栈区分配了一个刚好1MB的数组,在加上其他函数入栈占用的空间,肯定就会超出1MB产生溢出
在VS2013里编译,Debug模式下调试运行,果然报错,跟我们前面预想的一样
我们注意到此时的调用栈如下:
看此时的栈,没又被破坏,最后停在了_chkstk()里,这是个什么样的函数呢。这个函数其实是VC++的crt函数,用来检测堆栈的。停在这里,说明这个函数检测到了这种情况栈溢出。我们看看它的代码,在VS里转到汇编模式下
在地址栏里输入上面错误提示框里地址0x00F21767,得到
_chkstk: push ecx cmp eax,1000h lea ecx,[esp+8] jb lastpage probepages: sub ecx,1000h sub eax,1000h test dword ptr [ecx],eax cmp eax,1000h jae probepages (0040818c) lastpage: sub ecx,eax mov eax,esp test dword ptr [ecx],eax mov esp,ecx mov ecx,dword ptr [eax] mov eax,dword ptr [eax+4] push eax ret
大概流程
- chkstk例程是C编译器的助手例程。对于x86当局部变量超过4096字节;对于x64编译器,它分别是4K和8K。即, 当一个函数局部变量超过一个页面大小4k的时候, 编译器会自动插入这个函数。插入这个函数的位置在:
push ebp mov ebp,esp mov eax,1000D4h call _chkstk()
- 这个函数做什么?当函数为堆栈分配的页面不够时, (堆栈默认大小为1M), 堆栈需要更多的页面时调用这个函数。当堆栈使用大于分配的大小(默认1M)时,产生_XCPT_UNABLE_TO_GROW_STACK.
- 当调用这个函数时, 首先外面对eax进行赋值(已经分配的堆栈大小 + 即将分配给函数局部变量的堆栈大小)
- 调用 _chkstk() 首先,保持当期esp到eax中,然后开始判断:如果分配的大小大于一个页面, 到第5)步(大多都先第5)步);否则到第6)步。
- 当需要分配的大小大于一个页面, 则增加一个页面。“sub eax 1000h” 表示堆栈栈顶下移1000h,[由于堆栈是高地址(栈底部)->低地址(栈顶部)分布],所以堆栈扩大了1000h; “sub ecx 1000h”表示分配了1000h(1个页面)之后还需要多少空间;“test dword ptr [ecx],eax” 表示分配空间,这个时候之前只不过是分配虚存,内存没有 commit ,这个时候对这个内存地址进行读写操作都会引发一个 page fault 异常(_XCPT_GUARD_PAGE_VIOLATION), OS捕获这个异常,检查一定的条件,适合的时候就把这个内存页 commit 了,即分配了实际的物理内存。然后再次比较需要多少内存,如果还是超过1页(1000h), 则重复第5)步,否则到第6)步。
- 还需要分配的堆栈空间小于1页的时候,“sub ecx,eax” 堆栈继续扩大(扩大了剩余大小的空间);然后“mov esp,ecx”,保存到原来的esp;并且通过“test”为堆栈分配空间。
- 最后,esp的值不再是原来的值,堆栈的大小变成: 堆栈原来大小 + 局部变量需要的堆栈大小 + xx (push 用的一点堆栈的零头).
上面讲的是在vs里的debug模式下运行时的情况,那么在Release模式下是怎样的呢?我们转到Release模式下,编译运行,同样报错
可以看到,跟Debug模式一样。也就是说,如果是这种是在栈区分配了超大缓冲区导致异常,基本都会被_chkstk()检测出来。
针对这种情况,通常我们可以用下面两种方式来解决:
- 修改线程栈区大小,重新编译程序。这种方法只能对我们确定要使用多少栈区的情况下有用,要不然,不能彻底解决。
在VS里可以在这里设置一个比现在大的栈区 - 更好的方式是把这种需要的大缓冲区搬到进程的堆里区申请使用。