目录
1、dump文件
1.1、dump文件的生成方式
1.2、dump文件的大小
2、pdb符号文件
2.1、pdb文件的路径设置
2.2、pdb文件的时间戳与名称问题
2.3、如何确定要找哪些pdb文件?
3、使用Windbg静态分析dump文件以及动态调试程序的一般步骤
4、确定发生异常或崩溃的业务模块,到业务模块的函数中去排查
5、在分析从任务管理器中导出的dump文件时可能需要使用.effmach命令切换一下上下文
5.1、操作系统位数与程序位数
5.2、使用.effmach命令将线程上下文切换到32位
6、Windbg动态调试目标进程时遇到因为调用了IsBadWritePtr或者IsBadReadPtr引发的异常
7、什么时候使用Windbg静态分析?什么时候使用Windbg动态调试?
7.1、程序发生死循环或死锁问题时
7.2、程序发生异常,但异常捕获模块没有捕获到
7.3、异常捕获模块感知到了异常,但导出dump文件时产生了二次崩溃,dump文件生成失败
7.4、程序运行过程中检测到不正常,直接调用abort函数强制结束进程,导致程序闪退
7.5、用IDE调试程序时产生异常,但看不到有效的函数调用堆栈,可以尝试使用Windbg进行动态调试
7.6、程序启动崩溃或失败时
7.7、程序弹出报错提示框时
8、有时可能需要使用反汇编工具IDA查看汇编代码上下文去辅助定位问题
9、熟悉Windbg命令
10、最后
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.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 Windbg是微软提供的Windows平台上强大调试器,既可以静态分析dump文件,也可以动态调试目标程序,是排查Windows平台软件异常的利器,很好用的问题分析工具。本文根据多年使用Windbg遇到的坑与经验心得,给大家完整地总结一下使用Windbg的诸多细节与技巧,以供大家借鉴和参考。
1、dump文件
dump文件一般是软件发生异常时导出的包含程序进程异常上下文信息的文件,可以在dump文件中查看到发生异常的那条汇编指令及异常发生时的各个寄存器的值,也可以查看各个线程信息以及线程的函数调用堆栈,甚至可以查看到程序进程内存中相关变量的值。
dump文件是分析软件异常的重要依据和来源,大部分情况下的软件异常都是事后使用Windbg静态分析dump文件去排查的。
1.1、dump文件的生成方式
那dump文件是如何生成的呢?一般都是调用系统API函数MiniDumpWriteDump生成的。生成dump的方式主要有三种:
1)通过程序中安装的异常捕获模块去自动生成dump文件。这是生成dump文件最常见、最高效的方式,很多软件都会内置异常捕获模块,当程序发生异常时异常捕获模块能自动感知到,并自动生成dump文件。
那异常捕获模块该如何实现呢?一般不用我们自己去实现,可以选择开源的CrashRpt、CrashPad和BreakPad,稍微改造一下,就可以集成到我们的软件中。
2)从任务管理器中导出dump文件。当程序发生死循环、死锁等卡死问题时,或者程序弹出报错提示框时,程序进程还在的(这点和异常崩溃闪退的场景不同,发生崩溃后,程序进程就不在了),此时可以选择打开系统任务管理器,在进程列表中找到目标进程,然后右键点击之,在弹出的右键菜单中点击“创建转储文件”:即可导出dump文件。
3)从正在调试的Windbg中使用.dump命令导出dump文件。在程序中安装异常捕获模块去捕获异常,只能感知到大部分异常,还是有少部分场景捕获不到,或者在导出dump时产生了二次崩溃,所以也就没有生成dump文件。此时就需要使用Windbg的动态调试了。把Windbg附加到目标进程上,和目标进程一起跑,一旦程序发生异常,调试器Windbg会第一时间感知到,并中断下来,然后就可以进行分析了。如果一时半会分析不出来问题,可以使用.dump命令,将进程上下文信息导出到dump文件中。
1.2、dump文件的大小
dump文件一般分小的mini dump文件和大的全dump文件。可以通过设置生成dump文件的API函数MiniDumpWriteDump参数去控制dump文件的大小。一般通过异常捕获模块自动生成的dump文件,可能会很频繁,这些文件是保存在用户电脑磁盘上,此外可能还要考虑到自动上传到运维服务器上,所以dump文件不宜太大,一般在几百KB到几MB左右。比如可以给MiniDumpWriteDump函数设置如下的参数,这样生成的dump文件最多大概几MB左右:
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 = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
hDumpFile, MiniDumpWithDataSegs, &ExpParam, NULL, NULL);
从任务管理器中导出的以及从动态调试的Windbg中导出的,都是全dump文件,包含了进程的所有内存信息,dump文件的大小比较接近程序进程占用的虚拟内存大小。程序占用的虚拟内存(用户态的虚拟内存),在Windows任务管理器中看不到,可以使用Process Explorer工具查看,这点我们已经讲过多次了,特别是在排查内存泄漏问题时会用到。
有时我们在用Windbg分析dump文件时,除了要查看崩溃时的函数调用堆栈,可能还要查看堆栈中的函数中的变量值,比如函数中局部变量的值、函数所在类的成员变量的值,这些值可能是排查问题的关键线索,我们在项目中多次遇到了!
要查看变量的值,得有变量的符号,所以要加载相关模块的pdb文件,pdb文件中包含了函数、变量等符号信息。mini dump文件比较小,只包含了部分变量的内存信息,能不能查看到想查看的变量的值,需要看运气的。而全dump文件包含了进程所有的内存信息,可以查看所有变量的值。可以在展开调用堆栈中的函数时查看变量的值,也可以使用x命令去搜索变量,比如查看某个模块中的某个全局变量的值
x mediasdk!g_pMainLogic
其中,mediasdk是不带后缀名的模块名,g_pMainLogic是该模块中的全部变量的名称(符号)。
关于dump文件的详细说明,也可以参看我之前写的文章:
dump文件类型与dump文件生成方法详解https://blog.csdn.net/chenlycly/article/details/127991002
2、pdb符号文件
PDB-Program Databse File,程序数据库文件,存放了二进制文件中所有函数及变量的符号,还有一些调试用的信息,要查看完整的函数调用信息及变量信息,都需要用到pdb文件。pdb文件是在编译工程时产生的,它是和对应的模块(exe或dll)一起生成出来的。在Visual Studio中,不管是Debug还是Release下默认都会生成pdb文件,相关的配置选项如下:
此外,本地程序之所以能调试,是因为其二进制文件中在本地编译生成时会自动写入对应的pdb文件的绝对路径,而pdb文件中存放着用于调试的各种调试信息,启动调试时调试器会根据文件中记录的pdb文件路径去加载pdb文件,去获取pdb文件中的调试信息。如果手动将pdb删除,在没有重新生成的情况下,肯定是无法调试的。
要在Windbg中查看函数调用堆栈中的具体函数名及代码的行号,以及查看变量的值,都需要使用到pdb符号文件。为了方便日后排查问题,不同时间点、不同版本的pdb文件要统一维护起来,比如我们的自动化编译系统每天会去自动版本,然后将各个模块编译生成的pdb文件都拷贝到文件服务器上维护起来,如下所示:(不同时间点的文件放置在不同的文件夹中,以方便后期查找)
加载pdb符号文件后,不仅能在函数调用堆栈中看到具体的函数名和行号,甚至有时还会显示多几行的函数调用(在实际项目中遇到过),这对于问题的分析很有好处。
2.1、pdb文件的路径设置
要让Windbg去加载pdb文件,则需要将pdb文件的路径设置给Windbg。在使用Windbg分析问题时,会涉及到两类pdb文件,一类是我们写的业务模块的pdb文件,一类是操作系统的系统库pdb文件。对于我们的业务模块的pdb文件,需要根据对应模块的时间戳到保存pdb文件的地方去找;对于系统库的pdb文件,可以直接在Windbg中设置微软系统库pdb在线服务器下载地址,Windbg会根据需要去服务器上自动下载pdb文件的。
点击Windbg菜单栏中的File -> Symbol File Path ...,打开设置pdb文件路径的窗口,如下所示:
一般我们设置如下格式的pdb组合文件路径
C:\Users\Administrator\Desktop\pdbdir; srv*f:\mss0616*http://msdl.microsoft.com/download/symbols
这么个一长串组合路径主要由下面两个路径构成:(路径之间使用分号隔开)
1)应用程序库的pdb文件路径(非系统库):
C:\Users\Administrator\Desktop\pdbdir。我们开发的业务模块的pdb文件,集中拷贝到该路径中,路径名称可以随意设置。
2)Windows系统库的pdb文件在线下载路径:
srv*f:\mss0616*http://msdl.microsoft.com/download/symbols,其中http://msdl.microsoft.com/download/symbols,是微软提供的在线系统pdb文件下载服务器(放置在公网上,大家都可以访问)。如果设置了该地址,Windbg会自动连接该服务器,去自动下载与当前dump文件中用到的系统库版本一致的pdb文件。另外,f:\mss0616路径是从微软pdb文件服务器上下载下来的pdb文件在本地机器上的存放路径。
这里需要注意一下,有时微软这个服务器会有连接不上或卡顿的情况,会直接导致Windbg卡顿。所以遇到Windbg比较安顿的时候,可以先将该地址删除掉。但有时我们需要设置该在线地址,因为有时我们想去看到底是调用了系统库中的哪个接口触发的崩溃。
为啥要加载系统库的pdb文件呢?是为了在函数调用堆栈中看到系统模块中的具体函数名称,甚至加载系统pdb文件后可以看到多一两行的函数调用,有时系统模块的具体函数调用可能是比较关键的线索,我们在项目中多次使用到。
对于dll模块,其导出函数的符号对外是公开的,可识别的,dll模块内部函数对外是不可见的,要在函数调用堆栈中看到具体的函数名,则需要加载pdb文件,pdb文件中包含了所有的函数符号。
2.2、pdb文件的时间戳与名称问题
加载pdb文件是严格校验时间戳(文件的生成时间)的,即使两次编译的代码没有修改,两次编译出来的pdb文件都不能交叉使用的。Windbg在加载pdb文件时,会严格校验pdb文件的时间戳是否和对应模块一致的,如果不一致,会加载失败。
此外,pdb文件的名称不能随意改动,pdb文件名称必须和Visual Studio工程的名称一致,否则也会加载失败,我们在实际项目中遇到过。比如工程名为videocodec_hp.vcxproj,手动将生成的pdb文件的hp后缀去掉,即videocodec.pdb,会加载失败,解决办法很简单,将pdb名称改成与工程名称一样的就可以了。
2.3、如何确定要找哪些pdb文件?
其实很简单,只要根据函数调用堆栈中显示的模块名去找就可以了。但一般不需要去找堆栈中所有模块的pdb文件,一般只需要在堆栈中找离崩溃点最新的一两个模块就可以了。当然,你想看整个函数调用堆栈中的函数名称,则去找堆栈中所有模块的pdb文件。
要找函数调用堆栈中模块的pdb文件,先用lm命令查看模块的时间戳(生成时间),比如:
上述命令中的vm是lm命令的参数,具体含义可以到Windbg的帮助文档中查看lm命令的说明,此处就不再赘述了。通过二进制文件的生成时间,到文件服务器上去查找对应时间点的pdb文件(事先已经将各个时间点和版本的pdb文件统一放在服务器维护了),然后将pdb拷贝到本地电脑上,然后将路径设置到Windbg中即可。
注意,设置路径时要勾选reload选项:
这样Windbg会去自动加载pdb文件。有时,可能Windbg会自动加载失败,我们需要使用.reload命令去强制加载,比如:
.reload /f mediasdk.dll
参数/f表示强制加载,让Windbg到设置的pdb文件路径中去强制加载pdb。注意,该命令中的模块名,必须带后缀名,且不能带完整的路径。如果pdb文件时间戳不对,即使强制加载也会加载不成功,因为加载时会严格校验pdb文件与二进制文件的时间戳。使用.reload命令去强制加载,只是解决Windbg自动加载pdb失败的问题。
如何确定pdb文件有没有加载成功呢?其实很简单,只需要重新执行一下lm命令,如果加载pdb文件加载成功,会将pdb文件的路径显示出来,如下所示:
如果pdb文件没加载成功,则不会显示pdb文件的路径,即没看到pdb文件的路径,则表示该模块没有加载pdb文件。
3、使用Windbg静态分析dump文件以及动态调试程序的一般步骤
使用Windbg分析软件异常主要有两种方式,一种是使用Windbg静态分析dump文件,一种是将Windbg附加到目标进程上进行动态调试。初学使用Windbg分析软件异常问题时,一般需要了解Windbg静态分析dump文件的一般步骤、了解使用Windbg动态调试目标进程的一般步骤,这对于尝试使用Windbg分析问题的初学者很重要。我之前已经写了两篇文章详细介绍这方面内容,在此就不再赘述了。
关于使用Windbg静态分析dump文件的一般步骤,可以参看我的文章:
使用Windbg静态分析dump文件的一般步骤及要点详解https://blog.csdn.net/chenlycly/article/details/130873143 关于使用Windbg动态调试目标进程的一般步骤,可以参看我的文章:
使用Windbg动态调试目标进程的一般步骤及要点详解https://blog.csdn.net/chenlycly/article/details/131029795
4、确定发生异常或崩溃的业务模块,到业务模块的函数中去排查
根据发生异常的那条汇编指令及当时的函数调用堆栈,可以确定发生异常的模块。如果异常发生在我们的业务模块中,则直接到业务模块中去排查。比如下图:
崩溃在业务模块mediasdk.dll的函数中,直接到业务模块mediasdk中去排查。
如果异常发生在操作系统的系统库,一般系统库是不会有问题的,一般都是上面的业务库在调用系统库接口时有问题,比如传入了异常的内存地址、传入了无效的参数等。此时需要沿着函数调用堆栈网上看,还是要到上面的业务模块中找问题。比如下图:
虽然崩溃在系统运行时库ucrtbased.dll中,但该系统库本身没问题,是上面的mediasdk.dll中在调用系统运行时库ucrtbased.dll接口时传入了有问题的参数导致的。
5、在分析从任务管理器中导出的dump文件时可能需要使用.effmach命令切换一下上下文
此处给大家讲一个场景,要分析的dump文件可能是从Windows任务管理器中导出的,要分析的目标程序是32位的,且当前操作系统是64位的,在静态分析dump文件查看函数调用堆栈之前,可能需要使用.effmach x86 命令将上下文切换到32位后才能看到有效的函数调用堆栈,这点我们在项目中遇到过。
5.1、操作系统位数与程序位数
现在普遍的使用的Windows操作系统都是64位的,比如Win10和Win11,只有少部分系统是32位的,比如部分XP和Win7系统,但这些系统已经很少用了。64位操作系统是支持32位程序的,即32位程序可以在64位系统中运行,但32系统中时不能运行64位程序的。
很多软件为了既能在32位系统中运行,也能在64位系统中运行,就直接做成32位程序。当然有些软件也做了两个版本,即32位版本和64位版本,用户可以根据自己操作系统的位数选择对应的版本去安装。
这里要说明一下,32位二进制文件是不能和64位二进制文件混用的,比如两个有依赖关系的dll库,位数不同,基本数据类型的占用的长度不同,寻址范围也不同,所以不能混用的。如果混用,程序启动时会报错的。
程序会依赖系统库,程序启动时会将依赖的系统库加载到进程空间中。操作系统为了同时支持32位程序和64位程序,分别搞了两套系统库,即32位版本系统库(位于C:\Windows\SysWOW64路径下)和64位版本系统库(位于C:\Windows\System32路径下)。
关于WOW64的解释:
WOW64 is the x86 emulator that allows 32-bit Windows-based applications to run seamlessly on 64-bit Windows.
简而言之,就是32程序运行在64位系统上。
5.2、使用.effmach命令将线程上下文切换到32位
如果32位程序运行在64位操作系统上,从任务管理器中导出该32位程序的dump文件,用Windbg打开dump文件后输入kn命令查看函数调用堆栈,可能会看到一堆“驴头不对马嘴”的函数调用信息,比如:
堆栈中的函数好像都是系统函数,和我们的业务模块没有一点关系。 遇到这种情况,就需要使用.effmach X86命令将当前的上下文切换到32位上下文,然后重新输入kn,就能看到正常的函数调用堆栈了,如下:
6、Windbg动态调试目标进程时遇到因为调用了IsBadWritePtr或者IsBadReadPtr引发的异常
系统API函数IsBadWritePtr和IsBadReadPtr已经实现不了指定的内存检测功能(检测内存是否可读或可写),这两个函数已经被Windows官方废弃了:
但有些比较老的库可能还在使用这两个API函数。比如我们软件使用的某个第三方dll库中就调用了这两个函数。
在使用Windbg动态调试目标进程中,可能会遇到因为调用了IsBadReadPtr或IsBadWritePtr异常中断了下来,如下所示:
如果要查看是哪个模块调用了IsBadReadPtr或IsBadWritePtr,此时直接输入kn命令查看函数调用堆栈就可以看到了,如下:
这个异常虽然会引发Access violation内存访问违例异常,但这个异常不是致命性,并不会导致程序崩溃,可以输入g命令将当前的异常给跳过去,这样Windbg可以继续调试运行。大家在使用Windbg动态调试,如果遇到因为这两个函数的调用引发的异常中断,则直接输入g命令直接跳过去。
在调试我们的软件时就会遇到这样的异常中断,所以对这个异常专门研究了一下,并写了相关的记录文章:
使用Windbg排查C++程序调用IsBadReadPtr或IsBadWritePtr引发内存访问违例问题https://blog.csdn.net/chenlycly/article/details/129892952
7、什么时候使用Windbg静态分析?什么时候使用Windbg动态调试?
使用Windbg分析软件异常主要有两种方式,一种是使用Windbg静态分析dump文件,一种是将Windbg附加到目标进程上动态调试,两种方式在使用场景上是有一些差异的。
使用Windbg静态分析dump文件,是排查软件异常的主要方式。有dump文件时,则优先使用Windbg静态分析dump文件;如果现有dump文件不足以查出问题,或者没有生成dump文件时,则需要使用动态调试。下面我们着重看一下哪些场景需要使用动态调试(主要看没有生成dump文件的场景)。
7.1、程序发生死循环或死锁问题时
当程序运行过程中发生死循环或者死锁时,导致线程发生堵塞,但程序并没有发生异常崩溃,所以不会生成dump文件,也就无法使用Windbg进行静态分析的,此时可以使用Windbg进行动态调试。动态调试时,可以查看各个线程信息,可以查看各个线程的函数调用堆栈,可以查看相关变量的值,可以设置断点进行调试等。
对于UI客户端程序,如果死循环或死锁发生在UI主线程中,则会直接导致UI主线程的堵塞,就会导致UI界面无法操作的问题,这样的问题我们在项目中都遇到过。之前写过一个使用Process Explorer和Windbg排查死循环的实例,感兴趣的朋友可以去看一下:
使用 Process Explorer 和 Windbg 排查软件线程堵塞案例分享https://blog.csdn.net/chenlycly/article/details/135361532
7.2、程序发生异常,但异常捕获模块没有捕获到
程序中安装的异常捕获模块,并不能捕获到所有场景下的异常,有少部分异常是捕获不到的。捕获不到的,自然就不会生成dump文件,也就没法使用Windbg进行静态分析了。我们在日常项目中也时常会遇到这类场景,程序在使用过程中发生闪退或崩溃,异常捕获模块没捕获到,没有生成dump文件。
一般遇到这类情况,都会让测试人员将Windbg附加到目标进程上,和目标进程一起跑,一旦目标进程在运行过程中发生异常,Windbg会第一时间感知到并中断下来,此时就可以进行分析了。可以查看发生异常的汇编指令信息,并查看此时的函数调用堆栈。
7.3、异常捕获模块感知到了异常,但导出dump文件时产生了二次崩溃,dump文件生成失败
程序发生了异常,异常捕获模块感知到了,但在导出dump文件的过程中产生了二次崩溃,生成dump文件失败,这种情况下也需要使用Windbg进行动态调试。这个场景我们在项目中也遇到过多次。
7.4、程序运行过程中检测到不正常,直接调用abort函数强制结束进程,导致程序闪退
如果程序在运行过程中监测到不正常,直接调用abort函数强制将进程终止了,这样给人一种程序发生崩溃闪退的感觉。其实这种情况并没有发生C++异常,只是调用abort函数直接将进程终止了,异常捕获模块也是感知不到的,所以也不会产生dump文件。
比如在开源jsoncpp库中,如果解析的json节点的类型不匹配,会被jsoncpp内部检测到,会触发abort函数的调用,相关代码截图如下:
再比如WebRTC开源库中在监测到malloc动态申请内存失败后,也会触发abort函数的调用,强行终止进程。估计是因为动态申请内存失败了,相关业务无法正常展开了,程序活着也没意思了,所以就强行终止进程了。相关代码截图如下:
上述代码在调用abort函数之前会先调用DebugBreak,DebugBreak接口仅仅是让正在调试的调试器中断下来,这样我们就有机会感知可能出的问题了。除了DebugBreak接口会让调试器中断下来,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 _DEBUGif (__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
7.5、用IDE调试程序时产生异常,但看不到有效的函数调用堆栈,可以尝试使用Windbg进行动态调试
如果使用IDE(比如Visual Studio)调试代码的过程中产生了异常,但在Visual Studio中看不到完整的或者有效的函数调用堆栈,分析不出来问题,比如刚启动调试时就产生异常或者报错,可以尝试着使用Windbg启动程序或者将Windbg附加到已经启动的程序进程上进行动态调试分析。
7.6、程序启动崩溃或失败时
当程序启动崩溃闪退,或因为某些原因启动失败时,可以尝试使用Windbg启动程序进行动态调试。可能Debug下不好复现,也可能即使能复现,用IDE调试有可能也不太方便。有可能问题只在客户机器上才会出现,没法到客户机器上使用IDE(比如Visual Studio)调试代码。
安装IDE可能会需要几个小时的时间,可能还需要联网在线安装,比较耗时。此外,客户是否允许在其机器上安装软件,还是个未知数。
这些场景下的问题,使用Windbg去动态调试分析很方便。
一般双击程序后没反应,程序没有启动起来,程序可能也没弹出报错提示框,一般都是程序启动过程中发生崩溃导致的。我们最近就遇到过这样的问题,可以参见我之前的文章:(VX交流群中也有朋友遇到类似的问题)
程序启动时访问了未初始化的类指针引发内存访问违例导致程序崩溃的问题排查文章浏览阅读1w次,点赞129次,收藏110次。程序启动时访问了未初始化的类指针引发内存访问违例导致程序崩溃的问题排查_每次当我试图访问一个未初始化的指针或者越界访问内存时,程序就会崩溃,https://blog.csdn.net/chenlycly/article/details/134871805
7.7、程序弹出报错提示框时
程序在运行过程中出现异常,弹出报错提示框,此时进程还在,此刻将Windbg附加到进程上还来的及,可以直接分析。可能问题很难复现,错过了就很难复现了,所以要及时地将Windbg附加到进程上分析。即使一时半会分析不出原因,也可以将进程上下文导出到dump文件中,事后再去分析。
8、有时可能需要使用反汇编工具IDA查看汇编代码上下文去辅助定位问题
有时将现有的函数调用堆栈与C++源码对照看,很难排查出问题时,可能需要借助IDA工具去查看发生崩溃的那条汇编指令的上下文。汇编指令才能最本真最直接地反映出为什么会发生崩溃,将汇编代码上下文与C++源码对照起来去看,借助源码去读懂汇编代码上下文,然后根据汇编上下文推断出那句C++代码有问题。在阅读发生崩溃的那条汇编指令的上下文时,很难直接去阅读汇编代码(如果直接去阅读,则需要很深的汇编功底和逆向分析能力),需要借助汇编代码中的注释,需要与C++源码对照着看,进而去搞懂汇编上下文的含义。
这里需要注意一下,生成的Release版本的软件中,编译器可能会对C++源码进行优化,会导致汇编代码可能较难和C++代码完全对应起来。比如C++源码中有个函数调用,编译器直接将函数调用优化掉,用几句汇编代码替代了,在汇编上下文中看不到调用函数的call指令。之所以要将函数调用优化掉,是为了较少函数调用的开销(比如参数的入栈与出栈、函数调用时的保护现场与恢复现场等),提升代码的执行效率,这也正是编译器优化代码的初衷。
比如一个if判断中有多个条件判断:
if ( A && B && C && D)
其中A、B、C、D代表不同的表达式,包含函数调用的返回值,从崩溃堆栈看直到代码崩溃这一行C++代码行上,但没法确定是崩溃在那个表达式上,此时可以通过查看汇编代码去分析,去确定哪个表达式出现了异常。
再比如我之前给安卓移动开发组排查的一个崩溃问题,是崩溃在底层的C++库中,安卓系统生成了TombStone崩溃日志文件,但该文件中函数调用堆栈只显示了相对于函数的偏移,但没有显示具体的行号:
即只知道崩溃在这个函数中,但不知道具体崩溃在哪一行代码上。后来想到可以使用IDA查看汇编代码上下文,和C++源码对应起来,看看崩溃在哪一行C++代码上。用IDA打开发生崩溃的.so动态库文件,后来根据TomStone文件中显示的相对函数的偏移,找到对应的位置,然后根据汇编代码上下文,找到了崩溃的那条汇编指令对应的C++代码行,最终定位了问题。这个案例很有代表性,我也写了文章记录了问题的完整排查过程,感兴趣的朋友可以去看看:
使用IDA查看汇编代码,结合安卓系统生成的Tombstone文件,分析安卓app程序崩溃问题https://blog.csdn.net/chenlycly/article/details/132283582 此外,这个安卓平台的案例中的.so文件是在安卓ARM64平台上编译的,老版本的IDA6.1是不识别的,没法查看到汇编代码,需要使用7.0及以上的IDA才能正常打开,我之前也写文章记录了这个问题,感兴趣的话,可以去看一下:
Relocations for this machine are not implemented,IDA版本过低导致生成汇编代码失败https://blog.csdn.net/chenlycly/article/details/135076536
9、熟悉Windbg命令
在Windbg中要查看相关信息,需要输入Windbg命令去查看,所以要熟悉.excr、kn/kv/kp、lm、.relaod、.dump等常用的命令,可以到我的文章中查看:
Windbg常用命令详解https://blog.csdn.net/chenlycly/article/details/125508027 也可以去大概地了解一下Windbg都有哪些命令,可以查看我写的Windbg命令汇总:
Windbg调试命令汇总https://blog.csdn.net/chenlycly/article/details/51711212
虽然很多命令在日常工作中很少用到,但在个别场景下可能会用到,可以大概地了解一下Windbg都支持哪些命令,在需要使用的时候再来详细查阅!
关于命令及支持的参数的详细说明,可以查看Windbg的帮助文章。在Windbg中,在菜单栏中点击Help -> Index,打开chm格式的帮助文档,可以在帮助文档中输入命令,查看命令的详细说明及命令支持的参数。以.dump命令为例,如下所示:
输入命令过程中会自动匹配到相关的记录,双击其中的条目即可跳转到命令的详细说明页面。
10、最后
本文根据近几年使用Windbg分析软件异常时的使用经验与心得,详细总结了使用Windbg过程中的诸多细节与技巧,希望能给大家提供一个借鉴或参考。希望大家在了解这些内容之后,能够有效地避坑,提高使用工具的效率。