一、什么是线程栈溢出

我们都知道,每一个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", n);
    return 0;
}

由于在VS里默认线程栈空间为1MB,我们代码里在栈区分配了一个刚好1MB的数组,在加上其他函数入栈占用的空间,肯定就会超出1MB产生溢出

在VS2013里编译,Debug模式下调试运行,果然报错,跟我们前面预想的一样

我们注意到此时的调用栈如下:

看此时的栈,没又被破坏,最后停在了_chkstk()里,这是个什么样的函数呢。这个函数其实是VC++的crt函数,用来检测堆栈的。停在这里,说明这个函数检测到了这种情况栈溢出。我们看看它的代码,在VS里转到汇编模式下

在地址栏里输入上面错误提示框里地址0x00F21767,得到

_chkstk:
push ecx
cmp eax,1000h
lea ecx,[esp+]
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+]
push eax
ret

大概流程

  1. chkstk例程是C编译器的助手例程。对于x86当局部变量超过4096字节;对于x64编译器,它分别是4K和8K。即, 当一个函数局部变量超过一个页面大小4k的时候, 编译器会自动插入这个函数。插入这个函数的位置在:

    push        ebp
    mov ebp,esp
    mov eax,1000D4h
    call _chkstk()
  2. 这个函数做什么?当函数为堆栈分配的页面不够时, (堆栈默认大小为1M), 堆栈需要更多的页面时调用这个函数。当堆栈使用大于分配的大小(默认1M)时,产生_XCPT_UNABLE_TO_GROW_STACK.
  3. 当调用这个函数时, 首先外面对eax进行赋值(已经分配的堆栈大小 + 即将分配给函数局部变量的堆栈大小)
  4. 调用 _chkstk() 首先,保持当期esp到eax中,然后开始判断:如果分配的大小大于一个页面, 到第5)步(大多都先第5)步);否则到第6)步。
  5. 当需要分配的大小大于一个页面, 则增加一个页面。“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)步。
  6. 还需要分配的堆栈空间小于1页的时候,“sub  ecx,eax” 堆栈继续扩大(扩大了剩余大小的空间);然后“mov  esp,ecx”,保存到原来的esp;并且通过“test”为堆栈分配空间。
  7. 最后,esp的值不再是原来的值,堆栈的大小变成: 堆栈原来大小 + 局部变量需要的堆栈大小 + xx (push 用的一点堆栈的零头).

上面讲的是在vs里的debug模式下运行时的情况,那么在Release模式下是怎样的呢?我们转到Release模式下,编译运行,同样报错

可以看到,跟Debug模式一样。也就是说,如果是这种是在栈区分配了超大缓冲区导致异常,基本都会被_chkstk()检测出来。

针对这种情况,通常我们可以用下面两种方式来解决:

  • 修改线程栈区大小,重新编译程序。这种方法只能对我们确定要使用多少栈区的情况下有用,要不然,不能彻底解决。
    在VS里可以在这里设置一个比现在大的栈区
  • 更好的方式是把这种需要的大缓冲区搬到进程的堆里区申请使用。

