目录
1、问题描述
2、访问空指针或者野指针
3、C++程序中常见的异常内存值
4、0xdddddddd内存访问违例问题分析与排查
4.1、初步分析
4.2、CConfMeidaConfigDlg窗口类对象是何时被销毁的?
4.3、为啥会访问到已经释放内存的CConfMeidaConfigDlg类对象?
5、关于0xcdcdcdcd和0xfeeefeee异常值的排查案例
6、最后
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.htmlWindows C++ 软件开发从入门到精通(专栏文章,持续更新中...)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 在C++软件中访问了有问题的指针变量,可能会引发程序产生异常,比如访问了空指针和野指针,这也是操作指针错误最常见的原因。今天我们就来讲一个操作已经释放内存的野指针实例,本例中的问题指针有些特殊,释放内存后指针值变为0xdddddddd,和以往常见的异常值0xfeeefeee有所不同。本文将详细讲述一下这个问题的完整分析过程,以供大家借鉴或参考。
1、问题描述
某天在调试某个功能时发现,在执行某个操作(为了方便说明问题,下文将该操作称作A操作)后再次执行该操作,程序就出现了短暂的卡死,然后出现了闪退。这个问题是必现的,于是用Visual Studio调试运行,按照复现的步骤复现了问题,弹出了如下的访问0xdddddddd异常内存地址的弹框:
对于32位程序,这个0xddddddddd地址属于内核态的内存地址,用户态的代码是禁止访问的,所以触发了内存访问违例,产生了崩溃。下面就详细讲述一下这一问题的排查过程。
2、访问空指针或者野指针
在操作指针时,最常见的问题就是访问空指针和野指针。操作这两类指针产生的异常,均是因为使用该指针去访问了指向类对象的数据成员(把指针中存放的地址作为类对象首地址去访问),访问了不该访问的内存地址,引发了内存访问违例。对于空指针,在使用空指针访问了类对象的数据成员,就会访问很小的内存地址,小于64KB地址值的内存区域是禁止访问的。
产生野指针,主要有两个场景:
1)Release下没有对指针变量进行初始化,指针变量的值是个随机值,是分配内存时内存中残留的随机值,此时的指针就是野指针。
2)指针指向的内存被释放了,但指针没有置为NULL,此时的指针也是野指针。
通过野指针中的值去访问指向类的数据成员,也会触发访问不该访问的内存地址,触发内存访问违例,引发崩溃。
3、C++程序中常见的异常内存值
在讲述这个问题排查过程之前,需要先讲一下C++程序中常见的异常内存值,这了解这些异常内存值之后,可能就能知道是什么原因引发的,这样就给我们排查问题指明了方向。所以作为Windows C++开发人员,很有必要去了解这些常见的异常地址。
C++程序中常见的异常内存值,如下所示(异常值不区分大小写,比如0xcdcdcdcd也可以写成0xCDCDCDCD):
* 0xcccccccc:Used by Microsoft's C++ debugging runtime library and many DOS environments to mark uninitialized stack memory.CC resembles the opcode of the INT 3 debug breakpoint interrupt on x86 processors.
* 0xcdcdcdcd:Used by Microsoft's C/C++ debug malloc() function to mark uninitialized heap memory, usually returned from HeapAlloc().
* 0xfeeefeee:Used by Microsoft's debug HeapFree() to mark freed heap memory. Some nearby internal bookkeeping values may have the high word set to FEEE as well.
* 0xdddddddd:Used by MicroQuill's SmartHeap and Microsoft's C/C++ debug free() function to mark freed heap memory.
* 0xabababab:Used by Microsoft's HeapAlloc() to mark "no man's land" guard bytes after allocated heap memory.
* 0xabadcafe:A startup to this value to initialize all free memory to catch errant pointers.
* 0xbaadf00d:Used by Microsoft's LocalAlloc(LMEM_FIXED) to mark uninitialised allocated heap memory.
* 0xbadcab1e:Error Code returned to the Microsoft eVC debugger when connection is severed to the debugger.
* 0xbeefcace:Used by Microsoft .NET as a magic number in resource files.
这里我们主要关注0xcccccccc、0xcdcdcdcd、0xfeeefeee和0xdddddddd这四个异常内存值。这几个异常地址都是Debug下默认设置的,0xcccccccc用来填充未初始化的栈内存,0xcdcdcdcd用来填充未初始化的堆内存,0xfeeefeee用来填充已经释放的堆内存,0xdddddddd也是用来填充已经释放的堆内存。
0xfeeefeee和0xdddddddd都是用来填充已经释放的堆内存区域,但不太清楚这两个异常值的区别。我们经常在调试代码遇到0xfeeefeee,基本很少遇到过0xdddddddd,本案例还是第二次遇到0xdddddddd。
我们需要对这些异常内存值有较强的敏感度,如果在调试代码中遇到这些异常值,要第一时间反应过来,通过这些异常内存值的含义,可以明确地提示我们当前访问的内存大概出了什么问题。这样我们排查时就有了一定的方向。
4、0xdddddddd内存访问违例问题分析与排查
根据异常中断时的弹窗提示,当前访问了异常内存值0xdddddddd:
通过这个异常值的含义大概知道可能是访问了已经释放内存的野指针引发的。
0xdddddddd异常值提示我们程序中访问了已经释放的内存区域,那我们需要去分析这块内存是何时释放的,以及为什么会出现访问已释放内存的问题。可以结合当前的函数调用堆栈,结合代码上下文的逻辑,大概就能分析出原因了。
4.1、初步分析
根据Visual Studio中断的代码位置:
问题发生在COptionUI::Selected函数中,当前函数中的this指针值为0xddddddddd,即当前的COptionUI类对象的地址为0xddddddddd,通过该类对象地址去访问该类的数据成员m_bSelected产生了异常,数据成员m_bSelected的内存地址就是相对所在类的类对象首地址的偏移,是个内核态的内存地址,所以出现了内存访问违例,产生了崩溃。
我们需要搞清楚当前出问题的COptionUI类对象位于那个具体的业务类中。沿着此时的函数调用堆栈,双击截图中的条目:
可以看到是CConfMeidaConfigDlg窗口类中的m_pBtnNotShow按钮控件指针的值为0xddddddddd,如下:
根据0xddddddddd的含义,是用来填充已经释放的堆内存的,所以基本可以确定m_pBtnNotShow所在的CConfMeidaConfigDlg类对象已经被delete了,即CConfMeidaConfigDlg类对象的内存被销毁了!
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到430多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有PE工具、Dependency Walker、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏3:
C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域的多个方面的内容,同时给出C/C++及网络方面的常见笔试面试题,并详细讲述Visual Studio常用调试手段与技巧!
专栏4:
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:
Windows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了Windows C++ 应用软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
4.2、CConfMeidaConfigDlg窗口类对象是何时被销毁的?
首先要搞清楚CConfMeidaConfigDlg类对象是何时被销毁的。很简单,我们直接在CConfMeidaConfigDlg类的析构函数中打个断点,复现一下问题,就知道了。
在CConfMeidaConfigDlg类的析构函数中打了断点,然后重新启动调试,然后执行第一次操作,操作完成后命中了断点,如下所示:
查看此时的函数调用堆栈,是某个窗口消息触发CConfMeidaConfigDlg类对象销毁的。于是双击收发窗口消息的函数条目,进入函数:
是代码中给CConfMeidaConfigDlg窗口发送了WM_CLOSE,然后duilib框架将窗口类对象delete了。
4.3、为啥会访问到已经释放内存的CConfMeidaConfigDlg类对象?
相关代码是其他同事写的,于是查看了代码的上下文逻辑,在软件执行当前的A操作时会去创建这个CConfMeidaConfigDlg窗口类:
但操作结束后并不会去销毁这个窗口,而是保存在成员变量中,下次再次执行当前的这个A操作时继续使用。
但第一次执行A操作时,操作结束后CConfMeidaConfigDlg类对象就被销毁了,这个和原有的逻辑就不一致了。于是搜索WM_CLOSE消息,看看哪里给CConfMeidaConfigDlg窗口类发送了WM_CLOSE,然后找到了问题代码:
是这个地方发送了WM_CLOSE消息,后面还专门有个注释,于是查看svn的修改记录,原先是发送WM_UPDATE_CTRL_WINDOW消息,被一个同事改成了发送WM_CLOSE消息。这个同事有点不负责啊,没搞清楚代码的逻辑,就随意地修改了!
上述代码在A操作动作结束后会被调用到,给CConfMeidaConfigDlg窗口类发送了WM_CLOSE消息,导致CConfMeidaConfigDlg窗口被销毁,CConfMeidaConfigDlg窗口类对象被析构(析构后会将原先分配给CConfMeidaConfigDlg对象的内存区域置为0xdddddddd),但存放CConfMeidaConfigDlg类对象地址的指针变量m_pConfMeidaConfigDlg并没有置空,指针变量中还是之前的类对象指针,这个指针就变成了野指针。
再次执行A操作时,判断m_pConfMeidaConfigDlg指针变量不为空,就直接使用这个指针了,从而访问了野指针,从而出现访问0xdddddddd导致内存访问违例,引发崩溃的问题。
解决办法很简单:
将上述代码中的WM_CLOSE改成之前的WM_UPDATE_CTRL_WINDOW消息即可。
5、关于0xcdcdcdcd和0xfeeefeee异常值的排查案例
关于常见异常内存值0xcdcdcdcd、0xfeeefeee和0xdddddddd的排查案例,可以参见我之前写的文章:
0xcdcdcdcd异常值引发C++程序崩溃问题的详细分析https://blog.csdn.net/chenlycly/article/details/128380751排查软件启动时访问了0xcdcdcdcd内存地址导致内存访问违例的崩溃https://blog.csdn.net/chenlycly/article/details/125266735排查软件关闭时访问了0xfeeefeee内存地址导致内存访问违例的崩溃https://blog.csdn.net/chenlycly/article/details/125267046访问0xdddddddd内存地址引发软件崩溃的问题排查https://blog.csdn.net/chenlycly/article/details/132631020
6、最后
大家在调试代码要提高对0xcccccccc、0xcdcdcdcd、0xfeeefeee和0xdddddddd等常见异常内存值的敏感度,看到这些异常值就能大概地估计是什么原因导致的,这样我们就有了问题排查的方向。此外,在设置断点也要有一定的技巧,比如使用数据断点监测内存越界问题、人为添加if条件语句构造“条件”断点、在命中一个断点后再设置后续断点等等。