目录
1、问题说明
1.1、Release下崩溃,Debug下很难复现
1.2、用Windbg打开dump文件,发现崩溃在通用的框架代码中
2、进一步分析
2.1、使用IDA查看汇编代码尝试寻找崩溃的线索
2.2、在Windbg中查看相关变量的值
2.3、查看最近代码的修改记录,找到了引发问题的点
2.4、该问题中需要关注的点
3、问题总结
4、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html 有时程序可能会崩溃在通用的框架代码中,但一般不是框架代码的问题,基本都是上层业务代码有问题导致的,框架代码中可能会引用业务层管理的业务类对象,如果引用的业务类对象有问题,则可能导致框架代码出异常。今天我们就来讲一个崩溃在通用框架代码中的实例,这个问题虽然比较简单,但有很多值得思考的细节,这个实例很有价值。下面详细讲述一下该问题实例的完整分析过程,以及有哪些值得思考的点与细节。
1、问题说明
测试同事在测试新版本软件(Release版本)时,发现在执行某个常用的操作时会发生崩溃,且问题在测试PC机上是必现的,这个功能在前几天的版本中是没问题的。
1.1、Release下崩溃,Debug下很难复现
最近,我们的开发人员在这个功能点上做了一些修改,新增了一些需求点,但开发人员在Debug下没有复现这个崩溃,一直查不出具体的原因。
Debug下运行没问题,Release下运行有问题,这种现象我们之前也时不时的遇到。究其原因,可能是因为Debug下和Release下的内存管理分配机制是不一样的,Debug下会额外多分配一些内存存放调试信息。也有可能是变量未初始化引起的,Debug下会对变量自动进行初始化,比如:
* 0xcccccccc : Used by Microsoft's C++ debugging runtime library to mark uninitialised stack memory
* 0xcdcdcdcd : Used by Microsoft's C++ debugging runtime library to mark uninitialised heap memory
Release下不会对变量内存进行初始化,变量的值会是分配内存时内存中残留的随机值。这些是引发程序在Debug下和Release下运行不同表现现象的常见原因。此外,也有可能测试场景有所不同,开发同事这边Debug下的场景和测试同事那边的Release下的场景有差异,所以表现出不同的现象。
1.2、用Windbg打开dump文件,发现崩溃在通用的框架代码中
程序在发生崩溃时,程序中安装的异常捕获模块捕获到异常并自动生成了包含异常上下文信息的dump文件,测试同事让我帮忙分析一下这个问题。取来dump文件,用Windbg打开,使用.ecxr命令切换到发生异常的那个线程,然后使用kn命令查看崩溃时的函数调用堆栈,如下所示:
首先查看崩溃的那条汇编指令,汇编指令中访问了很小的内存地址,所以触发了内存访问违例,引发崩溃。像是空指针引发的,但有待于进一步确定。
然后查看崩溃时的函数调用堆栈。和以往大部分崩溃有所不同,大部分时候函数调用堆栈中显示都是与崩溃有直接关系的业务层代码的函数调用,但这次堆栈中显示的是duilib开源库中的框架代码,和具体的业务层代码没有直接的关系,所以这个问题有着一定的隐蔽性。
所以这个问题没法快速定位出来,并且我这边也有开发任务,没有多余的时间和精力去详细研究这个问题,于是只能让相关开发人员自行排查,想办法复现问题,并详细查看最近修改的代码,看看能否找到引发问题的点。
2、进一步分析
开发同事排查了,但始终找不到引发问题的代码。出问题的功能点是软件的主要功能点,一操作就崩溃,导致后续的很多功能点测试没法展开了。所以这个崩溃需要尽快解决,于是又把我拉过来,希望尽快将这个问题解决掉。
2.1、使用IDA查看汇编代码尝试寻找崩溃的线索
函数调用堆栈都是框架中的代码,没法直接找到引发崩溃的线索,于是尝试使用IDA查看汇编代码上下文,看看为什么会产生崩溃。用IDA打开函数调用的堆栈中的模块文件,将汇编代码与C++源码对照起来看:
但看下来并没有找到有效的线索。
2.2、在Windbg中查看相关变量的值
于是又在Windbg中尝试查看函数中相关变量的值,有时相关变量的值是排查问题的关键线索。查看CWindowWnd::__WndProc函数中相关变量的值,发现收到消息id为0xf,如下所示:
对应的消息为WM_PAINT消息(#define WM_PAINT 0x000f),为啥收到该消息会产生崩溃呢?
2.3、查看最近代码的修改记录,找到了引发问题的点
当前出问题的这个功能点,不是我这边开发的,对相关的细节不清楚,于是让同事看svn上的修改记录,和他一起看都修改了哪些代码。
当看到某个cpp的修改记录时,一眼看出了问题,相关代码片段如下所示:
程序在收到底层的某个消息时,会弹出一个提示框,这个提示框之前是模态的,后来应测试要求(模态框体验不好,要改成非模态的)将之改成非模态的。之前将模态框换成了非模态框,即本来是调用ShowModal显示模态框,后来改成调用ShowWindow接口显示非模态框。调用ShowModal接口时,窗口关闭时ShowModal才会返回,对应的函数中的局部变量窗口类对象CMicStateTipWnd dlg的生命周期和ShowModal接口一致,不会有问题。
但改成ShowWindow后,窗口显示出来后,ShowWindow接口立即返回,这样就退出了当前函数,这样局部变量CMicStateTipWnd dlg的内存就自动释放了,但窗口已经创建出来了,窗口需要绘制,会产生WM_PAINT消息,就会进入到CWindowWnd::__WndProc静态函数中处理,就需要调用对应的窗口类CMicStateTipWnd对象,调用窗口类CMicStateTipWnd::HandleMessage去进行窗口的绘制刷新。但窗口类CMicStateTipWnd对象是局部变量,对象已经析构,内存已经释放引用已经释放内存的对象地址,所以就产生了崩溃。
2.4、该问题中需要关注的点
这个地方有一点需要注意一下,特别是新人会容易混淆。
窗口类CMicStateTipWnd对象在函数退出时自动析构销毁,但使用该类创建的窗口是不会跟着自动销毁的!窗口类对象的销毁是业务代码管理和控制的,而窗口创建成功后是操作系统管理的,窗口的销毁需要调用DestroyWindow去销毁,当然这是业务层去控制类对象与窗口的同步的!在这个问题中虽然窗口类CMicStateTipWnd对象析构了, 但窗口还在的,窗口会产生一些消息(比如本例中的WM_PAINT消息),这些消息会走到CWindowWnd::__WndProc静态函数中进行处理,当引用到CMicStateTipWnd对象,但对象已经析构了,所以导致内存访问违例,产生了崩溃!
3、问题总结
本例中,程序崩溃在duilib界面库的框架代码中,这个通用框架用了很多年了,基本可以确定不是框架代码的问题,应该是上层业务代码的问题。在排查这类框架代码崩溃问题时,应该从上面的业务代码入手,要从业务层代码维护的窗口类对象入手,因为框架代码中会引用到业务层的类对象,如果业务层将类对象内存释放了,框架代码还访问该业务类对象,则会引发内存访问违例,引发崩溃。
虽然本例中涉及到的是UI界面框架,但项目代码中有使用了很多其他框架,这些框架代码可能也会出现类似的异常崩溃场景,所以对这个实例的排查思路及排查方法有参考意义。
总之,如果程序崩溃在通用的框架代码中,一般不是框架的问题,基本都是上层业务代码的问题,应该从业务层代码入手,从框架中引用的业务层类对象入手(引用的业务层类对象是业务层维护和管理的)。比如,程序崩溃在QT库中,一般不是QT库有问题,基本都是上层业务代码的问题。再比如,崩溃在系统库或者C/C++运行时库中,不是系统库或运行时库有问题,一般都是上层业务代码有问题。可能是传入了不合法的参数,传入了不可访问的内存地址。
4、最后
在编写代码时,要尽量考虑的全面一些,考虑尽可能多的场景,尽量将代码写的严谨缜密一点。遇到问题、排查问题时,一定要多关注细节,事后要进行积极的思考与总结。要搞清楚为什么会出问题以及问题是怎么解决的,对出问题的代码进行进一步或深入研究,要彻底搞清楚根源,这样下次再遇到类似问题代码时能快速做出反应!
对于复杂的问题,事后要主动进行复盘,要搞清楚问题的来龙去脉(为什么会出这个问题?这个问题是如何解决的?),即便问题点所在的模块不是自己负责的,也要尽量去接触去了解。事后要积极地思考和总结,可以将以往遇到的一些问题给串联起来,将相关的知识点进行归纳,比如归纳出引发问题的常见原因积极常用的排查方法。
细节出真知!要多关注细节,多提炼出一些值得关注和思考的点,甚至可以从一些看似简单的实例中找到一些有价值的点进行展开和总结。思考的多了,了解的技术细节和知识点就多了,可以总结的东西就越多,对一些编程问题与细节点理解的就更深刻了。在遇到新的问题时,就能在已取得的认知和积累的基础上做出快速反应,有更多更开阔的思路去排查问题。如果之前遇到的问题,后面又遇到了,或者遇到了类似的问题,依然是没有思路、没有头绪,不知从何查起,这就不应该了!
对相关细节和编程点了解的更透彻后,能让我们在编写新的代码时能考虑的更加全面,在最开始编码时就能想到可能存在的潜在问题。