Windows Debuggers:第一部分:WinDbg 教程






4.91/5 (126投票s)
2004 年 2 月 14 日
16分钟阅读

1684973

10814
这是关于使用 WinDbg 进行调试的多部分教程的第一部分。
目录
引言
在我职业生涯中,我看到我们大多数人使用 Visual Studio 进行调试,但很少使用其他免费提供的调试器。您可能因为多种原因需要这样的调试器,例如,在您的家用电脑上,您并不用它进行开发,但它上安装的某个程序偶尔会崩溃。通过堆栈转储,您可以弄清楚 IE 是因为第三方插件而崩溃的。
我没有找到任何好的 WinDbg 入门指南。本文将通过示例讨论 WinDbg。我假设您了解调试的基本概念——单步进入、单步跳出、断点以及远程调试的含义。
请注意,这是一份入门文档,您可以阅读它并开始使用 WinDbg。要了解更多关于特定命令的信息,请查阅 WinDbg 文档。您可以在 Microsoft 提供的任何调试器中使用本文档中介绍的命令,例如,在 Visual Studio .NET 的命令窗口中。
本文基于 WinDbg 6.3。
这是关于调试的一系列文章中的第一篇。在下一篇文章中,我将解释如何编写调试器扩展 DLL。
调试器概述
简要概述您可以在 此处 免费下载的 Windows 调试器
- KD – 内核调试器。您希望使用它来远程调试操作系统问题,例如蓝屏。如果您开发设备驱动程序,您就需要它。
- CDB – 命令行调试器。这是一个控制台应用程序。
- NTSD – NT 调试器。这是一个用户模式调试器,可用于调试您的用户模式应用程序。实际上,这就是添加到 CDB 的 Windows 风格 UI。
- Windbg – 使用了 KD 和 NTSD,并提供了一个不错的 UI。WinDbg 既可以作为内核模式调试器,也可以作为用户模式调试器。
- Visual Studio, Visual Studio .NET – 使用与 KD 和 NTSD 相同的调试引擎,并提供比 WinDbg 更丰富的 UI 用于调试目的。
调试器比较
功能 | KD | NTSD | WinDbg | Visual Studio .NET |
内核模式调试 | Y | N | Y | N |
用户模式调试 | Y | Y | Y | |
非托管调试 | Y | Y | Y | Y |
托管调试 | Y | Y | Y | |
远程调试 | Y | Y | Y | Y |
附加到进程 | Y | Y | Y | Y |
在 Win2K 和 XP 中分离进程 | Y | Y | Y | Y |
SQL 调试 | N | N | N | Y |
WinDbg
WinDbg 是一个将 NTSD 和 KD 包装在更好的 UI 中的调试器。它提供了命令行选项,例如最小化启动(-m)、按 pid 附加到进程(-p)以及自动打开崩溃文件(-z)。它支持三种类型的命令
- 常规命令(例如:k)。常规命令用于调试进程。
- 点命令(例如:.sympath)。点命令用于控制调试器。
- 扩展命令(例如:!handle)——这些是您可以添加到 WinDbg 的自定义命令;它们在扩展 DLL 中实现为导出函数。
PDB 文件
PDB 文件是链接器生成的程序数据库文件。私有 PDB 文件包含有关私有和公共符号、源行、类型、局部变量和全局变量的信息。公共 PDB 文件不包含类型、局部变量和源行信息。
调试场景
远程调试
使用 WinDbg 进行远程调试很简单,可以通过多种方式完成。在以下场景中,“调试服务器”是指在您想要调试的机器上运行的调试器;“调试客户端”是指控制会话的调试器。
- 使用调试器:您需要在服务器上使用 CDB、NTSD 或 WinDbg。WinDbg 客户端可以连接到 CDB、NTSD 和 WinDbg 中的任何一个,反之亦然。服务器和客户端可以选择 TCP 和命名管道作为通信协议。
- 启动服务器
- WinDbg –server npipe:pipe=pipename(注意:可以连接多个客户端),或
- 在 WinDbg 内部:.server npipe:pipe=pipename(注意:只能连接一个客户端)
您可以使用多个协议启动多个服务器会话。您可以为会话设置密码保护。
- 从客户端连接
- WinDbg -remote npipe:server=Server, pipe=PipeName[,password=Password]
- 在 WinDbg 内部:文件->连接到远程会话:对于连接字符串,输入 npipe:server=Server, pipe=PipeName [,password=Password]
- 启动服务器
- 使用 remote.exe:remote.exe 使用命名管道进行通信。如果您使用的是基于控制台的应用程序,如 KD、CDB 或 NTSD,您可以使用 remote.exe 进行远程调试。注意:使用 @q(而不是 q)退出客户端而不退出服务器。
- 启动服务器
- Remote.exe /s “cdb –p <pid>” test1
- 从客户端连接
- Remote.exe /c <machinename> test1
上面提到的 test1 是我们选择的任意命名管道名称。
- 启动服务器
服务器将显示所有连接的客户端来自哪个服务器以及执行的命令。您可以通过发出“qq”来退出服务器;或者通过文件->退出退出客户端。如果您想进行远程调试,您需要属于“调试器用户”用户组,并且服务器必须允许远程连接。
即时调试
WinDbg 文档中的“启用事后调试”部分对此有很好的讨论。简而言之,您可以通过运行 Windbg –I 将 WinDbg 设置为默认的 JIT 调试器。这会将注册表项 HKLM\Software\Microsoft\Windows NT\CurrentVersion\AeDebug
设置为 WinDbg。要将 WinDbg 设置为默认的托管调试器,您需要显式设置以下注册表项
- 将
HKLM\Software\Microsoft\.NETFramework\DbgJITDebugLaunchSetting
设置为 2 - 将
HKLM\Software\Microsoft\.NETFramework\DbgManagedDebugger
设置为 Windbg。
通过 JIT 设置,如果一个应用程序在未被调试且未自行处理异常的情况下抛出异常,WinDbg 将会被启动。
64 位调试
所有这些调试器都支持在 AMD64 和 IA64 上的 64 位调试。
托管调试
WinDbg 6.3+ 支持托管调试,使用 Whidbey .NET CLR。文档中对托管调试有很好的讨论。请记住,托管代码没有 PDB,因为托管代码被编译为 ILASM;调试器与 CLR 通信以查询额外信息。
注意事项
只有在托管代码函数被调用至少一次后,您才能在其上设置断点;因为那时它才被 JIT 编译为 ASM 代码。请记住
- 函数地址(因此断点)的复杂性
- CLR 可以丢弃已编译的代码,因此函数地址可能会发生变化。
- 如果多个应用程序域不共享代码,则相同的代码可能会被多次编译。如果您设置了断点,它将为当前线程的应用程序域设置。
- 泛型的特化可能导致同一函数有多个地址。
- 数据布局(因此数据检查)的复杂性
- CLR 可能在运行时任意更改数据布局,因此结构中的字段偏移量可能会随时间变化。
- 类型信息仅在首次使用时加载,因此如果数据字段尚未被使用,您可能无法检查它。
- 调试器命令的复杂性
- 在跟踪托管代码时,您会经过运行时代码块,例如 JIT 编译器代码(当您第一次单步进入一个函数时),或者在从托管代码过渡到非托管代码时。
调试服务
您可以通过 WinDbg 调试服务,就像调试其他应用程序一样,既可以通过启动服务然后附加到服务进程来调试,也可以通过使用 WinDbg 作为 JIT 调试器并以编程方式调用 DbgBreakPoint
或 DebugBreak
,或者在 x86 上使用汇编指令 int 3
来调试。
调试异常
调试器会收到两次异常通知——第一次是在应用程序有机会处理异常之前(“第一次机会异常”);如果应用程序不处理该异常,调试器将有机会处理该异常(“第二次机会异常”)。如果调试器不处理第二次机会异常,应用程序将退出。
.lastevent 或 !analyze –v 将显示异常记录和发生异常的函数堆栈跟踪。
您还可以使用 .exr、.cxr 和 .ecxr 命令来显示异常和上下文记录。另外请注意,您可以使用 sxe、sxd、sxn 和 sxi 命令更改异常的第一次机会处理选项。
WinDbg 特性
调试器扩展 DLL
调试器扩展是 DLL,您可以将其挂接到调试器以在调试器内部执行自定义命令。DLL 需要实现某些函数,并且需要满足一些要求才能作为扩展 DLL。在下一篇文章中,我们将学习如何自己编写扩展 DLL。感叹号 (!) 命令是从您的扩展 DLL 执行的命令。请注意,扩展 DLL 加载在调试器的进程空间中。
转储文件
您可以使用转储功能来获取进程的快照信息。迷你转储通常很小,除非您进行完全内存的迷你转储(.dump /mf)。转储句柄信息也有用,例如 .dump/mfh。迷你转储包含有关所有线程的信息,包括它们的堆栈和已加载模块列表。完整转储包含更多信息,例如进程堆的信息。
崩溃转储分析
如果您的 Windows 操作系统崩溃,它会将物理内存内容和所有进程信息转储到一个转储文件中,该文件通过“系统->控制面板->高级->启动和恢复”进行配置。还可以通过中断实时进程来转储任何实时进程。您还可以通过将 WinDbg 配置为 JIT 调试器来转储任何异常终止的进程(.dump)。请注意,从崩溃转储中找出代码中的错误可能是一个复杂的过程。
要分析转储,请按照以下步骤操作
步骤 1:在 WinDbg 中,选择文件->“打开崩溃转储”,然后指向转储文件
步骤 2:WinDbg 将显示您的应用程序崩溃时正在执行的指令。
步骤 3:正确设置您的符号路径和源路径。如果您无法匹配符号,您可能很难弄清楚控制流。如果您能将符号匹配到正确版本的源代码,此时应该很容易找出错误。请注意,私有符号文件包含行号信息,并且会盲目显示源代码中的行,而无需进一步检查;如果您的源代码版本不匹配,您将看不到与汇编代码匹配的正确源代码。如果您有公共 PDB 文件,您将看到最后一个被调用的公共函数(在调用堆栈上)。
请注意,调试驱动程序或托管代码差别很大。有关设备驱动程序的调试技术,请参阅 [2]。
WinDbg 设置
符号 文件和目录
要进行有效的调试,您需要符号。符号文件可以是旧的 COFF 格式或 PDB 格式。PDB 是程序数据库文件,包含公共符号。这些调试器允许您指定一个 URI 列表,它们将在其中查找已加载二进制文件的符号。
操作系统符号通常安装在 %SYSTEMDIR%Symbols 目录中。驱动程序符号(.DBG 或 .PDB 文件)通常与驱动程序(.sys 文件)位于同一文件夹中。私有符号文件包含有关函数、局部变量和全局变量的信息,以及用于将汇编代码与源代码关联的行信息;通常提供给客户的符号文件是公共符号文件——这些文件仅包含有关公共成员的信息。
您可以通过“文件->符号文件路径”设置符号目录,或使用 WinDbg 命令窗口中的 .sympath。要添加对 Web 上符号服务器的引用,请添加
SRV*downstream_store*http://msdl.microsoft.com/download/symbols
到您的 .sympath,如下所示
.sympath+ SRV*c:\tmp*http://msdl.microsoft.com/download/symbols
其中 c:\tmp 是 download_store
,必要的符号将在其中下载和存储。请注意,此特定符号服务器仅公开公共符号。
调试器在匹配 PDB 和二进制文件(DLL 或 exe)时,会匹配文件名、时间戳和校验和等信息。如果您有符号信息,您将在调用堆栈中看到函数名称及其参数。如果二进制文件和 PDB 来自您的应用程序,您将额外获得有关私有函数、局部变量和类型信息。
sympath
可以包含多个 URI。Sympath
从 _NT_SYMBOL_PATH
系统环境变量初始化。
源代码目录
您可以通过“文件->源文件路径”设置源代码目录,或使用 WinDbg 命令窗口中的 .srcpath。如果您设置了源代码目录,调试器将在调试期间根据 PDB 文件中的行号信息显示匹配的源代码。
断点、跟踪
- 使用
bp
命令或工具栏上的断点图标设置软断点。 - 使用
DbgBreakPoint()
或KdBreakPoint()
等代码设置硬断点。 - 使用跟踪例程
DbgPrint
、KdPrint
、OutputDebugString
从调试器扩展 DLL 向 WinDbg 输出窗口打印。
Commands
基本命令
WinDbg 安装附带的帮助文件对命令有很好的记录,但以下基本命令应该能让您开始
功能 | 命令 | 作用 | 示例/注释 | 另请参阅相关命令 |
堆栈跟踪 | K, KB x |
显示当前线程的堆栈跟踪(x 帧)。Kb 会在显示中包含传递给每个函数的前三个参数。 | KP, Kp, or KV | |
Frame | .frame X |
|||
寄存器监视 | R | 显示寄存器组。reax – 显示 eax 寄存器。 |
||
步骤 | t | 跟踪 = 单步进入 (F11) | ||
p | 单步跳过 (F10) | |||
单步退出 | Shift + F11 | |||
反汇编 | u | 反汇编接下来的几条指令 | ||
u <start_address > |
在 start_address 处反汇编指令 |
|||
u <start_address >< |
反汇编从 start_address 到 end_address 的指令 |
|||
断点 | Bl | 列出断点。 | ||
be, bd, bc | 启用/禁用/清除断点。 | |||
bp | 设置断点。 | |||
bu | 设置未解析的断点。断点通过符号名称而不是绝对地址解析。使用此命令在尚未加载包含模块的函数处设置断点。 | bu foo | ||
注释 | * | 忽略该命令 | * Hello World | |
Continue | G <address_X / symbol > |
继续。恢复执行直到 address_X |
||
GH | 继续,异常已处理 | |||
GN | 继续,异常未处理 | |||
退出 | Q | |||
转储数据 | dv | 显示局部变量。 | 您需要私有符号。 | |
Dd <address > |
在指定地址显示 dword 值。 |
要查看 int 的值,DD <addr > L1 |
||
Ds, da (ASCII), du (Unicode) | 转储字符串 | |||
Dt [dt module!typedef adr] | 转储类型。将使用 typedef 作为模板来转储内存内容。 |
|||
更改/编辑值 | Eb (byte ), ed (dword ), ea (ASCII), eu (Unicode) |
编辑变量的值 | ||
列出模块 | lm | 列出已加载的模块 | Lmi, lml, !dlls | |
线程 | ~ | 列出所有线程 | ||
线程 n 上的命令 | ~n<command > |
通过线程 ID 切换到特定线程并在该线程上执行命令。 | ~2kb(第二个线程的堆栈) | |
在模块中搜索符号 | X module!<pattern> | X blah!*foo* | ||
Dump | .dump | |||
源代码行显示 | .lines | 打开源代码显示 | ||
ln adr | 将显示离该位置最近的符号。 |
- 没有“单步退出”(Shift+F11)。您必须手动在堆栈上找到返回地址并使用“g adr”。您可以通过使用“k”找到此地址。如果您知道该函数使用 ebp 帧,您可以使用“g poi(ebp+4)”来单步退出。
- 检查局部变量
- 使用“dv”命令。
- 然后使用“dt <
variablename
>”命令。 - 注意:如果值存储在寄存器中或由于 FPO,您可能看不到正确的值。
更多命令
功能 | 命令 | 作用 | 示例/注释 | 另请参阅相关命令 |
Vertarget | 显示您正在调试的系统信息。 | |||
数据断点(硬件断点) | Ba [ba r/w/e size adr] |
设置数据断点。您可以在内存位置的读取/写入/执行尝试时中断。 | ba w4 adr | |
异常 | .lastevent | 显示最后一条异常记录 | ||
异常 | Sx, Sxe, sxd, sxn, sxi exception_X |
启用/禁用/仅通知/忽略第一次机会异常/事件 exception_X . 事件示例:模块卸载/线程创建。 |
||
显示类型 | Dt | 显示 struct 和字段值。 |
Dt x; // x: int Dt myStruct; // struct myStruct Dt myStruct myVar1; // 显示 myStruct.myVar1 |
|
重新加载符号 | .reload | 使用您设置的符号路径重新加载符号。 | ||
源代码行 | l+l, l+o, l+s, l+t | 源代码行选项 | ||
.ecxr | 如果您遇到异常,则切换到故障上下文。 | |||
.quit_lock | ||||
; | 命令分隔符 | |||
? | 评估表达式 | |||
| | 显示进程信息 | |||
.chain | 列出所有已加载的调试器扩展。 | |||
.echo <string > |
回显/打印任何字符串 | Echo xyz | ||
.exr <address_x > |
显示 x 处的异常记录。 |
|||
.cxr <address_x > |
显示 x 处的上下文记录。 |
|||
.trap | 转储陷阱帧。 |
有用的扩展命令
- !help – WinDbg 扩展命令的帮助。
- !load, !unload – 用于加载和卸载调试器扩展 DLL。
- !handle – 显示有关进程拥有的句柄的信息。
- !peb - 显示 PEB(进程环境块),包括 DLL 信息。
示例
附带了一个带有这些示例函数的示例应用程序
- 示例 1:程序似乎挂起,因为一个线程无限期地等待一个被另一个线程获取然后退出而未释放的关键部分。
- 示例 2:异常:除以零。
- 示例 3:每次命中断点时执行命令。
- 示例 4:异常:空指针访问
- 示例 5:异常:重复删除
- 示例 6:异常:由于无限递归导致的堆栈溢出
建议练习
- 异常:数组越界访问
- 异常:已删除指针访问
- 异常:堆栈下溢
结语
注意事项
请注意:
- 当您运行 WinDbg,附加到一个进程并发出 kb 命令时,您将看到调试器注入的线程的堆栈跟踪。所有调试命令都在注入线程的上下文中执行。
- 帧指针省略 (FPO)
表示当您的代码被编译时,堆栈上不会放置帧指针(EBP)。这使得函数调用更快,并将 EBP 寄存器用作临时寄存器。MSC++ 编译器的优化选项 /Oy => FPO;/O2 或 /Ox(完全优化)=> /Oy。
问答
- 如何列出模块导出的所有符号?
x <module>!*
- 如何查找特定命令的帮助?
.hh <command>,或 <command> /?
- 我希望某个应用程序 x.exe 始终在 WinDbg 下运行。如何配置?
在“HKLM\Software\Microsoft\Windows NT\currentversion\image file execution options”下创建一个名为 x.exe 的键,并向其中添加一个名为“Debugger”的新字符串值;将其值设置为 windbg.exe 的路径。
- 我希望每次命中断点时都执行某个操作。如何实现?
bp 命令接受一个命令列表作为参数,您可以在每次命中断点时执行这些命令。示例
bp WindbgEx1!Example3+0x3d "dd [ebp-0x14] L1; .echo hello world;g"
(参考:附带代码)
在函数 Example3 的每次迭代中打印局部变量的值。
- 可以设置一个只触发一次的断点吗?
是:bp /1
- 可以设置一个断点,使其在 k-1 次传递后才开始命中吗?
是,bp <address> k
参考文献
- WinDbg 文档 [来自 Microsoft]
- 《Windows 2000 设备驱动程序开发指南》– Art Baker, Jerry Lozano