目录
1、概述
2、dump文件的分类
2.1、dump按大小分类
2.2、查看dump文件中函数调用堆栈中变量的值
3、调用SetUnhandledExceptionFilter设置异常处理回调函数,然后调用MiniDumpWriteDump生成dump文件
4、使用Google开源库CrashRpt捕获异常,并自动生成dump文件
4.1、开源库CrashRpt捕获异常的原理及缺陷
4.2、使用微软detours技术对CrashRpt进行改进
5、通过Windows任务管理器导出目标进程的dump文件
6、从正在动态调试的Windbg中导出dump文件
6.1、有时需要将Windbg动态调试目标进程
6.2、使用.dump命令导出dump文件
6.3、将Windbg附加到目标进程上的方法
7、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931 我们在处理C++软件运行异常崩溃问题时,主要是通过事后分析dump文件去排查的。这就需要程序能感知到异常崩溃并自动生成包含异常上下文信息的dump文件。除了程序自动生成dump文件外,有时我们可能需要通过其他途径去产生dump文件。今天我们就来详细地讲讲dump文件的相关内容。
我们在处理C++软件运行异常崩溃问题时,主要是通过事后分析dump文件去排查的,能否在异常发生时自动生成dump文件就显得尤为关键了。
对于Windows系统,一般我们会在程序中安装异常捕获模块,去自动捕获软件异常,即当软件发生异常时,异常莫捕获模块能感知到,并自动生成包含异常上下文的dump文件。对于Linux系统,则会自动生成包含异常上下文的coredump文件,我们可以配置生成的文件名称。本文主要讲述Windows系统中dump文件的相关内容。
Windows程序中安装的异常捕获模块主要用来捕获发生异常崩溃的场景,有些在运行过程中发生的非崩溃异常,异常捕获模块可能感知不到,也就无法生成dump文件。比如程序发生了死循环、死锁等非崩溃的运行异常时,异常捕获模块是感知不到的,这时候如果需要导出dump文件,就需要使用其他手段了。
此外,异常捕获模块只能捕获大部分情况下的异常崩溃,有少部分场景是捕获不到的,这时候可能就需要使用Windbg(WIndows平台)或者gdb(Linux平台)调试器去动态调试了,如果调试器在运行过程中捕获到了异常,会中断下来,可以通过命令手动将dump文件导出来。
在Windows平台上,产生dump文件的方式主要有三种:
1)异常捕获模块感知到异常时自动生成dump文件;
2)通过Windows任务管理器导出dump文件;
3)用Windbg动态调试时用Windbg命令导出dump文件。
根据生成的dump文件的大小,可以将dump文件分两类,一类是很小的mini dump文件,一类是很大的全dump文件。软件的异常捕获模块在感知到异常时自动生成的dump文件,会自动保存在指定的目录中,可能会因为崩溃多次生成多个dump文件,会占用用户很多的磁盘空间,所以对于自动生成的dump文件,我们一般生成只包含部分信息的mini dump文件,mini dump文件的大小一般在几MB左右。
异常捕获模块是通过调用API函数MiniDumpWriteDump生成dump文件的,那如何去控制生成的dump文件的大小的呢?是通过设置调用MiniDumpWriteDump函数时传入的参数,去控制dump文件大小的。
从Windows任务管理器中导出的dump文件、从动态调试的Windbg中使用.dump命令导出的dump文件,一般都是包含进程完整信息的全dump文件。全dump文件则比较大,一般都有几百MB,甚至有1GB以上。因为全dump文件中保存了进程的所有内存信息,所以全dump文件的大小,接近对应进程占用的总虚拟内存的大小,所以文件会比较大。
有时我们需要去查看Windbg中线程函数调用堆栈中某个函数中局部变量或者类对象的成员变量值,去辅助分析问题,变量的值可能是关键线索,如下:
但能不能查看到目标变量的值(变量内存中的值),取决于dump文件的类型。对于mini dump文件,只能看到部分变量的值,很多变量的值是看到不到的,能否看到目标变量的值是要看运气的。而全dump文件,则是包含了进程所有内存的信息,是可以看到所有变量的值的。
通过查看目标变量的值去定位问题的方法,我们在项目中已经用过多次了,比如下面的三篇文章:
通过查看Windbg中的变量值去定位C++软件异常问题https://blog.csdn.net/chenlycly/article/details/125731044通过查看windbg中变量值去定位C++软件异常的又一典型案例分享 https://blog.csdn.net/chenlycly/article/details/125793532
通过查看Windbg中汇编指令及内存中的值去定位软件崩溃问题 https://blog.csdn.net/chenlycly/article/details/127033741
程序中可以调用系统API函数SetUnhandledExceptionFilter去设置异常处理回调函数,当发生异常时系统会回调这个函数,这样程序就能感知到,然后在回调函数中调用系统API函数MiniDumpWriteDump就可以生成dump文件了。相关代码如下所示:
// unhandled exception callback set with SetUnhandledExceptionFilter()
static LONG WINAPI SEHUnhandledExceptionFilter(EXCEPTION_POINTERS* pExInfo)
{
HANDLE hDumpFile;
hDumpFile = CreateFile(sFile, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_WRITE | FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);
MINIDUMP_EXCEPTION_INFORMATION ExpParam;
ExpParam.ThreadId = GetCurrentThreadId();
ExpParam.ExceptionPointers = pExceptionPointers;
ExpParam.ClientPointers = TRUE;
MINIDUMP_TYPE MiniDumpWithDataSegs = (MINIDUMP_TYPE)(MiniDumpNormal
| MiniDumpWithHandleData
| MiniDumpWithUnloadedModules
| MiniDumpWithIndirectlyReferencedMemory
| MiniDumpScanMemory
| MiniDumpWithProcessThreadData
| MiniDumpWithThreadInfo);
BOOL bMiniDumpSuccessful = FALSE;
bMiniDumpSuccessful = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
hDumpFile, MiniDumpWithDataSegs, &ExpParam, NULL, NULL);
return EXCEPTION_EXECUTE_HANDLER;
}
// Install structured exception handler
LPTOP_LEVEL_EXCEPTION_FILTER pOldExceptionFilter = SetUnhandledExceptionFilter( SEHUnhandledExceptionFilter );
但SetUnhandledExceptionFilter只对调用其的线程有作用,即将异常回调函数设置给当前的线程,其他线程的异常需要另外设置回调函数。软件从上层到底层一般会包含多个dll模块,模板中可能创建了线程,整个进程中会包含多个线程,没法直接给所有线程设置异常回调的,所以这种方法不够通用,需要改进。
可以使用Google的C++开源异常捕获库CrashRpt,将CrashRpt引入到项目中,作为程序的异常捕获库。
开源的CrashRpt异常捕获库是动态地将已加载的库的导入表中创建线程的API函数CreateThread HOOK成自定义的MyCreateThread函数(不管调用哪个创建线程的接口,最终都会走到CreateThread API接口中的),这样就会走进HOOK函数MyCreateThread,在该函数中就额可以调用系统API函数SetUnhandledExceptionFilter给每个创建的线程挂载异常处理回调函数了。
但CrashRpt这种处理机制是有缺陷的,没法给软件所有模块的所有线程都挂载上异常处理回调函数的,只能给在CrashRpt库之前加载的dll库挂载异常处理回调函数,在CrashRpt之后加载的库就没法去HOOK了,这样就会导致没进行HOOK操作的那些dll库中发生的异常都捕获不到了。在exe启动时,会把所有依赖的库加载到进程空间中,我们没法控制所有的库都在CrashRpt库之前被加载的,这也导致了有些模块的异常崩溃CrashRpt是捕获不到的。
后来我们针对上述缺陷,对CrashRpt库进行了改进,使用微软开源的detours项目中的代码将windows系统库中的UnhandledExceptionFilter接口给HOOK掉。因为基本所有的异常都会最终进入到该系统函数中,我们将UnhandledExceptionFilter接口HOOK成我们自定义的接口,我们就能在该自定义的接口中感知到几乎所有的异常了。感知到异常后,就可以生成包含异常上下文的dump文件了。这样就能很好的解决老版本CrashRpt不能hook后加载的库的问题,新版本的CrashRpt就可以作用于当前进程的所有模块了,基本可以捕获到进程的所有异常了。
当然改进后的CrashRpt也不是所有的异常都能捕获到,但可以捕获大概90%以上的异常。对于捕获不到的场景,可以尝试将Windbg调试器挂载到目标进程上,看看动态调试时能否感知到。对于将Windbg挂载到目标进程上进行动态调试的相关说明,下面会详细地讲到。
当程序运行弹出报错提示框或者程序卡死时,程序的进程还在的(进程还没退出),可以到Windows任务管理器中找到该进程,将包含进程上下文信息的dump文件导出来。具体的操作步骤是,打开Windows任务管理器找到目标进程,右键点击该进程,在弹出的右键菜单中点击“创建转储文件”菜单项,如下所示:
即可完成进程dump文件的导出了。
当然出现这类问题时,我们可以直接将Windbg调试器直接挂载到出问题的进程上,去直接查看进程上下文,查看线程的函数调用堆栈。但问题可能出现在客户的机器上(不能占用客户时间或者客户因为安全涉密问题没法进行远程操作),或者将Windbg挂到目标进程上后一时半会分析不出问题,这些时候就可以选择从Windows任务管理器中导出目标进程的dump文件。
这种导出dump文件的方式我们会时不时地使用到,比如前段时间帮兄弟项目组排查他们软件死锁问题时,当时的dump文件就是从任务管理器中导出的。
有少数异常崩溃,程序中安装的异常捕获模板是捕获不到的,当遇到这类问题时,可以尝试将Windbg附加到目标进程上调试运行,看看Windbg在异常发生时能否感知到异常。当Windbg感知到异常时,就会中断下来,这样我们就可以进行分析及其他操作了。
对于好复现的崩溃,这种方法处理很快;对于很难复现的问题,只能将Windbg附加到进程上和进程一起运行,直到出现异常为止(遇到这类难复现的问题时,我们都是让测试同事挂着Windbg和程序一起跑的)。
有人可能会说,如果Windbg在动态调试的过程中捕获到了异常直接分析就好了,为啥还要从Windbg导出dump文件呢?这不是多此一举吗?其实是这样的,有的问题可能没法很快分析出来,我们不能长时间占用别人的电脑,别人也有很多活要干的,那么这时就可以从Windbg中导出dump文件,事后将dump取来进行详细的分析。
从Windbg中导出dump文件的命令如下:
.dump /ma D:\20221118.dmp
其中.dump是命令名,/ma是参数,D:\20221118.dmp是存放dump文件的完整路径,Windbg中的运行效果如下:
参数/ma是导出包含所有信息的dump文件,即全dump文件。至于.dump命令还支持哪些命令,可以到Windbg的帮助文档中查看:
前段时间兄弟项目组遇到的一个软件崩溃问题就是通过Windbg动态调试(附加到目标进程上)捕获到的。当时测试同事找到了崩溃复现的方法,很容易复现,但软件中安装的异常捕获模块就是捕获不到,后来让测试同事手动将Windbg附加到目标进程后复现崩溃,Windbg捕获到了异常,手动使用命令将dump文件导了出来。
将Windbg附加到目标进程上进行动态调试有两种方式:
1)直接使用Windbg打开目标exe程序,即使用Windbg启动目标进程;
2)将Windbg附加到已经运行的进程上。
具体使用哪种方式,要看具体的场景。如果问题出在程序启动的过程中,则需要选择使用Windbg启动程序;如果是运行过程中出现的问题,则两种方式都可以使用。
本文结合多年的项目实战经验,详细讲述了dump文件的分类与dump文件生成方法,希望这些内容能对相关的朋友们提供一些借鉴和参考。