win32线程栈溢出问题 (一)的更多相关文章

  1. win32线程栈溢出问题 (二)

    3.2.函数递归调用引发的栈溢出 写一段最简单的无穷递归代码,如下: #include "stdafx.h" void f(void) { f(); } int _tmain(in ...

  2. win32线程池代码(WinApi/C++)

    win32线程池代码(WinApi/C++) 健壮, 高效,易用,易于扩, 可用于任何C++编译器 //说明, 这段代码我用了很久, 我删除了自动调整规模的代码(因为他还不成熟)/********** ...

  3. win32线程

    win32线程 一丶什么是线程 在windows中常听到的就是线程.多线程.啊什么的. 这里介绍一下什么是线程. 1.线程是附属在进程中的一个执行实体.简而言之就是执行代码的. 2.每个进程至少有一个 ...

  4. Win32线程——优先权

    <Win32多线程程序设计>–Jim Beveridge & Robert Wiener Win32 优先权是以数值表现的,并以进程的“优先权类别(priority class)” ...

  5. Win32 线程同步

    Win32 线程同步 ## Win32线程同步 ### 1. 原子锁 ### 2. 临界区 {全局变量} CRITICAL_SECTION CS = {0}; // 定义并初始化临界区结构体变量 {线 ...

  6. C++ win32线程数上限

    hThread = CreateThread( NULL,  0,  WorkerFunction,  &threadParm,  0, &dwThreadID  ); 这样的创建方法 ...

  7. Win32线程——等待另一个线程结束

    转载: https://blog.csdn.net/yss28/article/details/53646627 <Win32多线程程序设计>–Jim Beveridge & Ro ...

  8. win32 线程通信初步

    // 线程通信机制.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #define NUM_THREADS 10 #include < ...

  9. Win32线程安全问题.同步函数

    线程安全问题.同步函数 一丶简介什么是线程安全 通过上面几讲.我们知道了线程怎么创建.线程切换的原理(CONTEXT结构) 每个线程在切换的时候都有自己的堆栈. 但是这样会有安全问题. 为什么?  我 ...

随机推荐

  1. Luogu4827 Crash的文明世界 组合、树形DP

    传送门 又是喜闻乐见的\(k\)次幂求和题目 那么\(S(x) = \sum\limits_{i=1}^n dist(i,x)^k = \sum\limits_{i=1}^n \sum\limits_ ...

  2. 如何在 WPF 中获取所有已经显式赋过值的依赖项属性

    原文:如何在 WPF 中获取所有已经显式赋过值的依赖项属性 获取 WPF 的依赖项属性的值时,会依照优先级去各个级别获取.这样,无论你什么时候去获取依赖项属性,都至少是有一个有效值的.有什么方法可以获 ...

  3. C# vb .net实现扭曲角特效滤镜图像处理

    在.net中,如何简单快捷地实现Photoshop滤镜组中的扭曲角效果呢?答案是调用SharpImage!专业图像特效滤镜和合成类库.下面开始演示关键代码,您也可以在文末下载全部源码: 设置授权 第一 ...

  4. FindWindow SendMessage

    FindWindow 用来根据类名和窗口名来得到窗口句柄的.但是这个函数不能查找子窗口,也不区分大小写. 如果要从一个窗口的子窗口中查找需要使用FindWindowEX. 1.在C#中使用方法如下: ...

  5. HTTP API 认证授权术

    原文:https://coolshell.cn/articles/19395.html 我们知道,HTTP是无状态的,所以,当我们需要获得用户是否在登录的状态时,我们需要检查用户的登录状态,一般来说, ...

  6. PHP基础之输出缓冲区基本概念、原理分析

    一.概念 在PHP运行的过程中,可以将会产生输出的函数或操作结果暂时保存在PHP的缓冲区,只有当缓冲区满了.或者PHP运行完毕.或者在必要时候进行输出,才会将数据输出到浏览器,此缓冲数据的区域称为PH ...

  7. 笔谈OpenGL ES(一)

    现在图形类.视频类app越来越多,学习OpenGL ES是很有必要的,作为程序员是有必要做技术积累的.现在做播放器开发的工作,正好也涉及这块,那就好好学一学. CSDN上有套教程不错,OpenGL E ...

  8. arm9的中断

    GPIO 习惯了stm32的GPIO,发现高端处理器arm在这方面反而简单了. ARM9控制GPIO只有三种寄存器. GPxCON:配置引脚功能,GPACON用一位控制一个GPIO,分别是0为输出引脚 ...

  9. 【MySQL】SQL语句基础

    一.操作数据库 1.1 创建数据库 1.2 查看数据库 1.3 修改数据库 1.4 删除数据库 1.5 选择数据库 二.操作表 2.1 创建表 2.2 查看表 2.3 修改表 2.4 删除表 三.操作 ...

  10. 加标签的continue用法

    1.加标签的continue,类似于C语言的goto语句