【Stack around the variable ‘xxx‘ was corrupted】C++程序中被调函数中发生栈内存越界,越界到主调函数栈内存上,导致内存被篡改的典型案例分析

目录

1、问题描述(栈内存越界 - Stack around the variable 'byVol' was corrupted)

2、查看函数调用堆栈,进行初步分析

3、Visual Studio中的/RTC编译选项说明

3.1、RTC运行时检测可以做哪些检测?

3.2、RTC运行时检测的原理

3.3、为什么Release下/RTC编译是关闭的

4、进一步分析,找到引发问题的原因

5、最后


C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.youkuaiyun.com/chenlycly/article/details/125529931C/C++实战进阶(已更新到460多篇,持续更新中...)https://blog.youkuaiyun.com/chenlycly/article/details/140824370VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.youkuaiyun.com/chenlycly/article/details/124272585Windows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.youkuaiyun.com/chenlycly/category_12695902.htmlC++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.youkuaiyun.com/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)https://blog.youkuaiyun.com/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.youkuaiyun.com/chenlycly/category_2276111.html       最近在项目中调试某个功能时,遇到了一个很典型的栈内存越界问题,被调用函数中在操作传入的引用类型的参数内存时发生栈内存越界,因为传入的参数内存是引用,该参数的内存是在主调函数中分配的栈内存,所以直接越界到主调函数的栈内存上。这个问题很有代表性,且有一定的隐蔽性,本文将此案例的详细排查过程分享出来,以供借鉴或参考。

​1、问题描述(栈内存越界 - Stack around the variable 'byVol' was corrupted)

       在Visual Studio中调试一个新功能时,执行某个操作后,弹出了如下的报错提示框:

​提示信息为:

Run-Time Check Failure #2 - Stack around the variable 'byVol' was corrupted.

提示变量byVol附近的栈内存被破坏了。对于这类问题,一般是栈内存越界导致的,主要有以下两个场景:

1)栈内存越界操作发生在当前的函数中,即局部变量byVol所在的函数中。
2)栈内存越界操作发生在当前函数调用的函数中,将当前函数中局部变量的引用或地址传给被调用的下层函数,被调用的下层函数中对传入的引用或地址有越界行为,因为传入的参数是引用或地址,其内存是在主调函数函数中分配的,对传入的参数内存越界,实际上就是对主调函数中的栈内存越界,内存越界到主调函数中(会篡改主调函数中栈内存中的内容,比如可能修改主调函数中局部变量的值)。这种越界行为具有很强的隐蔽性,较难发现。我们在项目中已遇到过多次了。

       本案例中的栈内存越界就是属于第二种场景,之前没有写过这类问题的排查文章,所以今天来详细讲讲这个案例,很有代表性。

2、查看函数调用堆栈,进行初步分析

       弹出上述报错提示窗时,Visual Studio就中断了下来,于是打开call stack页面,查看此时的函数调用堆栈,如下所示:

​Visual Studio中看不到详细的函数调用堆栈,只能看到问题出在底层的某个dll动态库中。

       对于这种问题出在底层模块中的问题,Visual Studio的函数调用堆栈子窗口中,看不到详细的函数调用堆栈,则需要Windbg附加到程序进程上动态调试去看。直接启动exe主程序(不是在Visual Studio中调试运行),然后将Windbg附加到程序进程上调试运行,复现问题后Windbg会中断下来,就可以查看详细的函数调用堆栈了。当然需要拿来堆栈中涉及到的模块的pdb文件,然后设置到Windbg中,才能在调用堆栈中看到详细的函数名和代码的行号。这个问题是执行某个操作时必现的,所以很好复现。

        到Debug目录中双击启动exe主程序,然后将Windbg附加到进程上,复现问题时产生异常,Windbg中断下来。使用kn命令查看函数调用堆栈,然后使用lm命令堆栈中的涉及到的模块时间戳,找到pdb文件,设置到Windbg中,查看到如下详细的函数调用堆栈:

​在堆栈中看到了校验变量栈内存的RTC_CheckStackvars系统接口(校验栈上的局部变量),我们沿着堆栈向上看,就看到了我们的业务模块中的接口CAudDecCtrl::0nGetOutputVolReq,然后到该函数中查看代码上下文,看看局部变量byVol附近的栈内存为啥被破坏(篡改)了。


HANDLE handle;
handle = CreateFile(strHeadPicFilePath, GENERIC_WRITE, FILE_SHARE_READ,
    NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle == INVALID_HANDLE_VALUE)
{
    DWORD dwLastError = GetLastError();

    strLog.Format(_T("[DownloadHeadPicFunc] 创建图片文件失败:%s,GetLastError:%d."),
        strHeadPicFilePath, dwLastError);
    WriteLog(strLog);

    CloseHandle(handle);
    continue;
}

DWORD dwNumByteWritten = 0;
WriteFile(handle, chunk.memory, chunk.size, &dwNumByteWritten, NULL);
CloseHandle(handle);
free(chunk.memory);

       在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)

专栏1:【C++软件异常与异常排查从入门到精通系列教程】该精品技术专栏的订阅量已达到10000多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,已经更新到210篇以上!欢迎订阅!)

