栈又溢出了

简介: 栈又溢出了

缘起

最近,项目代码再次出现了栈溢出问题。这次的栈溢出跟上次有点不同,调用栈不深,而且报错的时候函数代码还没开始执行。是不是有点“诡异”?一起来看看这次是什么原因导致的吧。

“诡异” 的栈溢出

运行程序后,异常发生了。对于程序崩溃,早就见怪不怪了。重启程序,附加调试器,再次执行相同的功能,果然中断到调试器中。有了上次的经验(没仔细看错误提示导致懵逼了很久,文章在这里),仔细检查了错误码,又是 0xC00000FD, stackoverflow。在 vs2013 中按 ctrl + alt + c 查看调用栈,发现调用栈并不深,没有递归调用的迹象。仔细看报错的位置,居然没有执行到任何代码。下图是我用测试代码截的图:

stackoverflow.png

函数调用的时候,会把参数、局部变量、返回地址等信息都存储在栈上,而栈空间默认只有 1 MB,如果调用栈帧太多,那么可能会用光这 1 MB,从而导致 stack overflow

小贴士:这里的 1 MB 不太精确,实际可用的栈空间比 1 MB 小,最后一个页面永远是不可用的。为了描述简单而且好记就这么描述了。

大胆猜测

调用栈并不深,难道就是这几个栈帧就把栈耗光了?简单浏览当前函数中的局部变量和参数,很快就找到了几个值得怀疑的局部变量。但是通过 sizeof 查看对应结构体大小后发现:虽然大(大概 400 KB),但是并没有大到爆栈的程度。继续观察,发现了一个很有意思的现象,这些变量在每个 if else 分支中都定义了一份,难道这些分支中的局部变量占用的栈空间被累加了?一个大概 400 KB3 个加起来就超过 1 MB 了(默认的栈大小是 1 MB),足以爆栈了!到底是不是这样的呢?

确认猜想

为了确认猜想是否正确,新建一个简单的测试工程,测试代码如下:

#include "stdafx.h"

struct BigData { char data[409600]; /* 400KB */ };

void Use(BigData* pData) { printf("%c", pData[0]); }

void CorrpuptStackEasyly(int argc)
{
    if (argc == 2)
    {
        BigData data;
        Use(&data);
    }
    else if (argc == 3)
    {
        BigData data;
        Use(&data);
    }
    else
    {
        BigData data;
        Use(&data);
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    CorrpuptStackEasyly(argc);
    return 0;
}

BigData 大概占用 400 KB,如果猜想(三个 BigData 类型的局部变量会占用 1.2 MB 左右的空间)是正确的,那么这个函数应该会爆栈。编译运行,果真和猜想的一样——爆栈了!

stackoverflow.png

查看函数 CorrpuptStackEasyly 对应的反汇编,如下图:

alloca_probe.png

0x12C0DC 转换成十进制是 1229020 大概是 1.2 MB__alloca_probe 是编译器生成的函数,内部直接跳转到 _chkstk

__alloca_probe_chkstk.png

_chkstk

从名字可以很容易的猜出 _chkstk 是用来检查栈的。当函数中包含超大局部变量(大于等于一个页面, 4 KB)时,编译器会在函数头部插入一段检查栈是否够用的代码。

_chkstk 虽然是汇编代码写的,但是内部逻辑并不复杂,而且在安装 vs 的时候提供了带注释的源码,可读性极强。我机器上同时安装了 vs2010vs2013,可以在下图中的几个位置找到 _chkstk 对应的汇编代码文件 chkstk.asm,如下图:

_chkstk_asm_locations.png

因为这个函数超级精炼,有效汇编代码不到 20 行,这里截图放上来,感兴趣的小伙伴儿可以读一读:

chkstk_content.png

稍微解释几个关键点:

  1. EAX 记录了需要检查的栈大小,外部调用的时候需要设置好。

  2. ECX 记录最低地址(栈是从高向低扩展的)。(73,74 行)

  3. 根据 ESP 计算出当前地址所属页面的起始位置。(83,84 行)

  4. 判断是否结束,没结束则执行 5,6,7步。(87, 88 行)

  5. 减去 _PAGESIZE 得到下一页面的起始位置(98 行)

  6. 读取四字节(99行)。

    本行代码是关键,如果访问的地址所在的页面是保护页面(带有 PAGE_GUARD 属性)并且经判定不需要抛栈溢出异常,则会触发 STATUS_GUARD_PAGE_VIOLATION 异常(应该内部叫 _XCPT_GUARD_PAGE_VIOLATION,异常码是 0x80000001),操作系统会去除保护页面的保护属性,并分配物理内存,为下一个界面设置保护属性。

  7. 跳转到第四步(cs10 的位置),不断重复这个过程。(100行)

注意:创建线程的时候指定了一个栈保留大小(默认是 1MB),刚开始的时候这 1MB 并不是都对应着物理内存,是按需分配的。这里说的增长栈空间,并不是栈保留大小变大了,而是占用的物理页增多了。相信大多数小伙伴儿应该已经知道了,但是这里还是要啰嗦一句:访问某个虚拟地址的时候,只有当这个虚拟地址对应的页面有与之对应的物理页面才可以访问,否则会报访问异常。

排除问题

知道问题的根源后,解决就简单了。只需要消除重复的大局部变量即可。把分支中重复的变量提取到函数开始的位置即可。

void CorrpuptStackEasyly(int argc)
{
    BigData data;
    if (argc == 2)
    {
        Use(&data);
    }
    else if (argc == 3)
    {
        Use(&data);
    }
    else
    {
        Use(&data);
    }
}

深入思考

说实话,解决完这个问题后,我是震惊的! vs 真的就这么简单粗暴的把所有局部变量的大小累加起来为函数分配栈空间吗?这太太太不合理了!如果真是这样,分支多的函数太有可能出现栈溢出了。个人觉得合理的做法是:把分支中占用内存最大的作为分支部分的内存占用,加上其它不在分支中的局部变量的内存空间来为函数分配栈空间。

总结

  • 并不只有递归调用才会导致栈溢出,过度使用栈空间就会导致栈溢出。
  • 线程默认栈保留大小是 1 MB,如果确实需要使用大局部变量,请考虑在堆上分配,避免栈溢出的问题。
  • 如果函数内部有超大局部变量,编译器会在最开始的位置插入调用 _chkstk 的代码,来确保栈是可用的。如果栈不够用,则在检查的过程中就会抛出栈溢出错误,这也就是为什么在函数的一开始就报栈溢出的原因。
相关文章
|
15天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
6天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
15 1
|
9天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
12天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
14天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
41 4
|
1月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
30 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
18天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
初步认识栈和队列
初步认识栈和队列
58 10
|
1月前
数据结构(栈与列队)
数据结构(栈与列队)
17 1
|
1月前
|
算法
数据结构与算法二:栈、前缀、中缀、后缀表达式、中缀表达式转换为后缀表达式
这篇文章讲解了栈的基本概念及其应用,并详细介绍了中缀表达式转换为后缀表达式的算法和实现步骤。
44 3

热门文章

最新文章