调试教程第一部分:使用 CDB 和 NTSD 开始调试






4.90/5 (79投票s)
2004年3月20日
18分钟阅读

416360
学习如何调试软件中的问题。
引言
在软件开发和维护方面,调试是最有价值的技能之一。这项技能在产品生命周期的每个阶段都会用到。创建项目的开发人员显然会遇到 bug。这些 bug 可能涉及逻辑错误、语法错误和编译器错误。随着对软件进行更高级场景的测试以及软件与其他环境的交互,质量保证过程中可能会遇到问题。最后,产品发布后,还需要对其进行支持。调试并不会在客户获得软件时结束,bug 通常会被升级回公司,然后公司需要再次进行调试。
本教程的目标是什么?
本教程仅仅是调试的入门。这将是“教程 #1”,如果反馈良好,我将编写更多补充内容。有许多复杂的调试技术和问题,很难知道从何处着手。本教程试图从头开始,让您熟悉调试。我希望将初学者和中级程序员引入高级调试的世界。“高级”调试,基本上是在不重新编译的情况下,“不进行”消息框或 printf
调试”。
调试器和操作系统
要下载微软最新的调试器,请访问 此处。
CDB、NTSD 和 Windbg
本文档将主要讨论 Windows 2000 及更高版本的操作系统。我们将在这里讨论的三种调试器是 CDB、NTSD 和 WinDbg。Windows 2000 及更高版本的系统通常已在系统中安装了 NTSD!这是一个巨大的优势,因为您无需安装任何额外软件即可进行快速调试。
那么有什么区别呢?文档说“NTSD 不需要控制台窗口,而 CDB 需要”。这是真的。NTSD 运行不需要控制台窗口,而 CDB 需要。然而,我发现它们的区别远不止这些。首先,较旧的 NTSD 不支持 PDB 符号文件,只支持 DBG!我还发现 NTSD 不支持符号服务器,而 CDB 支持。较旧的 NTSD 无法创建内存转储,我还发现了其他问题,例如 NTSD 只支持最多 2 个断点命令。NTSD 现在有一个 CDB 没有的优势。那就是不需要控制台窗口。
不需要控制台窗口的能力,在调试用户模式服务或进程(在任何人登录系统之前)时至关重要。如果没有人登录系统,您就无法创建控制台窗口。有一个命令行选项 -d,它指定 NTSD 与附加的内核调试器通信(CDB 也有相同的选项)。这可以用于启动过程中的进程,通过内核调试器调试它们。虽然您已经可以使用内核调试器调试进程,但这使您能够使用用户模式调试器灵活地调试进程。这超出了本入门文章的范围,目前只需理解这个概念即可。
WinDbg 和 CDB 基本相同,只有少数例外。第一点是 WinDbg 是一个 GUI,而 CDB 是一个控制台应用程序。WinDbg 还支持内核调试和源代码级调试。
Visual C++ 调试器
我不用这个调试器,也不推荐使用它。原因有二:首先,这个调试器非常消耗资源。它加载缓慢,而且包含的不仅仅是调试工具,这使得它很麻烦。第二个原因是,通常您需要重新启动才能安装这个调试器。我通常遵循的原则是,运行或测试软件的机器可能没有预装调试器。VC++ 也是一个大型、耗时的安装。
Windows 9x/ME
在 Windows 9x/ME 上我们可以做什么?嗯,您实际上可以使用 WinDbg。调试 API 对所有系统都是相同的,所以对我来说,WinDbg 应该可以在 Windows 9x/ME 上“正常工作”已经很久了。我唯一担心的是 WinDbg 是否会尝试检测到它运行在 Windows 9x 上而不允许调试。我最近发现事实并非如此。唯一的问题是,最新的 WinDbg 安装程序是 MSI 包,它们无法在 Windows 9x 上原生安装。这可以通过在基于 NT 的机器上安装它们并共享目录,甚至将其刻录到 CD 来简单地解决。这当然也有其他副作用,例如不要指望您可以使用所有 `!xxx` 命令,因为 NT 和 9x 将它们的数据放在不同的内存位置。符号会起作用吗?是的,PDB 会起作用。我确实发现设置了 ba r1 xxxxx 后逐步执行代码非常慢。本文档不涵盖 Windows 9x/ME。
设置您的环境
在开始调试或成功设置调试环境之前,这是一个非常重要的步骤。需要根据您的喜好配置系统并包含您所需的所有工具。
符号和符号服务器
符号是任何调试操作的重要组成部分。微软有一个位置,您可以从中下载任何特定操作系统的所有符号(Windows XP 等)。问题是,您需要大量的硬盘空间,如果您在一台机器上调试多个操作系统(从崩溃转储等),那么这就很麻烦。
为了满足调试多个操作系统的需求,微软支持“符号服务器”。这将帮助您在系统中获取正确的符号。符号服务器位于 此处。如果您将符号路径设置为此位置,您的调试器将自动下载您需要的系统符号。您的应用程序所需的符号由您自己决定。
映像文件执行选项
注册表中有一个位置,当应用程序启动运行时,它会自动将调试器附加到该应用程序。这个注册表位置是:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
NT\CurrentVersion\Image File Execution Options
在此注册表项下,您只需创建一个新的注册表项,名称为要调试的进程,例如“myapplication.exe”。如果您以前没有使用过,可能会有一个名为“Your Application Here”或类似的默认项。如果您愿意,可以重命名该项并使用它。
此项下的一个值为“Debugger”。它应该指向您希望在运行此应用程序时启动的调试器。对于“Your Application Here”,默认值为“ntsd -d”。除非您附加了内核调试器,否则您不能使用此值,因此我建议移除“-d”部分。
注意:保留“-d”而不附加内核调试器,可能会导致每次运行该应用程序时系统锁定!请小心。如果您设置了内核调试器,可以通过按“g”来解锁系统。
可能还有另一个名为“GlobalFlags”的值。这是另一个可用于调试的工具,但它超出了本文档的范围。有关更多信息,请查找“gflags.exe”。
内核调试设备
要进行内核调试,首先需要以调试模式启动操作系统。虽然在系统属性下有一个 GUI 可以执行此操作,但我通常直接编辑boot.ini。找到 C:\ 驱动器根目录下的 boot.ini。它很可能是一个隐藏的系统文件。我建议使用 attrib -r -s -h boot.ini 然后打开它进行编辑。
警告:错误地编辑此文件可能会导致您再也无法启动系统!
引导文件可能如下所示:
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS.0=
"Microsoft Windows XP Professional" /fastdetect
我会复制“操作系统”下的第一行。
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS.0=
"Microsoft Windows XP Professional" /fastdetect
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS.0=
"Microsoft Windows XP Professional"
/fastdetect /debug /debugport=COM1 /baudrate=115200
复制的行可以包含您的设置。 `/debug`,然后是 `/debugport=port`,最后是 `/baudrate=baudrate`。要使用的调试端口是您将连接 SERIAL NULL MODEM CABLE 的那台机器的端口。这是您需要的一种硬件。您还需要另一台机器。除了使用 COM 端口,您还可以使用火线,它速度更快。
下次启动时,只需选择“Debugger Enabled”选项即可在调试模式下启动。
环境变量
我通常会将 `_NT_SYMBOL_PATH` 设置为指向 Microsoft 符号服务器以及包含符号信息的本地目录。要设置此环境变量,请转到系统属性 -> 高级 -> 环境变量。
默认调试器
当系统发生任何崩溃时,将使用此默认调试器。默认情况下,它通常设置为“Doctor Watson”。这个程序不值得在此提及。注册表项位于此位置:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
我将“Auto”设置为 1,将“Debugger”设置为您的调试器。
程序集
我强烈建议您学习汇编编程。这些教程将不展示源代码级调试,因为我从不这样做,而且我甚至不知道如何做!源代码级调试的问题在于源代码并非始终可用,有时问题不是通过查看源代码看到的,而是在生成的代码中。它也使跟踪系统更加容易。如果您了解环境是如何设置的,您可以轻松地逆向工程系统以找出您需要知道的信息,而这些信息可能并非总是可以通过源代码级调试获得。
我讨厌源代码级调试的另一件事是,如果源代码与符号不匹配,源代码调试器将不会显示正确的信息。这意味着,如果您创建程序的多个版本或在构建后更改程序,您最好能够找到与您正在调试的构建匹配的源代码!
开始吧
本教程基本上是第一部分,如果受欢迎,我将编写更多教程,每个教程都将越来越高级。第一个教程将介绍几个简单的用户模式编程问题场景。
发布可执行文件的符号
首先,如何为“发布”二进制文件创建符号?很简单。您创建一个 make 文件,该文件可以正确地重新基址化二进制文件。
我通常会为 cl.exe 使用的选项是:
/nologo /MD /W3 /Oxs /Zi /I "..\..\inc" /D "WIN32" /D "_WINDOWS"
/Fr$(OBJDIR)\\ /Fo$(OBJDIR)\\ /Fd$(OBJDIR)\\ /c
我通常会为 link.exe 使用的选项是:
/nologo /subsystem:console
/out:$(TARGETDIR)\$(TARGET)/pdb:<YourProjectName>.pdb
/debug /debugtype:both
/LIBPATH:"..\..\..\bin\lib"
这将为您的项目创建 .PDB 文件。当然,随着 VC++ 7 的推出,它们已经取消了 .DBG(所以 /debugtype:both 可能会在此编译器上出错)。.DBG 是 .PDB 的一个较小版本,它不包含源代码信息,严格来说是符号查找。它甚至不包含参数等信息。如果您使用的是仍能生成它们的编译器,请执行以下操作:
rebase -b 0x00100000 -x $(TARGETDIR) -a $(TARGETDIR)\$(TARGET)
-b 是重新基址化可执行文件的新内存位置。但是,这会剥离发布可执行文件中的调试符号,使其体积更小。如果您使用默认的 Visual Studio 方法构建可执行文件,它可能比此可执行文件稍小一点。然而,您没有符号。使用您指定的优化标志生成的代码相同且经过同样优化。不同之处在于,这些二进制文件现在更有用,因为无论它们去哪里,无论谁使用它们,您仍然可以获得符号!
请记住,最好的调试始终发生在您不必重新构建可执行文件的情况下。一旦您必须重新构建可执行文件,您还必须知道您已经改变了可执行文件的内存占用。您还可能改变了可执行文件的速度。这一点至关重要,因为您现在必须使用此二进制文件重现问题!如果导致问题需要 4 天怎么办?最好能够当场尽可能多地进行调试。
简单的访问冲突陷阱
让我们来分析一个简单的问题。您的程序因“访问冲突”而崩溃,这并不少见!这可能是运行可执行文件时最常见的问题。有三个步骤有助于解决此问题:
- 谁在尝试进行访问?哪个模块?
- 它试图访问什么?内存来自哪里?
- 为什么它试图访问它?它想做什么?
这些是解决此问题的一般指南。我将第二点斜体化,因为它可能是三者中最重要的一点。然而,解决第一点和第三点也可以帮助确定第二点,如果它不明显的话。
我创建了一个非常简单的崩溃程序。我已经将我的默认调试器设置为 CDB,并且我刚刚运行了该程序。我还为该可执行文件创建了符号,并将 `_NT_SYMBOL_PATH` 设置为 Microsoft 符号服务器。
正如我们所见,运行程序时会发生这种情况:
C:\programs\DirectX\Games\src\Games\temp\bin>temp
Microsoft (R) Windows Debugger Version 6.3.0005.1
Copyright (c) Microsoft Corporation. All rights reserved.
*** wait with pending attach
Symbol search path is:
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00404000 C:\programs\DirectX\Games\src\Games\temp\bin\temp.e
xe
ModLoad: 77f50000 77ff7000 C:\WINDOWS.0\System32\ntdll.dll
ModLoad: 77e60000 77f46000 C:\WINDOWS.0\system32\kernel32.dll
ModLoad: 77c10000 77c63000 C:\WINDOWS.0\system32\MSVCRT.dll
ModLoad: 77dd0000 77e5d000 C:\WINDOWS.0\system32\ADVAPI32.DLL
ModLoad: 78000000 78086000 C:\WINDOWS.0\system32\RPCRT4.dll
(ee8.c38): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=7ffdf000 ecx=00001000 edx=00320608 esi=77c5aca0 edi=77f944a8
eip=77c3f10b esp=0012fb0c ebp=0012fd60 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
MSVCRT!_output+0x18:
77c3f10b 8a18 mov bl,[eax] ds:0023:00000000=??
0:000>
我们首先注意到什么?此陷阱发生在 MSVCRT.DLL 中。这是显而易见的,因为调试器通常会使用 <module>!<nearest symbol>+offset 来显示此信息。这意味着 MSVCRT.DLL 中最近的符号是 _output
,而我们在这个符号的偏移量 +18h 处。考虑到这是一个非常小的偏移量,并且符号是正确的(即使符号也可能不正确,但这将在后面的教程中介绍),我们可以假设我们位于 MSVCRT 的 _output()
函数中。
(ee8.c38): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=7ffdf000 ecx=00001000 edx=00320608 esi=77c5aca0 edi=77f944a8
eip=77c3f10b esp=0012fb0c ebp=0012fd60 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
MSVCRT!_output+0x18:
77c3f10b 8a18 mov bl,[eax] ds:0023:00000000=??
0:000>
如果我们想证明这一点,我们可以怎么做?
<0:000> x *!
start end module name
00400000 00404000 temp (deferred)
77c10000 77c63000 MSVCRT (pdb symbols)
c:\symbols\msvcrt.pdb\3D6DD5921\msvcrt.pdb
77dd0000 77e5d000 ADVAPI32 (deferred)
77e60000 77f46000 kernel32 (deferred)
77f50000 77ff7000 ntdll (deferred)
78000000 78086000 RPCRT4 (deferred)
此命令将列出进程中的所有模块及其开始和结束内存位置。我们的陷阱位于 77c3f10b,即 77c10000 <= 77c3f10b <= 77c63000,所以我们肯定陷在 MSVCRT 中。下一步是找出这块内存来自哪里。
有几种方法可以做到这一点,我们可以反汇编代码并尝试找出内存的来源。我们也可以获取堆栈跟踪,然后弄清楚谁在堆栈上。让我们先尝试反汇编 _output
函数,看看内存的来源。
0:000>; u MSVCRT!_output
MSVCRT!_output:
77c3f0f3 55 push ebp
77c3f0f4 8bec mov ebp,esp
77c3f0f6 81ec50020000 sub esp,0x250
77c3f0fc 33c0 xor eax,eax
77c3f0fe 8945d8 mov [ebp-0x28],eax
77c3f101 8945f0 mov [ebp-0x10],eax
77c3f104 8945ec mov [ebp-0x14],eax
77c3f107 8b450c mov eax,[ebp+0xc]
0:000>; u
MSVCRT!_output+0x17:
77c3f10a 53 push ebx
77c3f10b 8a18 mov bl,[eax]
我已突出显示了所有重要的指令。即使您不懂汇编,您也应该听我说一下。首先,我们注意到内存来自 EAX
。它是 CPU 中的一个寄存器,但我们可以将其视为一个变量。 []
包围的 EAX
相当于 C 中的 *MyPointer
。这意味着我们正在引用 EAX
指向的内存。 EAX
来自哪里? EAX
来自 [EBP + 0Ch],您可以将其视为“ DWORD *EBP EAX = EBP[3];
”。这是因为在汇编中没有类型。 EAX
是一个 32 位(DWORD
)寄存器。解引用 EBP + 12 处的 DWORD
在 C 中相当于给 DWORD
指针加 3(或给字节指针加 12 然后类型转换为 DWORD
)。
下一个要看的是 MOV EBP, ESP
。 ESP
是堆栈指针。您应该知道,参数(取决于调用约定和优化)被推到堆栈上,返回地址被推到堆栈上,局部变量也在堆栈上。 ESP
指向堆栈!在内存中,函数调用对于 C 调用约定如下所示:
[Parameter n]
...
[Parameter 2]
[Parameter 1]
[Return Address]
现在,我们看到 PUSH EBP
。 PUSH
意味着将某物推到堆栈上。因此,我们正在将 EBP
的先前值保存在堆栈上。因此,我们的堆栈现在看起来像这样:
[Parameter n]
...
[Parameter 2]
[Parameter 1]
[Return Address]
[Previous EBP]
既然我们将 EBP
设置为 ESP
,我们可以将其视为一个指针,堆栈只是 DWORD
值的数组!因此,这里是 EBP
的偏移量及其指向的位置:
[Parameter n] == [EBP + n*4 + 4] (The formula)
...
[Parameter 2] == [EBP + 12]
[Parameter 1] == [EBP + 8]
[Return Address] == [EBP + 4]
[Previous EBP] == [EBP + 0]
在这种情况下,我们知道我们的变量来自 _output
的第二个参数。那么现在呢?让我们反汇编调用函数!我们知道 EBP
+ 4 指向返回地址,或者我们可以尝试只获取堆栈跟踪。
0:000> kb
ChildEBP RetAddr Args to Child
0012fd60 77c3e68d 77c5aca0 00000000 0012fdb0 MSVCRT!_output+0x18
0012fda4 0040102f 00000000 00000000 00403010 MSVCRT!printf+0x35
0012ff4c 00401125 00000001 00323d70 00322ca8 temp!main+0x2f
0012ffc0 77e814c7 77f944a8 00000007 7ffdf000 temp!mainCRTStartup+0xe3
0012fff0 00000000 00401042 00000000 78746341 kernel32!BaseProcessStart+0x23
0:000>
“KB”是执行此操作的命令之一。现在,我们可能不总是能获得完整的堆栈跟踪,但这对于更高级的教程来说也是如此。在本简单教程中,我们将假设我们得到了完整的堆栈跟踪。我们注意到,这是一个 printf
函数调用,或者看起来是这样。正如我们注意到的,printf
调用了 _output
。让我们反汇编 printf
。请注意,我们可能不总是想反汇编整个函数,我们可以使用分解。有时,仅通过堆栈跟踪就可以找出陷阱(我将在本简单示例的末尾介绍这一点)。但是,这些函数很小,我们可以通过简单地跟踪它们来找出原因。
0:000>; u MSVCRT!_output
MSVCRT!_output:
77c3f0f3 55 push ebp
77c3f0f4 8bec mov ebp,esp
77c3f0f6 81ec50020000 sub esp,0x250
77c3f0fc 33c0 xor eax,eax
77c3f0fe 8945d8 mov [ebp-0x28],eax
77c3f101 8945f0 mov [ebp-0x10],eax
77c3f104 8945ec mov [ebp-0x14],eax
77c3f107 8b450c mov eax,[ebp+0xc]
0:000>; u
MSVCRT!_output+0x17:
77c3f10a 53 push ebx
77c3f10b 8a18 mov bl,[eax]
77c3f10d 33c9 xor ecx,ecx
77c3f10f 84db test bl,bl
77c3f111 0f8445070000 je MSVCRT!_output+0x769 (77c3
77c3f117 56 push esi
77c3f118 57 push edi
77c3f119 8bf8 mov edi,eax
0:000>; u MSVCRT!printf
MSVCRT!printf:
77c3e658 6a10 push 0x10
77c3e65a 68e046c177 push 0x77c146e0
77c3e65f e8606effff call MSVCRT!_SEH_prolog (77c354
77c3e664 bea0acc577 mov esi,0x77c5aca0
77c3e669 56 push esi
77c3e66a 6a01 push 0x1
77c3e66c e8bdadffff call MSVCRT!_lock_file2 (77c394
77c3e671 59 pop ecx
0:000>; u
MSVCRT!printf+0x1a:
77c3e672 59 pop ecx
77c3e673 8365fc00 and dword ptr [ebp-0x4],0x0
77c3e677 56 push esi
77c3e678 e8c7140000 call MSVCRT!_stbuf (77c3fb44)
77c3e67d 8945e4 mov [ebp-0x1c],eax
77c3e680 8d450c lea eax,[ebp+0xc]
77c3e683 50 push eax
77c3e684 ff7508 push dword ptr [ebp+0x8]
0:000>; u
MSVCRT!printf+0x2f:
77c3e687 56 push esi
77c3e688 e8660a0000 call MSVCRT!_output (77c3f0f3)
这很简单。我们注意到 _output
的第二个参数是 [EBP + 8]。我们现在注意到存在 PUSH EBP
和 MOV EBP, ESP
,因此堆栈的设置方式与我之前提到的相同。这并非总是如此,但我们正在慢慢开始。
因此,我们可以确定 printf()
的第一个参数是内存的来源。并且,碰巧的是,printf()
是由我们的程序调用的!从陷阱信息中,我们知道 EAX
是 0,所以我们试图解引用一个 NULL
指针。
77c3f10b 8a18 mov bl,[eax] ds:0023:00000000=??
这是使用的代码:
int main(int argc, char *argv[]) { char *TheLastParameter[100]; sprintf(*TheLastParameter, "The last parameter is %s", argv[argc]); printf(*TheLastParameter); return 0; }
您可以看到它有很多问题!但是,printf
发生了陷阱,因为它为 NULL
。 `*TheLastParameter` 是 NULL
。令人惊讶的是,它没有在 sprintf()
上发生陷阱。那么,我们如何仅凭 KB 来解决这个问题呢?看看这个跟踪:
0:000> kb
ChildEBP RetAddr Args to Child
0012fd60 77c3e68d 77c5aca0 00000000 0012fdb0 MSVCRT!_output+0x18
0012fda4 0040102f 00000000 00000000 00403010 MSVCRT!printf+0x35
0012ff4c 00401125 00000001 00323d70 00322ca8 temp!main+0x2f
0012ffc0 77e814c7 77f944a8 00000007 7ffdf000 temp!mainCRTStartup+0xe3
0012fff0 00000000 00401042 00000000 78746341 kernel32!BaseProcessStart+0x23
0:000>
我们有符号,也有堆栈跟踪。斜体字是第一个参数。它是 0。我们还知道我们调用了它。这是一个非常简单的场景,我试图描绘一些可以用来回溯到问题根源的技术。学习堆栈。了解堆栈是如何设置的以及堆栈上的内存是什么,对于查找和跟踪数据来源至关重要。您不一定总能幸运地仅通过“kb”就能找到所有信息。
程序未按预期工作
这是一个常见的错误。您运行程序,但没有看到正确的输出,或者程序一直给您发送错误消息。想要创建的文件没有被创建,等等。这是一个非常普遍的问题,可能很容易也可能很难解决。您会采取哪些初步步骤来调试这个问题?
- 什么没有起作用?
- 这会围绕哪些 API 或模块?
- 什么会导致这些 API 无法正常工作?
这些是一些步骤,但它们并不通用。假设您有一个程序尝试在 Windows 中创建文件。但是文件没有被创建。让我们看一些代码:
HANDLE hFile; DWORD dwWritten; hFile = CreateFile("c:\MyFile.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if(hFile != INVALID_HANDLE_VALUE) { WriteFile(hFile, "Test", strlen("Test"), &dwWritten, NULL); CloseHandle(hFile); }
这是您的代码。通常,您会想要重新编译,可能使用 GetLastError()
并打印出来。但是,您不必这样做。尽管在这种情况下它可能很简单,但如果您逐步执行代码并且某个函数失败了,您难道不想立即知道发生了什么吗?让我们尝试调试一下。首先,我们将启动调试器并在我们的函数上设置断点。由于我们有符号,这很容易。如果我们没有,我们可以直接在 CreateFile
上设置断点,因为它是一个导出的符号,并且始终可用。
C:\programs\DirectX\Games\src\Games\temp\bin>cdb temp
Microsoft (R) Windows Debugger Version 6.3.0005.1
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: temp
Symbol search path is:
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00404000 temp.exe
ModLoad: 77f50000 77ff7000 ntdll.dll
ModLoad: 77e60000 77f46000 C:\WINDOWS.0\system32\kernel32.dll
ModLoad: 77c10000 77c63000 C:\WINDOWS.0\system32\MSVCRT.dll
(2a0.94): Break instruction exception - code 80000003 (first chance)
eax=00241eb4 ebx=7ffdf000 ecx=00000004 edx=77f51310 esi=00241eb4 edi=00241f48
eip=77f75a58 esp=0012fb38 ebp=0012fc2c iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
ntdll!DbgBreakPoint:
77f75a58 cc int 3
0:000> bp temp!main
0:000> g
我们在 main()
函数上设置了一个断点,然后按“go”。我们到达断点,然后使用“p”逐条指令地跳转到我们的 CreateFile
函数。
Breakpoint 0 hit
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401000 esp=0012ff50 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main:
00401000 51 push ecx
0:000>; p
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401001 esp=0012ff4c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x1:
00401001 56 push esi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401002 esp=0012ff48 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x2:
00401002 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401003 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x3:
00401003 33ff xor edi,edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401005 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x5:
00401005 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401006 esp=0012ff40 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x6:
00401006 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401007 esp=0012ff3c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x7:
00401007 6a03 push 0x3
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401009 esp=0012ff38 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x9:
00401009 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=0040100a esp=0012ff34 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0xa:
0040100a 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=0040100b esp=0012ff30 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0xb:
0040100b 6800000080 push 0x80000000
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401010 esp=0012ff2c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x10:
00401010 6810304000 push 0x403010
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401015 esp=0012ff28 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x15:
00401015 ff1504204000 call dword ptr [temp!_imp__CreateFileA (00402004)]{kernel3
2!CreateFileA (77e7b476)} ds:0023:00402004=77e7b476
0:000>; p
eax=ffffffff ebx=7ffdf000 ecx=77f939e3 edx=00000002 esi=00000000 edi=00000000
eip=0040101b esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei ng nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000286
temp!main+0x1b:
0040101b 8bf0 mov esi,eax
调用 CreateFile
后,EAX
将具有返回值。我们注意到它是 ffffffff 或“无效句柄值”。我们想知道 GetLastError
。它存储在 fs:34。 FS
是 TEB 选择器,所以我们可以转储它。
0:000> dd fs:34
0038:00000034 00000002 00000000 00000000 00000000
0038:00000044 00000000 00000000 00000000 00000000
0038:00000054 00000000 00000000 00000000 00000000
0038:00000064 00000000 00000000 00000000 00000000
0038:00000074 00000000 00000000 00000000 00000000
0038:00000084 00000000 00000000 00000000 00000000
0038:00000094 00000000 00000000 00000000 00000000
0038:000000a4 00000000 00000000 00000000 00000000
CDB 还有一个更快捷的方法:!gle
0:000> !gle
LastErrorValue: (Win32) 0x2 (2) - The system cannot find the file specified.
LastStatusValue: (NTSTATUS) 0xc0000034 - Object Name not found.
0:000>
所以,找不到文件。但是,文件就在那里!那么问题是什么?我们需要进一步调试。我们可以查看的一个方面是传递给 CreateFile
的参数。
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401010 esp=0012ff2c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x10:
00401010 6810304000 push 0x403010
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401015 esp=0012ff28 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x15:
00401015 ff1504204000 call dword ptr [temp!_imp__CreateFileA
(00402004)]{kernel32!CreateFileA (77e7b476)} ds:0023:00402004=77e7b476
幸运的是,它是一个常量,所以内存仍然会存在。即使它不是常量,内存也仍然会存在,因为我们没有离 CreateFile
返回太远。
然后我们可以使用“da”、“dc”或“du”。“da”是转储 ANSI 字符串,“du”是转储 Unicode 字符串,“dc”类似于“dd”但它转储所有字符,甚至包括不可打印的字符。因为我们知道它是一个 ANSI 字符串,所以只需使用 da。
0:000> da 403010
00403010 "c:MyFile.txt"
0:000>
那是不对的!我们需要使用 C:\\MyFile.txt 才能与 C:\ 一起工作!
所以,我们修复了这个问题。但是等等,它仍然无法写入!我们需要进一步调试。
我们再次执行相同的操作。
C:\programs\DirectX\Games\src\Games\temp\bin>;cdb temp
Microsoft (R) Windows Debugger Version 6.3.0005.1
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: temp
Symbol search path is:
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00404000 temp.exe
ModLoad: 77f50000 77ff7000 ntdll.dll
ModLoad: 77e60000 77f46000 C:\WINDOWS.0\system32\kernel32.dll
ModLoad: 77c10000 77c63000 C:\WINDOWS.0\system32\MSVCRT.dll
(80c.c94): Break instruction exception - code 80000003 (first chance)
eax=00241eb4 ebx=7ffdf000 ecx=00000004 edx=77f51310 esi=00241eb4 edi=00241f48
eip=77f75a58 esp=0012fb38 ebp=0012fc2c iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
ntdll!DbgBreakPoint:
77f75a58 cc int 3
0:000>; bp temp!main
0:000>; g
Breakpoint 0 hit
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401000 esp=0012ff50 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main:
00401000 51 push ecx
0:000>; p
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401001 esp=0012ff4c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x1:
00401001 56 push esi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401002 esp=0012ff48 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x2:
00401002 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401003 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x3:
00401003 33ff xor edi,edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401005 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x5:
00401005 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401006 esp=0012ff40 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x6:
00401006 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401007 esp=0012ff3c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x7:
00401007 6a03 push 0x3
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401009 esp=0012ff38 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x9:
00401009 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=0040100a esp=0012ff34 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0xa:
0040100a 57 push edi
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=0040100b esp=0012ff30 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0xb:
0040100b 6800000080 push 0x80000000
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401010 esp=0012ff2c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x10:
00401010 6810304000 push 0x403010
0:000>;
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401015 esp=0012ff28 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x15:
00401015 ff1504204000 call dword ptr [temp!_imp__CreateFileA (00402004)]{kernel3
2!CreateFileA (77e7b476)} ds:0023:00402004=77e7b476
0:000>;
eax=000007e8 ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=00000000 edi=00000000
eip=0040101b esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei ng nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000293
temp!main+0x1b:
0040101b 8bf0 mov esi,eax
0:000>; p
我们到达这里,注意到 EAX
是一个有效句柄,而不是无效句柄值!继续。
eax=000007e8 ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=0040101d esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei ng nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000293
temp!main+0x1d:
0040101d 83feff cmp esi,0xffffffff
0:000>;
eax=000007e8 ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401020 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x20:
00401020 741b jz temp!main+0x3d (0040103d) [br=0]
0:000>;
eax=000007e8 ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401022 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x22:
00401022 8d442408 lea eax,[esp+0x8] ss:0023:0012ff4c=00322cf8
0:000>;
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401026 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x26:
00401026 57 push edi
0:000>;
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401027 esp=0012ff40 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x27:
00401027 50 push eax
0:000>;
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401028 esp=0012ff3c ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x28:
00401028 6a04 push 0x4
0:000>;
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=0040102a esp=0012ff38 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x2a:
0040102a 6820304000 push 0x403020
0:000>;
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=0040102f esp=0012ff34 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x2f:
0040102f 56 push esi
0:000>;
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401030 esp=0012ff30 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x30:
00401030 ff1500204000 call dword ptr [temp!_imp__WriteFile (00402000)]{kernel32!
WriteFile (77e7f13a)} ds:0023:00402000=77e7f13a
0:000>; p
eax=00000000 ebx=7ffdf000 ecx=77e7f1c9 edx=00000015 esi=000007e8 edi=00000000
eip=00401036 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x36:
00401036 56 push esi
我们刚刚调用了 WriteFile
,而 EAX
== 0。这意味着 False!让我们检查其他变量。
第二个参数是正确的,长度为 4。
0:000> da 403020
00403020 "Test"
粗体显示的第四个参数,它是一个指向写入字节数的指针。它为 0。
0:000> dd 012ff4c
0012ff4c 00000000 00401139 00000001 00322470
0012ff5c 00322cf8 00403000 00403004 0012ffa4
0012ff6c 0012ff94 0012ffa0 00000000 0012ff98
0012ff7c 00403008 0040300c 00000000 00000000
0012ff8c 7ffdf000 00000001 00322470 00000000
0012ff9c 8053476f 00322cf8 00000001 0012ff84
0012ffac e1176590 0012ffe0 00401200 004020c0
0012ffbc 00000000 0012fff0 77e814c7 00000000
那么,让我们检查 GetLastError
。
0:000> !gle
LastErrorValue: (Win32) 0x5 (5) - Access is denied.
LastStatusValue: (NTSTATUS) 0xc0000022 - {Access Denied}
A process has requested access to an object,
but has not been granted those access rights.
0:000>
访问被拒绝?什么会导致这种情况!让我们检查一下,等等,我们只以读取模式打开了文件!我们没有以写入模式打开文件!所以,我们可以轻松地解决这个问题,然后继续我们的下一个项目!
hFile = CreateFile("c:\\MyFile.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
结论
总之,这只是对一些非常基本调试技术的一个介绍。这些例子很简单,但您必须认识到它们所展示的技术的价值。这只是本调试教程的第一部分。希望,如果感兴趣,我可能会添加更多教程,使内容越来越高级。
对一些人来说,本教程可能很简单,对另一些人来说可能太高级了。您不会一夜之间成为一名优秀的调试员,这需要练习。我建议尝试使用调试器来解决最简单的问题。您练习得越多,就会做得越好。我保证,您花在摆弄工具上的时间越多,学到的就越多。