C++软件调试与异常排查从入门到精通系列文章汇总https://blog.youkuaiyun.com/chenlycly/article/details/125529931

本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法详细讲述了C++软件的调试方法与手段详细介绍分析C++软件问题的常用分析工具,以图文并茂的方式给出具体的项目问题实战分析实例(详细讲述分析排查过程,很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!

考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!

专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到300篇以上!

专栏2:【C/C++实战进阶】(该专栏涵盖了C++多方面的内容,是当前重点打造的专栏,订阅量已达8000多个,专栏文章已经更新到500多篇,持续更新中...)

C/C++实战进阶(专栏文章,持续更新中...)https://blog.youkuaiyun.com/chenlycly/category_11931267.html

以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、数据结构与算法C++11及以上新特性(开源代码中可能会用到很多新特性(比如WebRTC开源库),日常编码中也会用到部分新特性,面试时也会频繁地涉及到,学习新特性很有必要)、常用C++开源库的介绍与使用(比如SQLite、libcurl、libwebsockets、libevent、jsoncpp/RapidJson、Redis、RabbitMQ、MongoDB、MQTT、ZooKeeper、OpenCV、FFmpeg、SDL、GStreamer、Live555、ReactOS等)、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(引发C++软件异常的常见原因分析与总结、排查C++软件异常的手段与方法、分析C++软件异常的基础知识、使用常用软件分析工具分析C++软件问题、多个项目实战问题分析案例分享等)、设计模式(单例模式、工厂模式、观察者模式、状态模式等)、网络基础知识与网络问题分析进阶内容(实战问题分析实例分享)等。本专栏的内容都是建立在项目实践的基础上,来源于项目实战,服务于项目实战,很有实战参考价值!

专栏3:【分析C++软件问题的实用软件与高效工具实战案例集锦】

C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.youkuaiyun.com/chenlycly/article/details/131405795

常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro以及内存泄漏检测工具等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!

专栏4:【VC++常用功能代码封装】

VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.youkuaiyun.com/chenlycly/article/details/124272585

将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。

专栏5:【C/C++软件开发从入门到实战】

C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.youkuaiyun.com/chenlycly/category_12695902.html

根据多年C++软件开发实践,详细地总结了C/C++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。


3、Visual Studio中的/RTC编译选项说明

       RTC的全称是Run-time Check,运行时检测,即在运行时对相关内存进行检测,在Visual Studio中其对应的是/RTC编译选项。在Visual Studio中,在Debug下,/RTC编译选项是默认打开的,如下所示:(C/C++ ->> 代码生成 ->> 基本运行时检查

​Release下则是默认关闭的。

3.1、RTC运行时检测可以做哪些检测?

       RTC运行时检测,可以检测代码中是否向较小的数据类型赋值会导致数据丢失、函数中局部变量附近的栈内存是否有越界等:

​        关于RTC运行时检测的详细说明,可以到微软MSDN上查看详细说明:

/RTC(运行时错误检查)https://learn.microsoft.com/zh-cn/cpp/build/reference/rtc-run-time-error-checks

3.2、RTC运行时检测的原理

       此处我们简单地说一下检测函数中局部变量附近的内存是否被越界的原理。开启/RTC编译选项后,在给变量分配内存时,会在代码申请的内存末尾处多追加额外分配一些内存,用来存放一些桩信息

​即向此部分额外的内存中填充固定的值。如果函数栈内存有越界,则会篡改变量内存末尾处追加的那部分内存中的桩信息(修改内存中的值),这样在函数末尾处去检测这些桩信息内存区域,就能确定局部变量的内存是否被篡改了。这就是本例中检测出函数中局部变量byVol附近的栈内存被篡改的大概原理。

       通过/RTC编译选项,可以帮我们发现程序很多潜在的问题,当检测到异常时,就会弹出类似的提示框。还可以检测出调用函数时,函数调用约定不一致导致的栈不平衡问题,当出现该问题时,会弹出如下的提示框:

​       之前我们在提供C++开发的SDK给第三方厂商使用时,就遇到过函数调用约定不一致导致的栈不平衡问题,我们的SDK包提供的标准C接口,其中有个设置回调函数的API接口,但是声明时回调函数没有指定调用约定,在我们编译SDK时使用了默认的C调用约定,而第三方厂商使用的是C#开发语言,默认的是__stdcall标准调用,他们在实现回调函数接口时没有指定调用约定,所以函数调用约定在第三方厂商这边编译时使用的是__stdcall标准调用约定。这样当程序调用回调函数后,就出现因为调用约定不一致产生的栈不平衡的问题。

       关于C/C++中的常用调用约定以及因为调用约定不一致导致的栈不平衡问题的详细说明,可以查看我的文章:

C/C++函数的调用约定详解https://blog.youkuaiyun.com/chenlycly/article/details/125354572        /RTC编译选项对函数中相关变量的校验代码,C++代码中是看不到的,是在编译代码时加进去的,查看汇编代码是可以看到的。关于/RTC编译选项的详细实例说明,可以参考下面的文章:

运行时错误检查(/RTC)编译选项及实现原理https://blog.youkuaiyun.com/chenlycly/article/details/52485476利用_RTC_CheckStackVars(...) 和 Windbg 发现访问越界https://blog.youkuaiyun.com/chenlycly/article/details/52485425

3.3、为什么Release下/RTC编译是关闭的

       Release下默认是关闭/RTC编译选项的,因为打开/RTC编译选项后,会给变量多分配一些用于存放桩信息的内存,还会增加检测内存操作是否异常(检测桩信息有没有被修改)的代码,这些都是额外的开销,会增加程序内存的占用,也会因为多执行了检测代码拖慢程序的执行速度,这对发布release版本程序是不利的。

       /RTC编译选项在Release下是默认关闭的,也是可以选择打开的:

​       有的模块还真是在Releae下开启了/RTC编译选项有次在排查软件下层的音视频模块问题时,居然在函数调用堆栈中看到居然调用了RTC运行时检测函数RTC_CheckStackvars(校验栈上的局部变量),当时就很疑惑,对于release发行版本,默认应该是关闭了/RTC编译选项,为啥程序运行起来后还会执行运行时检测呢?难道是Release下打开了/RTC编译选项?

       于是找到音视频模块的开发同事,到他机器上查看了该模块工程在Visual Studio中工程属性中的配置,居然在Release下也打开了/RTC编译选项,这就和函数堆栈中调用运行时检测函数RTC_CheckStackvars对上了,这就消除了我最开始的疑惑了。关于RTC_CheckStackvars函数的详细说明,可以参考下面的文章:

利用_RTC_CheckStackVars(...) 和 Windbg 发现访问越界https://blog.youkuaiyun.com/chenlycly/article/details/52485425

4、进一步分析,找到引发问题的原因

        根据Windbg显示的函数调用堆栈,报错窗口中的提示的局部变量byVol位于下层模块的CAudDecCtrl::0nGetOutputVolReq接口中。于是查看CAudDecCtrl::0nGetOutputVolReq接口的代码:

​发现该接口中没有触发内存越界的操作,但该接口调用了当前模块中另一个接口pcAudDecoder->GetAudOutputVolume,且传入的正是byVol变量的引用:

​但该接口中也没有引发内存越界的操作,主要调用了更下层的音视频编解码模块的m_pcAudDec->GetAudioVolume,然后调用了多个函数,进入了如下的这个函数:

​传入的参数依然是u8的引用,就是最上层的局部变量byVol的引用。紧接着看到了问题,居然将一个u8的引用强制转成u32的引用,这是导致内存越界的根本原因我们再向下看调用的接口,最后进入了如下的函数:

​CAudioMixer::GetVolumeValue函数,函数中调用系统API函数mixerGetControlDetails获取扬声器音量大小,获取到的音量保存在MIXERCONTROLDETAILS_UNSIGNED vol的dwValue字段中,然后将该DWORD类型的dwValue值直接赋值给传入的u32引用类型的dwvolume。这个u32(32bit)引用类型正是上层从u8引用强转过来的,此处的赋值拷贝的4字节的内存写到dwvolume中,而这个dwvolume引用其实就是最上层的byVol的引用,而byVol变量是u8(8bit)类型,只占1字节内存,所以此处的赋值导致内存越界了!

编写这段问题代码的人,可能觉得将u8引用强转成u32引用应该没什么问题,实则是因为编码经验不足,埋下了很大的隐患。

       因为传下来的一直是引用,即都是最上层函数中的byVol的引用(局部变量byVol占用栈内存是在最上面的主调函数中分配的),所以对byVol的越界,就是越到最上层函数的栈空间中,直接将byVol变量(占用1字节)后面的3个字节给越界篡改了,所以导致RTC运行时检测检测出byVol变量附近的栈内存被破坏(篡改)了。导致内存越界的根本原因,正是CAudDecWrapper::GetAudioSysVolume接口中将u8引用强转成u32引用导致的,引用可以理解成指针,传入的内存还是u8的1字节,所以导致赋值时产生了越界。

       注意,上面截图中的赋值,和直接将u32(4字节)的变量赋值给u8(1字节)时的内存截断有这个本质的区别

u32 dwVal = 3;
u8 byVal = dwVal; // 赋值操作其实就是内存拷贝,此处只会拷贝1个字节内存内容,即内存截断

       对变量的赋值,从汇编代码上看,就是内存拷贝,根据要被赋值的变量类型,确定从源内存中拷贝的内存块大小,然后拷贝到被赋值变量的内存中。很多代码的细节,都要从内存操作的角度去看去理解!

       此外,如果要了解函数调用时的栈分布情况,可以查看我的文章:

C++函数调用栈分布详解https://blog.youkuaiyun.com/chenlycly/article/details/121001096

5、最后

       本文讲解的问题实例,是栈内存越界越界到主调函数栈内存的典型场景,虽然不是很难排查的问题,但有一定的隐蔽性,很有代表性,所以在此分享出来。我们在排查问题的过程中,一定多关注细节多思考,一般都会加深对相关细节点的理解和认知,很有价值。

评论 84
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dvlinker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值