栈又溢出了

标签: C++    c++  栈溢出  调试  stackoverflow

缘起

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

“诡异” 的栈溢出

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

stackoverflow

函数调用的时候,会把参数、局部变量、返回地址等信息都存储在栈上,而栈空间默认只有 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

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

alloca_probe

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

__alloca_probe_chkstk

_chkstk

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

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

_chkstk_asm_locations

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

chkstk_content

稍微解释几个关键点:

  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 的代码,来确保栈是可用的。如果栈不够用,则在检查的过程中就会抛出栈溢出错误,这也就是为什么在函数的一开始就报栈溢出的原因。
版权声明:本文为xiaoyanilw原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xiaoyanilw/article/details/109462966

智能推荐

Java编程思想 第三章:操作符

Java中的操作符和c/c++中的操作符基本一致,因为我之前学习过C语言和C++,所以本章的内容大部分都已熟知,下面只做简单的介绍。 Java操作符及优先级 Java中的操作符包括算术操作符,关系操作符,逻辑操作符,位运算符、自操作运算符、移位运算符、赋值运算符和其他运算符。 算术操作符:包括加减乘除和取余(%),优先级乘除取余高于加减,都是双元运算符,其中加法(+)可以用来连接两个字符串,比如:...

JetBrains 系列开发工具,如何配置 `SCSS` `File Watcher` ,相关输出配置参数详解:webStorm phpStorm IDEA

JetBrains 系列开发工具,如何配置 SCSS File Watcher ,相关输出配置参数详解:webStorm phpStorm IDEA 前言 你目前已经了解了如何使用 SCSS 进行开发,了解了该文章的内容:『 SCSS 日常用法 』 在 JetBrains 系列开发工具中通过 FileWatcher 进行编译的 SCSS 文件都是通过 sass 这个程序进行的。『 如何添加 Fil...

C语言小函数—二进制与十六进制

测试如下 “` int main() { long int num = 15; } “`...

仿微博或微信的文章多图显示(自定义MultiImageView)

按照一般的规矩,先上张图来供大伙看看 如果大致是大伙们需要实现的功能,不烦一观 自定义MultiImageView 工具类 具体使用 app.gradle中添加依赖 implementation 'com.github.bumptech.glide:glide:4.8.0' AndroidManifest.xml中配置联网权限 <uses-permission android:name=&q...

经典进程同步和互斥问题

经典进程同步与互斥问题 前言 一、生产者-消费者问题 1.问题描述 2.问题分析 3.代码 二、读者-写者问题 1.问题描述&&分析 2.代码 三、哲学家进餐问题 1.问题描述&&分析 2.代码 四、理发师问题 1.问题描述&&分析 2.代码 前言 在多道程序设计环境中,进程同步是一个非常重要的问题,下面讨论几个经典的进程同步问题。 一、生产者-消费...

猜你喜欢

java设计模式——ThreadLocal线程单例

1、定义一个ThreadLocal线程单例,代码如下: 2、定义一个多线程类,代码如下: 3、定义一个测试类,代码如下: 4、输出结果,如下图:...

【tensorflow】线性模型实战

线性模型:y = 1.477 * x + 0.089   1. 采样数据 采样噪声eps在均值0,方差0.01的高斯分布中,而后在均匀分布U(0,1)中,区间[-10,10]进行n=100次随机采样:   2. 计算误差 循环计算每个点的预测值与真是值之间差的平方并累加,从而获得训练集上的均芳误差损失值。   3. 计算梯度   4. 梯度更新 对权重w和偏...

常见损失函数和评价指标总结(附公式&代码)

网上看到一篇很实用的帖子关于常见损失函数和评价指标,收藏下来 本文转载于https://zhuanlan.zhihu.com/p/91511706 ------------------------------------------------------------------------------------------------------------------------------...

为什么 4G/5G 的直播延时依然很高

通信技术的发展促进了视频点播和直播业务的兴起,4G 和 5G 网络技术的进步也使得流媒体技术变得越来越重要,但是网络技术并不能解决流媒体直播的高延迟问题。 本文不会介绍网络对直播业务的影响,而是会分析直播中常见的现象 — 主播和观众之间能够感觉到的明显网络延迟。除了业务上要求的延迟直播之外,有哪些因素会导致视频直播的延迟这么高呢? live-streaming  图 1 - ...

springboot 过滤器Filter vs 拦截器Interceptor 详解

1 前言       最近接触到了过滤器和拦截器,网上查了查资料,这里记录一下,这篇文章就来仔细剖析下过滤器和拦截器的区别与联系。 2 拦截器与过滤器之间的区别 从上面对拦截器与过滤器的描述来看,它俩是非常相似的,都能对客户端发来的请求进行处理,它们的区别如下: 作用域不同 过滤器依赖于servlet容器,只能在 servlet容器,web环境下使用 拦截器依赖于sp...