目录
2、Windbg版本
3、PDB文件说明
4、Windbg中pdb符号文件路径配置
5、在Windbg中配置pdb文件路径和源代码路径
6、常用的Windbg命令
7、Windbg静态分析dump文件
8、Windbg动态调试
Windbg是微软提供的Windows平台下强大的用户态和内核态调试利器,给我们分析Windows上软件的异常提供了极大的便利和有力的支持,比原始的直接查看代码分析异常的效率要来的高的多。Windbg在某些方面甚至要比微软的Visual Studio还要强大,作为Windows平台的开发人员,必须要会使用该工具分析问题。
Windbg的主界面如上所示。Windbg可以静态分析dump文件,也可以对目标进程进行动态调试。Windbg可以直接启动进程,也可以附加到正在运行的目标进程上,可以查看进程运行时变量内存中的数据。Windbg在排查内存访问违例、死循环、死锁、搞CPU占用、异常崩溃等方面,有着独到的优势。
其实,Windbg入门的门槛较低,只需掌握一些常用的Windbg命令即可。另外,有时需要深入研究问题时,可能还要使用到IDA反汇编工具,去查看二进制文件中的汇编代码的上下文,查看汇编代码才能最直观地看出软件崩溃的最直接的原因。所以,除了掌握Windbg之外,我们还需要学会使用IDA反汇编工具,本文主要讲解windbg的使用。
至于Windbg的版本,有无需安装的6.0绿色版本,也有需要安装的最新的10.0的版本,这里推荐大家使用10.0及以上的版本。
10.0新版本相对6.0版本,要智能很多,要查看相关内容,不用再输入各种复杂命令,只要点击超链接查看即可。在6.0版本中,很多命令是需要手动输入的。
另外,6.0版本不能加载VS2017编译生成的pdb文件(6.0版本的Windbg是无法识别VS2017编译出来的pdb文件的),所以要处理VS2017的程序问题,是需要使用10.0版本的Windbg的。
新版本的Windbg已经内置到Windows SDK的安装包中,这个安装包可以到微软的官方网站上下载,安装时只需要勾选安装Windbg即可。对于Win10系统,也可以到系统自带的应用商店中去免费下载安装。
PDB - Program Databse File,程序数据库文件,存放了二进制文件中所有函数及变量的符号,要查看完整的函数调用信息和变量信息,都需要使用pdb文件。Windbg和IDA都需要使用pdb文件。
Windbg使用时需要配置pdb文件路径,IDA使用时只需将pdb文件放置在目标二进制的同级目录中, IDA会从目标二进制文件所在的目录中搜索对应的pdb文件,去自动加载。
另外,加载pdb文件时会严格校验pdb文件的时间戳,必须要和二进制文件完全一致。即使是同样的代码,在不同时间编译生成的pdb文件,也是不能交叉使用的。
需要注意的是,pdb文件的名称必须和工程的名称一致,否则也是无法加载的,比如工程名称为codec_hw.vcxproj,生成的pdb文件的名称默认应为codec_hw.pdb,但在执行文件拷贝时将文件名称改成codec.pdb,这种情况下Windbg也是无法加载的。必须手动将文件名改成工程名后,才能正常加载。
在Windbg中配置pdb符号库路径时,一般设置如下格式的pdb文件路径:
C:\Users\Administrator\Desktop\tlpdb;srv* f:\mss061*http://msdl.microsoft.com/download/symbols
这么个一长串路径主要由下面三个部分构成:
1)应用程序库pdb文件路径(非系统库):
C:\Users\Administrator\Desktop\pdb,一般我们将我们自己编写的库的pdb统一放置到一个路径中。
2)本地安装的Windows系统库pdb文件路径:
f:\mss0616,可以到微软官网上下载和当前Windows系统一致的系统符号库安装包(网页中会自动检测当前系统对应的pdb文件版本),这个路径就是本机上安装的路径。但系统pdb库一般只是我们自己机器的pdb,在分析崩溃在其他机器上的dump文件是没用的,所以还需要设置微软在线pdb下载路径。
3)Windows系统库在线下载路径:
http://msdl.microsoft.com/download/symbols,Windbg会根据dump文件中的系统库的版本,到这个在线网站上去自动下载对应版本的pdb文件。这里需要主要一下,有时设置这个在线路径后,可能因为这个在线网址访问比较慢,会使windbg卡住阻塞住,如果遇到这种情况,可以将暂时将该路径从配置中去除,以保证windbg的流畅使用。
我们需要将函数调用堆栈中涉及到的模块的pdb文件添加到Windbg中,具体的添加pdb文件(路径)的方法是,在Windbg菜单栏中点击File --> Symbol File Path,打开如下的窗口,将pdb文件所在的路径设置进来即可:
这样我们在windbg打印出来的函数调用堆栈中就能看到具体的函数名及变量信息了。
我们不用通过函数调用堆栈中显示的代码行号到源代码文件中找对应的代码行,我们可以将项目源代码的根路径设置到Windbg中,在菜单栏中点击File --> Source File Path,将项目源代码的路径拷贝进来即可:
这样我们在点击调用堆栈中的序号链接时,Windbg的源代码视图就会自动跳到对应的代码行上,这样看起来很方便,特别是我们在查看多个堆栈帧时。
1).ecxr
用来切换到异常发生时的上下文,主要用在分析静态dump文件的时候。当我们使用.reload命令去强制加载库的pdb文件后,需要执行.ecxr命令,重新切换到异常上下文。
2)kn/kv/kp
用来查看当前线程的函数调用堆栈。如果要查看当前进程的所有线程的函数调用堆栈,可以使用~*kn命令。
3)lm
用来查看库的信息,比如库的路径、时间戳、库的加载地址等。一般使用模糊匹配的模式,比如:lm vm codec*。
4).reload
用来加载pdb文件,一般用来强制加载某个pdb文件,比如:.reload /f kdcodec.dll。注意这个命令中的名称要使用完整的文件名称。
5)!analyze -v
输出当前异常的详细分析信息。
6)g
在中断模式下,跳过中断,继续运行,主要用于实时调试的场景。
7)bp/bl/bc
添加、查看、删除断点,主要用于实时调试的场景。
8)~ns
切换到n号线程中,在GUI应用程序中,UI线程为0号线程,是GUI程序的主线程。
9).dump
创建一个用户模式或内核模式的转储文件,比如.dump /ma C:\1125.dmp。
10)r
显示当前线程所有寄存器的值。
11).cls
清除当前屏幕显示。
安装版本的Windbg(非绿色版)会自带帮助文档,可以到帮助文档中查看命令的详细说明:
我们在代码中添加了异常捕获的模块,一旦软件发生异常崩溃,我们的异常捕获模块一般都能捕获到,然后自动将当时的异常信息保存到dump存储文件中,事后可以将dump文件取来分析。使用Windbg静态分析dump文件,有以下几点重要的说明。
1)切换到异常上下文
Windbg打开dump文件后,输入.ecxr命令切换到异常上下文,此时可以看到异常发生时的寄存器中的值,以及发生异常的汇编指令。可以看到汇编指令所属的模块及该指令代码段的地址。
2)查看函数调用堆栈
紧接着,输入kn/kv/kp命令,查看异常发生时的函数调用堆栈,通过函数调用堆栈,我们就能大概地看出是什么操作引起的异常。要在函数调用堆栈中看到具体的函数名及函数中的行号,我们需要将对应模块的pdb文件加载Windbg中来。需要看哪个模块,就将该模块的pdb文件加载进。
3)加载pdb符号库
我们先使用类似于lm vm libcurl*命令,查看directui.dll库生成的时间戳,这样就能确定该库的编译时间,然后到0.99服务器上找到对应的版本路径,找来对应的pdb文件,然后将pdb路径设置到windbg中。注意添加路径时,记得勾选reload选项,这样windbg就会自动去加载所需要的pdb文件。但有时自动加载不进来,就需要我们使用命令去强制加载了。
使用类似于.reload /f libcurl.dll命令,其中/f参数表示强制加载,注意命令中的文件名必须是带后缀的完整文件名。如果还是加载失败,我们可以使用!sym noisy命令打开pdb符号库加载详情,看显示的详情中为啥会加载失败。
pdb符号库加载失败,可能是pdb文件的时间戳对不上,也有可能是因为pdb文件名和工程名不一致引起的。
另外,有时没加载pdb时看到的堆栈可能都是无效的二进制地址信息,加载pdb后才能看到完整的模块信息和函数调用信息,这个情况我们也遇到过不少次。
4)结合函数调用堆栈进行具体分析
pdb符号库加载成功后,重新输入.ecxr切换到异常上下文,再次输入kn命令,此时打印出来的函数调用堆栈中就能看到具体的函数名和代码的具体行号了。将函数调用堆栈和C++源代码结合起来,逐步排查出代码中存在的问题。
5)全dump文件和简要dump文件
有些时候我们可能需要查看函数调用堆栈中某个函数中变量的值,我们可以点击函数调用堆栈中的帧序号,就能看到该函数中局部变量以及当前函数所在类的this指针在内存中的值。
但因为我们异常捕获库自动保存的dump文件中只包含了部分概要信息的,可能看不到变量的值或者只能看到部分变量的值,只有全dump文件才能看到完整的内存信息。
一般我们通过.dump命令导出的dump文件就是全dump文件,文件中包含了完整的异常信息和内存数据信息,这种全dump文件一般比较大,大概有好几百M。
另外,在Windows任务管理器中通过进程的右键菜单导出的转储文件也是比较完整的全dump文件,文件也比较大。
6)64位上下文需要转化为32位上下文
有时dump文件可能是从64系统的资源管理器中导出的转储文件,windbg打开后看到的函数调用堆栈很奇怪,完全和我们的业务代码对应不上,此时可能Windbg调试工具的使用是64位的异常上下文:
而我们的程序是32位的,所以需要用.effmach X86命令转换成32位的上下文,例如转换后的函数调用堆栈如下:
7)实例分析
后续我们将陆续给出windbg分析异常的实例,敬请期待。
Windbg可以直接启动程序进行调试,也可以将Windbg附加到已经启动的进程上进行调试。
当软件发生卡死(堵塞)、死锁、死循环时,可以将Windbg附加到目标进程上调试,去查看相关线程的函数调用堆栈,可以设置断点进行调试。甚至在软件发生崩溃弹出系统提示框时:
将Windbg附加到出问题的进程上也是能获取到异常信息的。
在程序中安装的异常捕获模块不是所有的异常和闪退都能捕获到的,在捕获不到异常时,可以尝试将Windbg挂载到目标进程上运行,如果能将异常复现出来,Windbg就能捕捉到。挂上Windbg后也可以一直拷机运行,直到将问题复现出来为止。
有时在VS中调试源代码产生崩溃,VS中断下来,但是在函数调用堆栈中看不到有效的函数调用,比如只能看到运行时库中相关接口的调用。再比如,发生stack overflow栈溢出异常后,进程直接退出了,VS直接中断了调试,也是无法看到函数调用堆栈的。这些时候,可以尝试重新启动程序,将Windbg附加上去,根据之前的操作,将问题复现出来,如果Windbg捕获到异常后,就能看到完整的函数调用堆栈了。
有些时候,我们在查看dump文件分析异常时,需要查看内存中变量的值去辅助排查问题,但是我们自动生成的dump文件比较小,不是全dump文件,只能查看到部分变量内存中的值,可能查看不到想要查看的变量的值。如果问题比较好复现,则可以将windbg挂载上去,实时去捕捉异常信息,或者使用.dump命令导出全dump文件,供后续分析。
Windbg在动态调试的过程中,如果代码中调用了API函数IsBadReadPtr或者IsBadWritePtr,并且这些函数返回TRUE,则会触发Windbg中断下来,其实这两个函数已经被微软废弃了,遇到这种中断,直接go一下,忽略掉,程序就能继续运行了。
因为软件异常基本都是运行在用户态的代码触发的,所以大多数时候我们进行的都是用户态的调试。当涉及到驱动及内核对象等调试时,我们才会使用内核态的调试,内核态的调试想对要复杂一些。