目录
1、概述
2、使用Windbg静态分析dump文件
2.1、异常捕获模块自动生成dump文件
2.2、从Windows任务管理器中导出dump文件
2.3、从正在动态调试的Windbg中使用命令导出dump文件
2.4、使用Windbg静态分析dump文件的一般步骤
3、使用Windbg动态调试目标进程
3.1、程序发生异常,但异常捕获模块没有捕获到
3.2、异常捕获模块感知到了异常,但导出dump文件时产生了二次崩溃
3.3、程序并没有发生C++异常,只是发生卡死或死循环,异常捕获模块感知不到
3.4、程序运行过程中检测到不正常,调用abort函数强制结束进程,导致程序闪退
3.5、Visual Studio调试程序时产生异常,但看不到有效的函数调用堆栈,可以尝试使用Windbg进行动态调试
3.6、程序启动崩溃或失败时
3.7、程序弹出报错提示框时
3.8、使用Windbg动态调试目标进程的一般步骤
4、其他场景说明
5、在实际排查问题时,可能要尝试多种排查方法
6、最后
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 使用Windbg分析C++软件问题时,可以静态分析,也可以动态调试。经常有朋友问,到底什么时候使用Windbg静态分析?什么时候Windbg进行动态调试?下面根据近几年来排查C++软件异常遇到的问题场景和项目实例,给大家做个详细的总结。
1、概述
一般情况下,有dump文件时,使用Windbg静态分析dump文件即可;没有dump文件时,可能就需要使用Windbg去动态调试目标进程了。如果dump文件中的信息不足以分析出问题时,也可以尝试使用Windbg去动态调试。
此外,如果使用Visual Studio调试代码时遇到异常,但在Visual Studio中分析不出来问题,比如刚启动调试时就产生异常或者报错,可以使用Windbg动态调试进行分析。
2、使用Windbg静态分析dump文件
Windbg静态分析的对象就是dump文件,必须要有dump文件。一般生成dump文件的方式主要有三种,分别是异常捕获模块生成、从Windows任务管理器中导出、从正在调试的Windbg中使用命令导出。
2.1、异常捕获模块自动生成dump文件
为了让软件能自动捕获运行时的异常,一般会在软件中安装异常捕获模块。当软件发生异常时,异常捕获模块感知到异常,自动将包含异常上下文信息导出到dump文件中。有的软件甚至会弹出崩溃反馈提示框,比如如下所示的企业微信的提示框:
提示用户上传log日志和dump文件。为什么不自动上传呢?因为要上传用户机器上的文件,可能会涉及到用户隐私和用户信息安全,不能随意上传,必须经过用户授权后上传或者让用户上传。
异常捕获库可以选用Google开源库CrashRpt,或者使用Chrome开源代码中的Breakpad或者Crashpad,对于QT程序则可以选用qBreakpad。程序发生异常时,异常捕获模块感知到,会将异常上下文信息导出到dump文件中。事后可以取来dump文件,使用Windbg进行静态分析了。
异常捕获模块一般是调用系统API函数miniDumpWriteDump去生成dump文件,可以控制传给该API函数的参数去控制生成的dump文件的大小。此时一般生成的是比较小的mini dump文件,因为dump文件是保存在用户电脑上,不能生成全dump文件,因为全dump文件比较大(包含了所有内存信息,文件大小可能接近进程的总虚拟内存),会占用用户大量的磁盘空间。此外,dump文件可能要上传服务器,文件过大会给上传带来较大的网络负担,所以dump文件也不能太大。一般分析mini dump文件就能定位问题了。
2.2、从Windows任务管理器中导出dump文件
如果程序中的线程出现卡死(可能是死循环或死锁引发的)或者程序运行弹出报错提示框,这些情况下程序的进程还在,可以直接到Windows任务管理器中手动导出dump文件。事后取来dump文件进行分析。
从Windows任务管理器中导出dump文件的操作步骤是这样的。打开Windows任务管理器,找到出问题的目标进程,右键点击该进程,在弹出的右键菜单中点击“创建转储文件”菜单项:
即可导出包含进程上下文信息的到dump文件中。
其实程序出现卡死或者运行报错时,还为时不晚的,可以直接将Windbg附加到进程上进行分析的。但有些客户比较敏感(比如安全级别比较高),不允许远程到机器上将Windbg附加上去,甚至都连Windbg都不让安装,这是就可以让客户手动从Windows任务管理器的进程列表中导出。这种方式导出的dump文件是全dump文件,文件会比较大,包含了进程的所有内存信息。
2.3、从正在动态调试的Windbg中使用命令导出dump文件
可能正在使用Windbg调试目标进程,但一时半会又分析不出问题,不能长时间占用出问题的电脑,电脑主人还需要用。我们可以使用Windbg命令.dump导出当前正在调试的进程的dump文件,具体的命令样式可以这样:
.dump /ma D:\0606.dmp
事后取来dump文件,然后使用Windbg静态分析这个dump文件,继续分析问题。一般此时导出的也是全dump文件。
2.4、使用Windbg静态分析dump文件的一般步骤
取来dump文件,使用Windbg打开,首先输入.excr命令切换到发生异常的上下文,然后使用kn命令查看发生异常时的函数调用堆栈,然后将函数调用堆栈与源码对应起来分析。一般此时需要根据函数调用堆栈中的模块,去找这些模块的pdb文件,然后再去查看函数调用堆栈,有了pdb文件中的函数及变量符号后,函数调用堆栈中可以看到具体的函数名称和代码行号:
甚至可以直接在Windbg中查看函数中局部变量的值或者函数所在类对象的成员变量的值。
关于Windbg静态分析dump文件的详细步骤及相关要点,可以参见我之前写的文章:
使用Windbg静态分析dump文件的一般步骤及要点详解https://blog.csdn.net/chenlycly/article/details/130873143
3、使用Windbg动态调试目标进程
如果没有生成dump文件,则没法使用Windbg进行静态分析,这时就需要使用Windbg进行动态调试了,即将Windbg动态附加到目标进程上进行调试。动态调试有两种方式:
1)直接使用Windbg启动目标程序;
2)先将目标程序启动起来,然后将Windbg附加到目标进程上。
当目标程序发生异常,Windbg就会感知到,会自动中断下来,这样使用Windbg命令就能查看当前发生的异常类型并查看此时的函数调用堆栈了。
没有生成dump文件,有几种场景,这些场景需要使用Windbg的动态调试了,下面来详细介绍一下。
3.1、程序发生异常,但异常捕获模块没有捕获到
程序中安装的异常捕获模块,并不能感知并捕获所有场景下的异常,即还是有少部分异常是捕获不到的。捕获不到的,自然就不会生成dump文件了。我们在项目中也时常会遇到,程序在使用过程中发生闪退或崩溃,异常捕获模块没捕获到,没有生成dump文件。一般遇到这类情况,都会让测试人员将Windbg附加到目标进程上,和目标进程一起跑,一旦目标进程运行过程中发生异常,Windbg会第一时间感知到并中断下来,此时就可以查看发生的异常类型和函数调用堆栈了。
3.2、异常捕获模块感知到了异常,但导出dump文件时产生了二次崩溃
程序发生了异常,异常捕获模块感知到了,但在导出dump文件的过程中产生了二次崩溃,可能没生成dump文件,或者生成的dump文件大小为0,这种情况下也需要使用Windbg进行动态调试的。这个场景我们在项目中也遇到过多次。
3.3、程序并没有发生C++异常,只是发生卡死或死循环,异常捕获模块感知不到
程序并没有发生C++异常,只是业务逻辑上产生异常(比如不能正常处理业务了),比如发生多线程死锁或死循环。因为并没有产生C++异常,异常捕获模块是感知不到的,所以也就无法生成dump文件。这个也需要将Windbg附加到进程上进行动态调试,进行详细分析的。
3.4、程序运行过程中检测到不正常,调用abort函数强制结束进程,导致程序闪退
如果程序在运行过程中监测到不正常的情况,直接调用abort函数强制将进程终止了,这样给人一种程序发生崩溃闪退的感觉。这种情况也是没有发生C++异常,异常捕获模块也是感知不到的,所以也不会产生dump文件。比如在开源jsoncpp库中,如果解析的json节点的类型不匹配,会被jsoncpp内部检测到,会触发abort函数的调用,相关代码截图如下:
再比如WebRTC开源库中在监测到malloc动态申请内存失败后,也会触发abort函数的调用,强行终止进程。估计是因为动态申请内存失败了,相关业务无法正常展开了,程序活着也没意思了,所以就强行终止进程了。相关代码截图如下:
上述两类问题,我们在实际项目中都遇到过。遇到这类情况,也可以将Widnbg附加到目标进程上进行动态调试,尝试着去复现问题。如果程序调用abort函数,就会触发Windbg中断下来,然后查看函数调用堆栈,通过函数调用堆栈就知道是什么函数调用触发的abort函数的调用的。那为什么调用abort函数会让正在调试的调试器Windbg中断下来呢?因为abort函数内部会raise(产生)一个SIGABRT信号终止异常,如果当前正在调试状态,会让调试器中断下来。abort函数的内部实现源码如下所示:
/***
*void abort() - abort the current program by raising SIGABRT
*
*Purpose:
* print out an abort message and raise the SIGABRT signal. If the user
* hasn't defined an abort handler routine, terminate the program
* with exit status of 3 without cleaning up.
*
* Multi-thread version does not raise SIGABRT -- this isn't supported
* under multi-thread.
*******************************************************************************/
void __cdecl abort (
void
)
{
_PHNDLR sigabrt_act = SIG_DFL;
#ifdef _DEBUG
if (__abort_behavior & _WRITE_ABORT_MSG)
{
/* write the abort message */
_NMSG_WRITE(_RT_ABORT);
}
#endif /* _DEBUG */
/* Check if the user installed a handler for SIGABRT.
* We need to read the user handler atomically in the case
* another thread is aborting while we change the signal
* handler.
*/
sigabrt_act = __get_sigabrt();
if (sigabrt_act != SIG_DFL)
{
raise(SIGABRT);
}
/* If there is no user handler for SIGABRT or if the user
* handler returns, then exit from the program anyway
*/
if (__abort_behavior & _CALL_REPORTFAULT)
{
_call_reportfault(_CRT_DEBUGGER_ABORT, STATUS_FATAL_APP_EXIT, EXCEPTION_NONCONTINUABLE);
}
/* If we don't want to call ReportFault, then we call _exit(3), which is the
* same as invoking the default handler for SIGABRT
*/
_exit(3);
}
关于调用abort函数强行终止进程场景的详细说明,可以参见我之前写的文章:
C++程序中执行abort等操作导致没有生成dump文件的问题案例分析https://blog.csdn.net/chenlycly/article/details/129003869
3.5、Visual Studio调试程序时产生异常,但看不到有效的函数调用堆栈,可以尝试使用Windbg进行动态调试
如果使用Visual Studio调试代码的过程中产生了异常,但在Visual Studio中看不到完整的或者有效的函数调用堆栈,分析不出来问题,比如刚启动调试时就产生异常或者报错,可以尝试着使用Windbg动态调试进行分析。
3.6、程序启动崩溃或失败时
当程序启动崩溃闪退,会因为某些原因启动失败时,可以尝试使用Windbg启动程序进行动态调试。可能Debug下不好复现,也可能即使复现用Visual Studio调试也很难排查。有可能问题只在客户机器上才会出现,没法到客户机器上使用Visual Studio调试代码。这些场景下的问题,使用Windbg去动态调试分析很方便。
3.7、程序弹出报错提示框时
程序在运行过程中出现异常,弹出报错提示框,此时进程还在,此刻将Windbg附加到进程上还来的及,可以直接分析。可能问题很难复现,错过了就很难复现了,所以要及时地将Windbg附加到进程上分析。即使一时半会分析不出原因,也可以将进程上下文导出到dump文件中,事后再去分析。
3.8、使用Windbg动态调试目标进程的一般步骤
将Windbg附加到目标进程上,一旦目标进程产生异常,Windbg就会感知到并中断下来,此时使用kn命令查看发生异常时的函数调用堆栈,然后将函数调用堆栈与源码对应起来分析。一般此时需要根据函数调用堆栈中的模块,去找这些模块的pdb文件,然后再去查看函数调用堆栈,有了pdb文件中的函数及变量符号后,函数调用堆栈中可以看到具体的函数名称和代码行号,甚至可以直接在Windbg中查看函数中局部变量的值或者函数所在类对象的成员变量的值。
关于Windbg动态调试目标进程的详细步骤及相关要点,可以参见我之前写的文章:
使用Windbg动态调试目标进程的一般步骤及要点详解https://blog.csdn.net/chenlycly/article/details/131029795
4、其他场景说明
产品发布后,问题是出在Release版本下的。如果使用Windbg静态分析dump文件分析不出来问题时,可以尝试使用Windbg动态调试的。如果使用Windbg分析不出来问题,并且问题是好复现的,则可以尝试使用Visual Studio去调试代码,进行Release下调试。如果问题出在底层的dll模块,则可以使用Visual Studio附加到进程调试的方式去单独调试这个dll模块,附加到进程调试还是很方便的。
此外,我们有时甚至需要在Visual Studio中查看并单步调试汇编代码。CPU中执行的是一句一句汇编代码(或者叫二进制机器码,两者是等价的),看汇编代码才能看到代码的执行细节。一行C++源码可能对应多行汇编代码,有时我们要知道C++源码为何有问题,可以尝试在Visual Studio中转到汇编代码页面,去单步调试汇编代码,通过调试汇编代码去寻找真相。
5、在实际排查问题时,可能要尝试多种排查方法
在排查问题的过程中,可能需要使用多种排查方法,一个方法排查不出来,就换另一种方法;或者将两个或两个以上的方法结合起来使用,直到定位问题为止!此外,除了使用调试器,还可以考虑同时使用其他的软件分析工具去辅助定位,比如Process Explorer或Process Monitor等。
之前使用多种方法去排查一个线程栈溢出的问题,相关问题排查实例记录文章如下:
线程栈溢出异常,程序崩溃在汇编代码test dword ptr [eax],eax上的问题排查https://blog.csdn.net/chenlycly/article/details/131743305在上述问题中先后使用了多种排查方法,从Windbg静态分析dump文件,到Windbg动态调试目标进程,再到使用Visual Studio调试源码,最后去单行调试汇编代码。最终调试汇编代码,才找出问题的原因。
6、最后
本文根据近几年来排查C++软件异常遇到的问题场景和实例,给大家做个详细的总结,希望能给大家提供一个详细的借鉴或参考。此外,分析问题的方法是多样的,不是一成不变的,要根据具体情况去灵活选用一种或多种方法去排查。需要通过大量的实践去积累经验的,经验多了,处理问题的思路就多了,处理问题就更加得心应手了!