一、什么是线程栈溢出

我们都知道,每一个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. voltile解析

    https://www.cnblogs.com/dolphin0520/p/3920373.html

  2. proxy_banner

  3. JAVA day2 语言基础

    一.注释 描述代码的文字 1.//    单行注释 2./*  */ 多行注释 (多行注释中不能再嵌套多行注释) 3./**  */  多行注释 配合JavaDoc工具使用(只可以看到注释,看不到代码 ...

  4. 「LibreOJ NOI Round #2」不等关系

    「LibreOJ NOI Round #2」不等关系 解题思路 令 \(F(k)\) 为恰好有 \(k\) 个大于号不满足的答案,\(G(k)\) 表示钦点了 \(k\) 个大于号不满足,剩下随便填的 ...

  5. redis订阅与发布系统

    一.概述 1.redis通过publish.subscribe等命令实现了订阅与发布模式. 2.这个功能提供两种信息机制,分别是订阅/发布到频道和订阅/发布到模式. 二.频道的订阅与信息发送 1.re ...

  6. Synchronized 与Lock的不同之处

    Synchronized 与Lock的不同之处 用法不一样.synchronized既可以加在方法上,也可以加载特定的代码块上,括号中表示需要锁的对象.而Lock需要显示地指定起始位置和终止位置.sy ...

  7. BUAAOO-Final-Summary

    目录 总结本单元两次作业的架构设计 总结自己在四个单元中架构设计及OO方法理解的演进 总结自己在四个单元中测试理解与实践的演进 总结自己的课程收获 立足于自己的体会给课程提三个具体改进建议 两次架构设 ...

  8. iOS - Xcode中从动态库剥离不需要的架构

    自从iOS 8发布以来,开发人员已经能够利用动态库对iOS开发的好处.对于一般开发,为所有需要的架构设置一个单一的动态库是非常好的,所以您可以在所有设备和iOS模拟器上运行,而无需更改任何东西.然而, ...

  9. iOS - app 进行安全加固

    研究了大半年逆向工程了,没在博客做记录,最近看到篇,跟自己的想法不谋而合,摘要下: 运行在越狱设备上的 iOS app,非常容易遭到破解分析,这里我列举一些可以加大破解难度的方法,希望有所帮助. 一些 ...

  10. 208道Java常见的面试题

    一.Java 基础 1.JDK 和 JRE 有什么区别? JRE=JVM+各种基础类库+java类库(String\System) JDK>JRE>JVM JRE:是java运行时环境  ...