Windows下c++程序崩溃问题定位
Windows下c++程序崩溃问题定位主要依赖于代码编译过程中生成的调试信息文件,比如.map和.pdb。
本文以前几天项目中出现的崩溃问题当做具体案例来分析。具体原因是配置了某个数据文件A,由于A文件是在线程T1中去加载的,T2线程中去读取缓存内容,这两个线程同时运行,没有进行异步控制,导致转代码线程先执行时,取缓存为空,同时有个函数逻辑错误,导致memcpy的目的地址为空,且对空指针进行访问了,程序崩溃。
本文首先通过Windbg工具结合pdb文件来分析崩溃的原因,然后自己制造一个崩溃来验证WinDbg的调试结果,最后再讲解如何通过map文件以及系统崩溃日志计算崩溃偏移地址直接定位代码行数。
一,WinDbg调试
在主程序中加入了dump文件生成逻辑后,还原当时现场场景,程序崩溃,在HqIssue同级目录的Dump文件夹下生成了20181226_093530.dmp文件。在自己的D盘上新建一个文件夹dump_test,将20181226_093530.dmp,A.pdb,B.pdb,B.exe,A.map放于文件夹内(其他pdb也可以加入进来,在你不确定到底崩溃在程序哪个插件中的时候;B.exe取决于如果程序崩溃还可以在windbg中继续调试的时候,可以加进来;A.map文件保存了地址信息)。
打开WinDbg工具,我们首先需要配置Symbol path,Source path以及Image path
1) Symbol path
这是pdb文件路径,包括当前dump调试对应的工程pdb文件,以及系统api对应的pdb文件。前者在上面第一点中的dump_test文件夹中,后者需要配置
SRVD:\mysymbolhttp://msdl.microsoft.com/download/symbols
其中D:\mysymbol为下载系统pdb文件存放在本机的路径。
在view-commond中输入 .reload命令,加载pdb符号
2) Source path
这是代码路径,此代码需要和pdb文件版本保持一致。
3) Image path
Exe程序路径,一般用于死循环时,在WinDbg中继续调试。
拖拽20181226_093530.dmp文件到WinDbg界面中,然后打开Debug-Modules查看我们需要的模块是否已经全部加载上来了(至少要保证系统pdb和程序中重要的pdb被加载进来)。
输入!analyze –v命令,这个命令是最简单也是最实用的的调试命令,命令执行需要一些时间,稍等一下,出现堆栈结果,在我的WinDbg上调试结果为:
由于源码,pdb完全匹配,所以可以直接在FAULTING_SOURCE_CODE中显示错误的代码。memcpy拷贝出错,刚好错误也就是上面我们说的pData == NULL,并且pNode->m_pFieldPoint == NULL。
如果没有下面的FAULTING_SOURCE_CODE,只有STACK_TEXT呢,这种情况又分为两种:
1) STACK_TEXT的堆栈信息后面带有代码行数的,我们可以直接定位到行数
2) STACK_TEXT的堆栈信息后面什么都没有带,只有一个偏移地址。
下面我们着重对第2)中情况进行分析
从上图中我们可以得出两个信息:
1)0ae7c1ec 8a4810 mov cl,byte ptr [eax+10h]
程序崩溃的地址为0x0ae7c1ec
2)程序崩溃在hq_shanghai_binary 组件的CCheckDBF::BCLXTimeCall中。
我们首先找到hq_shanghai_binary.dll加载到主程序中后在主程序中的地址空间,输入命令lm
可以看到hq_shanghai_binary.dll的地址基址为0x0ae70000,用0x0ae7c1ec – 0x0ae70000得到崩溃处在dll中的虚拟地址为:0xc1ec。但是对于dll或者exe来说,它们的地址空间都是有一个最佳装载地址的,也就是说它们存储数据的地方并不是从0x0ae70000基址开始,会加上一个偏移量再开始存储数据,所以我们需要找到这个最佳装载基址,公式为:
虚拟地址 = pe头文件大小 + 最佳装载地址 +相对虚拟地址
打开hq_shanghai_binary.map文件,可以在文件头部看到:
Pe头文件大小为0x10000000,然后再在当前map中找任何一个函数地址进行计算出当前dll的最佳装载地址,此处推荐全局搜索main(不管对于exe还是dll,都会存在main/dllmain)来进行计算最佳装载地址。
1004ff19 = 10000000 + 最佳装载地址 + 0004ef19 -》最佳装载地址 = 0x1000
所以崩溃处的相对偏移地址为:0xc1ec – 0x1000 = 0xb1ec
由于崩溃处在CheckDbf.cpp文件中,所以在hq_shanghai_binary.map中搜索CheckDbf.cpp
在这个段中查找与0xb1ec最接近的地址,找到为:
0xb1ec刚好在上图标红的两个地址之间,前面的2290,2291就是行号,那么就说明问题出错的地方在CheckDBF.cpp文件的2290行。
上面的地址偏移计算过程可以解决我们两个疑惑:
1) WinDbg为什么能够精准定位到程序崩溃处:因为pdb文件中是包含了文件名和代码行号信息,以及各个变量,函数的地址信息,当根据偏移地址找到行号后,再通过我们配置的Source path,直接将代码展示在FAULTING_SOURCE_CODE区域。
2) 通过完全匹配的map文件我们也可以直接定位到程序崩溃处:当我们的应用程序内部没有捕捉程序崩溃的机制时,windows默认会有一个崩溃捕捉处理,也即是我们常见的windows报错框,根据崩溃的模块地址,以及崩溃处地址,也是通过同样的地址计算,来定位代码行数,下面会介绍到。
二,地址偏移计算验证
现在我们来创造一个崩溃验证上面分析的根据地址去找寻崩溃错误行数的方法,在HqShanghaiBinary.cpp文件的TimeCall函数入口处加入两行代码,制造崩溃,编译代码,生成最新的pdb文件和map文件,然后运行程序,崩溃,在dump文件夹下找到dump文件。
具体打开WinDbg分析dump的步骤不再描述,可以参照上面。输入!analyze – v后如图所示:
当然,它这里已经告诉我们行数了,718行,刚好是我们崩溃开始的地方。然后我们再看崩溃发生的地址为:0x0b01610e。
0b01610e 8908 mov dword ptr [eax],ecx
输入lm命令,查看当前dll在主程序中的基址,为0x0afe0000
计算出:崩溃处偏移地址 + dll中最佳装载地址 = 0x0b01610e - 0x0afe0000 = 0x3610e。
再打开hq_shanghai_binary.map文件,以dllmain函数的地址为例求出dll最佳装载地址为:
dll最佳装载地址 = 0x1004e049 – 0x0004d049 – 10000000 = 0x1000
所以崩溃处偏移地址 = 0x3610e – 0x1000 = 0x3510e
然后再到hq_shanghai_binary.map中找到崩溃所在文件HqShanghaiBinary.cpp区域行号和偏移地址的映射关系:
崩溃偏移地址刚好在0x00035100 至 0x0003511c中间,所以崩溃代码所在行数为718-724之间,再回头看我们设置崩溃的地方,也刚好是这个区域,完美定位!
三,通过map文件定位程序崩溃代码行数
还是以(二)中的崩溃为例,现在我们屏蔽掉代码中的崩溃捕捉,不生成.pdb文件(如果程序自己捕捉了崩溃,操作系统就不会进行默认的崩溃处理)运行程序,程序崩溃,出现下图所示信息:
打开WER7FDF.tmp.WERInternalMetadata.xml文件,我们主要看第二个xml节点:
这里面包含了崩溃的地址信息,Parameter7就是Exception code,崩溃处地址,且是在崩溃模块中的偏移地址。
崩溃处偏移地址 + dll中最佳装载地址 = 0x3610e。
在此不再重述最佳装载地址的计算,参考(二)中计算可得最佳装载地址 = 0x1000,所以崩溃处偏移地址 = 0x3510e,到map文件中查找对应行数在718-724之间。结果和WinDbg定位出来的崩溃代码行数是一样的。
至此,碰到绝大部分的崩溃性问题,都可以通过使用WinDbg来分析dump帮助我们定位代码崩溃处,前提是pdb和dump文件匹配。
附problem signatures节点中前面几个属性的顺序和含义:
Problem Event Name: APPCRASH //问题事件名称
Application Name: acad.exe //应用名称
Application Version: 24.0.55.0 //应用版本号
Application Timestamp: 498ff0e7 //应用时间戳
Fault Module Name: StackHash_bade //故障模块名称
Fault Module Version: 6.1.7600.16385 //故障模块版本号
Fault Module Timestamp: 4a5be02b //故障模块时间戳
Exception Code: c0000374 //异常代码
Exception Offset: 00000000000c6cd2 //异常偏移地址
from:https://blog.csdn.net/bajianxiaofendui/article/details/85303089