使用Visual Studio中的数据断点快速定位内存越界问题的实战案例分享
目录
1、问题说明
2、初步排查
3、使用数据断点监测修改会议名称(内存)操作
3.1、在Visual Studio中对achConfName字段内存设置数据断点
3.2、复现问题,命中数据断点,查看函数调用堆栈深入分析
4、严禁对C++类对象或者包含C++类对象的结构体进行memset操作
C++软件异常排查从入门到精通系列教程(核心精品专栏,订阅量已达8000多个,欢迎订阅,持续更新...)
https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战专栏(重点专栏,专栏文章已更新500多篇,订阅量已达6000多个,欢迎订阅,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/140824370C++ 软件开发从入门到实战(重点专栏,专栏文章已更新300多篇,欢迎订阅,持续更新中...)
https://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)
https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_2276111.html 最近项目中遇到一个典型的内存越界问题,很有代表性,最终通过在Visual Studio中设置数据断点监测出来了。本文详细讲述一下该问题的排查过程以及涉及到的多个细节问题,以供借鉴或参考。
1、问题说明
软件在加入会议后,在某种操作场景下会出现会议名称显示为空的问题,且该问题在该操作场景下几乎是必现的。于是在设置会议名称的代码块中添加人为的条件断点,判断会议名称是否为空,在该条件体中设置一个断点:(人为在代码中构造出if语句条件断点(在if语句内部设置断点),是一种常用的调试方法,这比Visual Studio中的条件断点更加灵活,根据问题的需要设置对应的过滤条件即可)
if (strConfName.IsEmpty())
{int k = 0; // 在此行代码上设置断点
}
编译后发起调试,复现问题后命中了该断点,说明确实出现了会议名称为空的问题。
除了人为添加的条件断点,Visual Studio还有其他多个调试手段与技巧,我之前详细总结过,可以查看文章:
Visual Studio调试技巧与实用方法总结(实战经验分享)
https://blog.csdn.net/chenlycly/article/details/124884225
2、初步排查
查看代码的上下文,发现有不合理的地方,修改后,好像不再有问题了(可能修改后,问题就较难复现了),但想到会议名称是从内存中会议信息结构体成员变量中获取的,按讲加入会议后这个会议信息的结构体中的会议名称不应该变为空的(即使会议名称被会议管理员修改了,也不会修改为空的),所以问题的根源还是没找出来,是存在隐患的,所以还是决定详细排查一下。
到源码中搜索了操作会议信息结构体变量的所有地方,没有清空该结构体数据的操作,只有当会议信息发生变化时平台会主动将更新后的会议信息推送过来,覆盖到本地内存中。当前的会议名称变为空,是平台推送过来的会议信息有问题?还有一种可能,代码中发生了内存越界,越界到会议信息结构体变量的内存上,将会议名称篡改了,改为空了!
3、使用数据断点监测修改会议名称(内存)操作
对于内存中的会议名称被修改为空,可能是服务器推送过来会议信息更新数据有问题,也可能是内存越界篡改导致的,都可以使用Visual Studio数据断点进行监测,监测会议名称是何处修改的。本问题可以找到复现的方法,使用数据断点监测也比较方便,监测到修改后,Visual Studio就会中断下来,查看此时的函数调用堆栈就能知道是何处代码以及哪个业务触发的,就能快速定位问题。
数据断点我们在项目中已使用过多次,Visual Studio中支持数据断点(调试器Windbg也支持对内存地址设置数据断点),对目标变量的内存设置数据断点后,一旦目标变量内存中的内容被修改,数据断点就会中断下来(调试器就会中断下来),查看此时的函数调用堆栈就可以确定修改内存的地方了。
本例中存放会议名称的会议信息结构体定义类似如下:
struct TConfInfo
{char achConfId[64];char achConfName[256];// 还有其他字段,此处省略其他字段
}
加入会议后,会收到平台传过来的会议信息,其中的会议名称是有效的。
设置数据断点的方法是:在将平台传过来的会议信息保存到本端内存中的代码行的下一行设置一个断点,运行到此处时Visual Studio会中断下来,然后对本端的会议信息结构体对象的会议名称字段achConfName设置一个数据断点(先设置一个普通断点,该普通断点命中时,再去设置数据断点),即对achConfName内存进行监测。
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:【C++软件异常与异常排查从入门到精通系列教程】(该精品技术专栏的订阅量已达到10000多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,已经更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总
https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,详细介绍分析C++软件问题的常用分析工具,以图文并茂的方式给出具体的项目问题实战分析实例(详细讲述分析排查过程,很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:【C/C++实战进阶】(该专栏涵盖了C++多方面的内容,是当前重点打造的专栏,订阅量已达8000多个,专栏文章已经更新到500多篇,持续更新中...)
C/C++实战进阶(专栏文章,持续更新中...)
https://blog.csdn.net/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.csdn.net/chenlycly/article/details/131405795
在C++软件开发与维护的过程中,常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro以及内存泄漏检测工具等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏4:【VC++常用功能代码封装】
VC++常用功能开发汇总(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:【C/C++软件开发从入门到实战】(本专栏涵盖了C++多方面的内容,是当前重点打造的专栏,专栏文章已经更新到300多篇,持续更新中!欢迎订阅!)
C++ 软件开发从入门到精通(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了C/C++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
3.1、在Visual Studio中对achConfName字段内存设置数据断点
假设会议信息结构体对象m_tMtConfInfo,m_tMtConfInfo.achConfName就是achConfName字段的首地址(对于数组字段achConfName[256],数组名achConfName就是数组的首地址),所以不用再用&取址符了(即不用&m_tMtConfInfo.achConfName)。
在启动调试的Visual Studio中,点击菜单栏中的调试->新建断点->数据断点,打开如下的窗口:

将m_tMtConfInfo.achConfName拷贝进去,点击确定,这样就对achConfName字段的内存设置了数据断点。在断点窗口中,可以看到刚才设置的数据断点:

一旦achConfName内存中的内容被修改,就会触发该数据断点,调试器就会中断下来,然后查看此时的函数调用堆栈,就可以定位修改achConfName字段内存内容的代码了。
3.2、复现问题,命中数据断点,查看函数调用堆栈深入分析
本案例中的问题几乎是必现的,按照复现步骤复现了问题,命中了设置的数据断点,如下所示:

点击确定。然后到调用堆栈窗口中,查看此时的函数调用堆栈,发现中断在vcruntime140d.dll运行时库中,如下所示:

一般操作内存引发内存越界的函数主要有memset、memcpy等(传入了错误的内存长度,导致内存操作越界),这些函数是C/C++运行时库中的,所以中断在运行时库vcruntime140d.dll中是正常的。
沿着函数调用堆栈向上看,看最近的一条函数调用记录,看看是哪个业务接口调用了memset或memcpy等引发内存越界。双击调用堆栈中的行,跳转到源码中,但在当前函数中并没有看到memset或memcpy函数的调用,如下所示:

看到上述代码中调用了GetConfPtr()->GetConfStartSchedule()接口,于是尝试到GetConfPtr()->GetConfStartSchedule()函数内部去查看,查看函数中的代码上下文,凭借着多年的经验,一眼看出了问题,居然对一个stl容器对象m_tplConfStartSchList执行了memset操作,如下所示:

更为离谱的是,调用memset传入的大小是tagTMTMeetingsList_Api结构体大小,这个结构体非常大,估计是引发内存越界的原因。
为什么对m_tplConfStartSchList进行memset操作会越界到存放会议名称的结构体对象m_tMtConfInfo中呢?我们先go到m_tplConfStartSchList定义处,该变量是vector容器:

从上图可以看到,存放会议信息的结构体对象m_tMtConfInfo,正好在m_tplConfStartSchList不远的地方。
因为调用memset传入的buffer区域大小是tagTMTMeetingsList_Api结构体的大小,该结构体非常大,导致内存越界,然后越界到临近的m_tMtConfInfo变量内存中。这就是引发内存越界的根源,处理办法很简单,调用stl容器的clear接口清空列表即可。当然,如果要清空stl容器占用的内存空间,和一个空的stl容器置换一下位置就好了。
除了数据断点,Visual Studio还有其他多个调试手段与技巧,我之前详细总结过,可以查看文章:
Visual Studio调试技巧与实用方法总结(实战经验分享)
https://blog.csdn.net/chenlycly/article/details/124884225
4、严禁对C++类对象或者包含C++类对象的结构体进行memset操作
本案例中,对一个stl容器进行memset操作,引发了内存越界。对stl容器进行memset操作,会破坏stl容器内部用来管理元素的额外内存,对后续操作该stl容器造成致命的影响。我们在使用struct结构体对象之前,习惯性地对结构体对象进行memset操作,清空内存,按照这个惯性思维,我们习惯在使用很多对象之前进行memset,结果引发问题。
一般禁止对C++类对象进行memset操作的,因为类中如果包含虚函数,类中会隐含一个虚函数表指针,该指针存放的虚函数表首地址,如果进行memset,则会将虚函数表指针的值清为0,后续调用该类的虚函数时会引发崩溃。
一般只能对包含基础类型(比如char、int等)的结构体进行memset操作,但如果结构体中包含迭代器对象,或者类对象,可能包含维护内部内存结构的额外内存,memset就清空了额外内存的值,引发内存管理异常!对于stl容器,只能调用clear或者swap清理元素空间,不能进行memset操作。
如果结构体中包含C++类对象或者stl容器对象,严禁对该结构体对象进行memset操作。十年前在项目中就两次遇到这类问题(遇到过两次,是新人写的代码),代码中对包含stl容器成员的结构体对象进行了memset操作,导致后续对结构体中的stl容器对象进行操作出现异常。对应的实战案例,可以查看下面这篇文章的2.13节有详细的讲到:
引发C++软件异常的常见原因分析与总结(实战经验分享)
https://blog.csdn.net/chenlycly/article/details/124996473 此外,之前总结过C++操作STL容器发生异常的常见原因与场景,可以查看我的文章:
C++程序使用 STL 容器发生异常的常见原因分析与总结
https://blog.csdn.net/chenlycly/article/details/136991353 在编写代码时,一定要下意识地考虑代码是否合理,是否有违规的点,特别是新人写代码更要注意。要对一些基本常识或问题想想为什么,要养成多思考的习惯。之前新人问过一个问题:stl容器中到底存对象还是指针,要分情况看,这也是比较基本的问题,这些基本的问题要想清楚,该话题可以查看我之前写的文章:
当使用stl容器去存放数据时,是存放对象合适,还是存放对象指针(对象地址)合适?
https://blog.csdn.net/chenlycly/article/details/149605794
