目录
1. Windbg简介
Windbg是微软提供的Windows平台下强大的用户态和内核态调试利器,给我们分析Windows上软件的异常提供了极大的便利与有力的支持,比原始的直接查看代码分析异常的效率要高的多。Windbg在某些方面甚至要比微软的Visual Studio还要强大,作为Windows平台的开发人员,必须要会使用该工具分析问题。
Windbg在排查内存越界、内存访问违例、stack overflow线程栈溢出、空指针与野指针、死循环、死锁、高CPU占用、内存泄露、GDI对象泄露、函数调用约定不一致导致的栈不平衡等方面,有着独到的优势。
其实,Windbg入门的门槛较低,只需掌握一些常用的Windbg命令即可。另外,有时需要深入研究问题时,可能还要使用到IDA反汇编工具,去查看二进制文件中的汇编代码的上下文,查看汇编代码才能最直观地看出软件崩溃的最直接的原因。所以,除了掌握Windbg之外,我们还需要学会使用IDA反汇编工具,二者结合使用。
2. Windbg安装
2.1 版本说明
Windbg有无需安装的6.0绿色版,也有需要安装的10.0的版本。
10.0版本相对6.0版本要智能很多,很多时候要查看相关内容时不用再输入各种复杂的命令,只要点击工作区显示的超链接即可查看。
在6.0版本中,很多命令都是需要手动输入的,很多命令也比较复杂。另外,6.0版本的Windbg无法识别VS2017编译出来的pdb,所以要处理VS2017的程序问题,需要使用10.0版本的Windbg的。
2.2 安装
2.2.1 windbg
新版本的Windbg已经内置到Windows SDK的安装包中,这个安装包可以到微软的官方网站上下载
① 下载winsdksetup.exe
下载地址:Windows 调试工具 - Windows drivers | Microsoft Learn
下载安装程序winsdksetup.exe
② 下载windbg安装包
双击winsdksetup.exe
两种安装方式:
- 在线安装表示直接下载安装
- 下载到本地表示将windbg的安装包下载到本地再安装
只勾选Debugging Tools for Windows选项即可
下载完成以后如下所示:
③ 双击安装windbg
安装完成以后如下所示:
2.2.2 Windbg Preview
本文以下说明以Windbg Preview为例进行说明
3. Windbg分析崩溃的两种方式
Windbg支持两种使用方式:
- 静态分析dump文件--常用
- 动态调试目标进程
- 直接启动进程进行调试
- 附加到正在运行的目标进程进行调试
4. 静态分析dump
4.1 示例
第一步:使用Windbg打开dmp
此时会自动分析和下载需要的Windows系统的PDB文件。
这时候,我们可以根据Windbg的基本分析得到大概的崩溃原因。如上图所示,崩溃原因是:Access violation,code是c0000005,所以崩溃原因是:内存访问违例。
Windows下溃崩的原因也就那几种,注意遇到积累即可。例如:
- Access violation:内存访问违例
- Stack overflow:线程栈溢出
- Integer divided by zero:除0异常
第二步:设置pdb路径,然后输入.reload命令。
输入命令:
如果pdb加载失败,可以使用强制加载:.reload /f XXX.dll。
即使是强制加载,二进制文件和pdb也需要是对应的,即:同时编译生成,否则强制加载也会失败。
第三步:输入.ecxr命令,切换到发生异常的线程上下文。
此时是可以看到崩溃时的各种寄存器的值,例如:rcx、rdi、eax、ebx等,也可以看到发生异常时的汇编指令是什么。
此时我们可以根据汇编指令和对应寄存器的值来找到初步的线索。例如:
- 指令中是否访问了小于64KB地址的内存,从而引发内存访问违例--可能是空指针或野指针导致
- 指令中是否访问了内存态的内存地址,从而引发内存访问违例--可能是内存越界导致的
第四步:输入kn或kv或kp命令,查看异常发生时的函数调用堆栈。
其中kv能同时查看到调用时给函数传递的参数。
崩溃堆栈解析结果以!为分界:
- !前面为模块名,不显示dll和exe
- !后面为函数名、偏移值、文件名、代码行号
- 偏移值:每个模块被加载到内存中时都有一个首地址,偏移值表示调用堆栈相对于当前模块的偏移量。
如果此时没有pdb被加载进来,则只能看到模块名称和偏移值,无法看到具体函数名称和代码行号。我们可以使用lm vm命令查看对应模块的详细信息,帮助我们找到对应的pdb文件。
第五步:设置源码路径
然后就可以看到详细的崩溃函数,并且点击调用堆栈中的函数时,Windbg的源代码视图就会自动跳到对应的代码行上
此时就可以结合函数调用堆栈、变量信息、源码进行分析为何崩溃了。
4.2 64位上下文转换为32位上下文
有时dump文件可能是从64位系统的资源管理器中导出的,Windbg打开后看到的函数调用堆栈很奇怪,完全和业务代码对不上,此时可能是因为Windbg使用的是64位的异常上下文。
而我们的程序是32位的,所以需要使用.effmach X86命令转换成32位的上下文。
补充说明:
在64位Windows系统上抓32位进程的dmup文件时,如果用的是64位任务管理器,那么在用Windbg加载后,要用!wow64exts.sw切换到X86模式下。如果不想做这步切换,就要用32位的任务管理器来生成dmp文件。32位任务管理器在:C:\Windows\SysWOW64\Taskmgr.exe
5. Windbg动态调试
5.1 什么情况下使用Windbg动态调试
情景1:
当我们的软件在用户那里发生崩溃,但是我们自己本机又无法复现时,我们不可能在用户那里装一个VS2015去调试。但是我们可以把pdb、源码文件拷贝到用户机器,使用Windbg在用户机器进行调试。
如果短期内无法查出原因,崩溃以后我们可以使用.dump命令保存一份full dump文件,之后再详细分析。
情景2:
通过Windbg动态调试进程,如果程序在运行过程中发生异常,Windbg调试器会立即感知并中断,然后就可以查看发生异常的汇编指令和函数调用堆栈。
情景3:
5.2 附加到进程调试
第一步:附加进程
第二步:设置pdb文件路径
第三步:打开源码(.cpp文件即可)
第四步:设置断点
第五步:开始调试
其调试快捷键和VS相同
5.3 启动进程调试
第一步:设置启动进程和参数
第二步:设置pdb路径
第三步:打开源码文件
第四步:启动调试
第五步:设置断点,再次启动调试
6. Windbg使用技巧
6.1 打开dmp时不加载pdb
Windbg在打开时,默认会去微软符号服务器下载对应的pdb,这是因为我们设置的pdb路径中包含:srv*。
但是某些时候我们希望快速打开dmp,而不加载pdb。我们可以修改符号路径,将其指向一个不存在pdb的路径,这样在打开dmp时,去指定的路径下加载pdb,由于找不到pdb直接就可以直接打开dmp。
7. Windb命令
7.1 Windbg帮助文档
安装版本的windbg在安装目录下有帮助文档:debugger.chm
7.2 常用功能命令
.cls | 清屏 |
.ecxr | 切换到发生异常的线程 上下文 |
kn或kv或kp | 查看异常发生时的函数调用堆栈。 其中kv能同时查看到调用时给函数传递的参数 |
!analyze -v | 自动分析异常,并给出详细的分析结果 |
示例如下:
!analyze -v
这个命令可以详细分析异常,并给出详细的分析结果
7.3 线程相关命令
~*kb | 查看所有线程堆栈,*通配符表示所有线程 |
~0kb | 查看主线程堆栈 ● ~波浪线就像一个线头thread,所以线程相关命令都由~开头 ● 0是主线程的序号 ● kb表示输出调用堆栈中函数的前3个参数; |
~# | 查看崩溃/异常的线程号 |
~. | 查看当前线程号 |
~1s | 切换到1号线程 |
.thread | 显示当前线程信息 |
示例如下:
~*kb
查看所有线程堆栈,*通配符表示所有线程
~0kb
查看主线程堆栈
- ~波浪线就像一个线头thread,所以线程相关命令都由~开头
- 0是主线程的序号
- kb表示输出调用堆栈中函数的前3个参数;
7.3 模块相关命令--lm
lm:List Loaded Modules,用于显示指定的已加载模块
lm | 查看模块信息 主要显示模块名称,模块开始地址,模块结束地址,模块路径等信息。 |
lm vm XXXX | 查看指定模块的pdb是否加载,如果已经加载,则会显示出对应pdb的路径 lm vm命令能看到pdb文件有没有加载成功,能看到目标模块代码段的起始地址和结束地址,能看到目标模块文件生成时的时间戳。 查看时间戳有什么作用呢?通过查看时间戳,我们可以确定文件的编译时间,进而根据时间去找对应的pdb文件。 |
示例如下:
如下所示使用lm vm命令查看,可以看到没有显示出pdb的路径,则表示对应模块的pdb没有被加载
7.4 pdb加载命令
.reload | 重新加载pdb |
.reload /f xxxx.dll | 强制加载xxxx.dll的pdb。 即使是强制加载,二进制文件和pdb也需要是对应的,即:同时编译生成,否则强制加载也会失败。 |
7.5 dump导出命令
.dump /ma D:\test.dmp | 将当前进程的上下文导出到dump文件中 |
7.6 信息查看命令
.process | 显示当前进程信息 |
!dlls | 查看模块信息 |
!cpuinfo | 显示cpu信息 |
!address | 显示内存信息 |
!vm | 显示虚拟内存信息 |
!memusage | 显示物理内存信息 |
.srcpath | 查看当前源代码搜索路径等信息 |
.sympath | 查看当前符号搜索路径等信息 |