发布时间:2023-07-17 10:00
Windbg是微软开发的在Windows平台下强大的用户态和内核态调试工具,是Windows系统排查软件异常的一大利器,使用Windbg能极大的提高我们排查问题的效率,Windbg可以快速分析出软件崩溃、死循环、死锁等多种异常。
很多时候,如果仅仅通过排查代码或者添加打印去排查软件的异常崩溃,效率会很低,特别是代码量比较大的大项目,查起来会更加费劲。并且有些软件异常不是必现的或很难复现的,更不利于问题的排查。我们可以在工程代码中引入谷歌的CrashReport库,给软件安装异常捕获机制,当软件发生异常时,CrashReport库能捕获到异常信息,并将异常信息生成到dump文件中,这样我们事后就可以使用Windbg去分析dump文件中的异常信息定位软件发生异常崩溃的原因了。
下面简单的谈一下使用Windbg分析dump文件的一般步骤,聊一聊Windbg的一些常用命令,给大家提供一个借鉴或参考。
Windbg就像我们用的很多软件一样,也是在不断的演进,不断的更新版本,所以在网络上会搜到不同时期的版本,比如常用的6.0版本和10.0版。6.0是若干年前比较老的版本,10.0则是微软最近的新版本。10.0版本要比6.0版本强大很多,以前的很多复杂的Windbg数据解析命令在Windbg6.0中要手动输入,在10.0中则是以链接的方式,只要点击链接就会自动执行对应的指令,无需再记那些复杂的windbg命令了,只要掌握一些常用的命令就可以了。不过,10.0的Windbg的下载与安装比较复杂,Windbg的安装包是内置在windows SDK包中的,可以在下面的页面中去下载:
https://docs.microsoft.com/zh-cn/windows-hardware/drivers/debugger/debugger-download-tools
Win10系统中安装Windbg比较简单,可以使用微软自带的应用商店去安装最新版的Windbg。鉴于Windbg10.0安装比较复杂,并且网上提供免安装的绿色版的windbg6.0,所以很多人还是比较喜欢WIndbg6.0。
但windbg6.0毕竟是比较老的版本,除了之前讲的很多复杂的命令是需要自己手动输入之外,Windbg6.0打开VS2017编译的程序生成的dump文件时,始终无法加载pdb文件,Windbg6.0是能加载VS2010编译出来的pdb文件的。应该是VS2017生成的pdb文件中文件格式发生了改变,导致老版本的windbg6.0识别不了了,所以加载不起来。
用Windbg打开捕获到的dump文件,先在Windbg中先输入.ecxr命令,切换到异常的上下文:
可以看到崩溃信息的异常描述,可以看到崩溃时eax、ebx、ecx等各个寄存器的值,可以看到发生异常崩溃的那条汇编代码是什么。
有时通过这些东西,可以初步的判断出发生异常的原因,比如崩溃的汇编指令中访问了64KB小内存地址,并且ecx寄存器中的值为0(C++的汇编代码会把当前类对象的地址存放在ecx中),则很有可能是代码中访问了空指针导致的问题。
紧接着可以输入kn,kv或kp命令(kp命令可以看到函数调用时给被调用函数传递的参数变量的值),查看异常发生时的函数调用堆栈:
从调用堆栈中看是哪些函数调用触发的异常。注意,如果崩溃可能发生在Windows的系统库中,一般不是系统库本身的问题,应该是更上层的应用软件模块中出的问题。
在没有pdb符号库文件(pdb文件中包含了模块中的所有函数及变量等符号)的情况下,在打印出来的函数调用堆栈中看不到具体调用的是哪个函数,都是相对于库导出接口的偏移地址。
所以需要将函数调用堆栈中涉及到的模块的pdb文件添加到Windbg中,具体的添加pdb文件(路径)的方法是,在Windbg菜单栏中点击File --> Symbol File Path,打开如下的窗口,将pdb文件所在的路径设置进来即可:
此时记得要勾选reload选项,这样Windbg会自动在指定的路径搜索pdb文件,将pdb加载到Windbg中。
注意,设置路径后,还要执行.ecxr命令,重新切换到异常的上下文,然后输入kn查看函数调用堆栈中能不能看到具体的函数调用。不过,有时pdb文件自动加载不了,可以使用lm vm directui*(以directui.dll)命令查看目标库的pdb有没有加载成功:
如果加载成功,会在该命令执行后,将pdb的路径打印出来的。
如果目标库的pdb文件没有自动加载成功,则需要使用.reload命令强制去加载pdb文件,比如.reload /f directui.dll。注意,该命令中必须使用包括后缀名的完整的模块名称。如果还是加载不了,可能是pdb文件的时间戳和对应的dll或exe不一致,pdb文件版本可能不对,需要重新去找pdb文件。
加载pdb时,会严格校验pdb文件的时间戳,即使代码没改动,不同时间编译出来的pdb文件,都是不能加载的。dll模块文件的时间戳要和pdb文件的时间戳完全一致。
这个地方有个技巧,当发现打印出来的函数调用堆栈中的偏移地址特别大,比如达到0x1000以上,可能是对应的pdb文件没有加载成功,此时就需要手动去强制加载了。
此处,要注意一下,加载pdb符号库文件后,在函数调用堆栈中就能看到具体的函数了,也能看到位于文件的哪一行代码上了:
directui!DirectUICore::CButtonUI::DoEvent+0x322
(g:\svnroot\project_jyzb\20180926_jyzb_v5r2_sp4_jyzb\90-XXX\directui\source\uibutton.cpp @ 227)
然后我们就可以通过代码的行号,在源码中找到对应行号的C++代码,就能分析这句C++代码及上下文是否存在问题了。
我们不用通过函数调用堆栈中显示的代码行号到源代码文件中找对应的代码行,我们可以将项目源代码的根路径设置到Windbg中,在菜单栏中点击File --> Source File Path,将项目源代码的路径拷贝进来即可:
这样我们在点击调用堆栈中的序号链接时,Windbg的源代码视图就会自动跳到对应的代码行上,这样看起来很方便,特别是我们在查看多个堆栈帧时。
lm vm命令很实用,能看到pdb文件有没有加载成功,能看到目标模块代码段的起始地址和结束地址,能看到目标模块文件生成时的时间戳。
有时我们右键点击目标文件,打开文件属性页面查看目标文件的生成时间,即属性页面中的文件修改时间:
但这个时间不一定准,有时并不是目标文件的生成时间,可能是其拷贝到当前系统中的时间,所以使用Windbg查看目标文件的生成时间最直接,也是最准确的。
有时候光看C++代码,可能不够直观,也搞不清楚代码崩溃的真实原因,需要去看对应的汇编代码。可以在调用堆栈中查看到某个函数调用的返回地址(对应着一条汇编代码)0x73a76e52(也有可能是崩溃的最后一条汇编代码的地址,代码段的地址):
然后使用了lm vm命令查看所在模块的起始地址0x73a60000:
计算出目标汇编语句相对模块起始地址的偏移,然后在打开目标模块文件的IDA中查看目标模块的默认加载起始地址,拿之前的偏移地址加上IDA中的默认加载地址:
就能得到目标汇编指令在IDA中的静态地址:
0x73a76e52 - 0x73a60000 + 0x10000000 = 0x10016E52
在IDA中按下快捷键G,在弹出窗口中输入计算出来的地址:
点击确定,就可以Go到目标汇编代码的位置了。
汇编代码有点晦涩难懂,要看懂满屏的汇编代码很不容易,这要求我们要有很好的汇编功底。但我们只是辅助排查问题,直接看汇编代码的能力比较有限,只需要简单的看懂目标汇编指令附近的上下文,能和C++代码大概对应起来,不用看完整个函数的汇编代码实现。
但是一句C++代码可能会对应几句汇编代码,并且release下会对汇编代码进行优化,所以要把汇编代码和C++完全对应起来也没那么简单。那该如何是好呢?有个办法,使用IDA打开目标二进制文件时,将二进制文件对应的pdb文件放在二进制的同级目录中,这样IDA会自动搜索到pdb文件,将pdb文件中的函数及变量的符号添加到IDA反汇编出来的汇编代码中,会给汇编代码添加一定的注释:
通过这些注释我们就能将汇编代码和C++代码对应起来了。
此外,再介绍一个windbg的命令:!analyze -v
这个命令可以详细分析异常,并给出详细的异常分析结果,如下所示:
大家可能搞不清楚常用的Windbg命令都是什么含义,可以到Windbg的帮助文档中查看命令的详细说明。在菜单栏点击help --> Index,打开Windbg的帮助文档,在索引栏输入目标命令,即可以索引到目标命令,从而打开目标命令的详解说明页面。