程序调试指南1:使用CDB和NTSD进行调试

 
简介
调试无疑是软件开发和维护中是最有用的技能之一,它贯穿于产品的整个生命周期内。开发者在刚开始建立工程时就会遇到来自于逻辑、语法、编译器等方面的各种各样的BUG。当软件提交给QA,在复杂环境中做深入测试的时候,还会出现很多问题。即便产品发布后,它仍需要支持和维护。调试工作不会在用户得到软件那天停止,软件公司会陆续接到问题反馈,然后不得不重新组织调试工作。
目的
"tutorial #1"只是对调试的一个大致介绍,如果反响不错的话,我会继续写下去。调试方面的技术太多太复杂了,从哪里开始呢?好吧,我首先应该让大家对调试熟悉起来。我希望这篇指南可以将高级调试技术展现在初级和中级开发者面前。所谓高级调试意味着不需要重新编译,不需要“messagebox”或“printf”。
 
调试器和操作系统
CDB 和Windbg
本文是基于Windows2000或者更高的操作系统讨论的。我将会介绍3个调试工具:CDB、NTSD和WinDbg。Windows2000及更高的版本已经将NTSD集成到系统中,所以对于快速调试你根本不需要安装任何额外的软件。
那么它们的不同之处在哪里呢?开发文档说“CDB需要一个提示符窗口而NTSD不需要”。确实如此,但它们之间还有很多其它不同。第一,较老的NTSD版本只支持PDB符号文件而不支持DBG符号文件;第二,NTSD不支持符号服务器;第三,老版本的NTSD不能生成memory dump。另外,NTSD只能设置2个断点。NTSD的好处就是它能在没有提示符窗口的情况下运行。
这点是很重要的,比如,你可能需要在用户登录到系统之前调试一个用户级的服务或进程。因为没有用户登录时是不能创建提示符窗口的。NTSD有一个命令行参数-d,用带这个参数的命令可以与内核调试器进行通信(CDB也有同样参数)。这个在对开机启动的进程进行内核调试时很有用,它增加了你的灵活性,既可以用内核调试器也可以用用户模式调试器。关于这一点,我们会在后面的文章中讨论。
WinDbg和CDB基本上是相同的。只不过WinDbg是一个GUI程序而CDB是命令行程序。WinDbg同时支持内核调试和代码级调试。
VC++ 调试器
我从来不用这个调试器,也不推荐大家使用。原因是,这个调试器太耗资源了。它加载很慢并且它不是一个单一的调试工具,显得很笨重。还有,在安装VC++的调试器的时候你必须重新启动系统。在测试软件的时候,我通常首先会在机器上安装一个调试器,而VC++是一个巨大的东西,安装起来非常耗时。
Windows9x/ME
在Windows9x/ME上我们可以使用WinDbg的。由于调试用的API函数在所有系统上都是一样的,所以我以前认为WinDbg只能在Windows9x/ME上使用。于是我将所有注意力都集中在它是否会检测是否被运行在Windows98上而阻止在其它版本上运行。可我最近发现这个想法是错误的。还有一个问题是,最新版本的WinDbg是一个MSI的安装包,这个安装包在Windows9x上是不能运行的。这个问题也很好解决,可以将它安装在NT的机器上然后共享这个安装目录或者将安装包刻录在CD上。显然,这样也有副作用,比如不可能像在NT下那样使用所有的!xxx命令而且会将数据存储在不同的位置。
建立环境
在开始调试前,需要首先建立调试环境。因为需要配置系统来让它连接和集成所需的工具。
符号和符号服务器
对于调试来说符号都是非常重要的部分。你可以从微软网站上下载不同系统的符号。但如果要在同一台机器上完成在很多不同系统的调试的话你需要有很大的磁盘空间来存放这些符号。这样显得很笨重。
为了适应这样的需求,微软开辟了一个符号服务器,这个服务器地址是 http://msdl.microsoft.com/download/symbols。你可以将符号路径设置为这个地址,这样调试器就会自动从服务器上下载合适的符号。你的程序需要什么样的符号完全由你来决定。
镜像文件执行选项
在注册表中有一个位置决定某一个应用程序运行时要自动加载的调试器。如下所示:
HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows 
     NT/CurrentVersion/Image File Execution Options
在这个键下面可以创建新的子键,子键名字与要调试的程序名字相同(比如“myapplication.exe”)。如果以前没有使用过这个项,它下面会有一个默认的子键,名字大致是“Your Application Here”。也可以直接更改这个子键。
默认键下有一个值名叫“Debugger”,这个值应该指向程序运行时需要加载的调试器。它的默认值是“ntsd -d”。除非你连接了一个内核调试器否则就不能用“-d”。
注意:如果没有内核调试器,“-d”会导致每次启动程序时到要从系统中搜索内核调试器,所以一定要小心。在已经建立了内核调试器的系统中可以点击“g”来解锁系统。
还有一个值叫做“GlobalFlags”。它用来设定另外一个调试工具。但这已经超过了本文的讨论范围。要了解这方面内容,请查阅一下“gflags.exe”的相关内容。
内核调试环境
为了进行内核调试,需要重新启动系统进入调试模式。尽管系统中有一个GUI可以来做这件事情,我通常还是会直接改写boot.ini。boot.ini存放在C:/根目录下。这个文件是隐藏的。可以使用attrib –r –s –h boot.ini 命令,然后用edit打开这个文件。
注意:错误改写这个文件会导致无法启动系统。
Boot.ini文件如下所示:
 
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
复制“Operating Systems”节下的第一行:
 
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,/boudrate=baudrate。串口号规定了与另外一台机器相连的串口。除了使用串口,还可以使用火线(1394――译注),这个比串口更快些。
当下次启动的时候,选中“Debugger Enable”就可以进入调试模式。
环境变量
通常我会将_NT_SYMBOL_PATH设置为微软的符号服务器或者本地存放符号信息的路径。要设置这个环境变量请到System Properties->Advanced->Enviroment Variables。
默认调试工具
HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/WindowsNT/CurrentVersion/AeDebug 规定了当程序崩溃时使用的默认的调试器。默认情况下,这个调试器是“ Doctor Watson ”。但这个程序没什么用。我通常会将“ Auto ”设置为 1 ,“ Debugger ”设置为我的调试器。
汇编
我极力推荐大家学习一下汇编编程。这个指南不会使用代码级的调试因为我从来没做过,也不知道怎么做。代码级调试最大的问题是,代码并不是总是可见的,而且很多时候单从代码看找不到出错的地方。使用汇编还可以更容易地了解到系统的处理过程。在明白怎样设置调试环境之后,你就可以将系统反汇编来找到想要的信息,而代码级调试通常是做不到这一点的。
我不喜欢代码级调试的另一个原因是,当代码与符号不匹配时,代码调试器会显示错误的信息。这就意味着,如果你建立的是一个多版本的程序或者在建立程序后修改了源代码,你最好能够找到与正在调试程序相匹配的源代码。
开始调试旅程
这个指南仅仅是第一部分,如果大家喜欢,我还会继续深入的写下去。在第一部分中,我将向大家展示一下用户模式下一些简单程的序问题以及怎样调试它们。
Release 版本可执行文件加入符号
首先,怎么才能够为“ Release ”版本的二进制文件建立符号呢?这个很简单。你只需要建立一个合适的 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 介绍,将不会使用 .DBGs (所以 /debugtype:both 可能会在 VC++7 编译器上导致错误)。 .DBG .PDB 的浓缩版本,它不包含源文件信息,只能直接采用符号查看。它甚至不包含任何参数信息。如果你的编译器仍能够生成 .DBG ,你可以这样做:
rebase -b 0x00100000 -x $(TARGETDIR) -a $(TARGETDIR)/$(TARGET)
-b 后面跟的是符号在原始可执行文件中的新地址。这个命令会将 debug 的符号剥离出来生成尺寸更小的 Release 版本。与 VS 默认方法编译的可执行文件相比,它们都使用指定的标志进行了优化,而 VS 生成的文件会更小一点,因为它不包含符号。但上面生成的二进制文件会更有用,因为不论在什么环境下由谁使用,你都能使用符号调试。
请记住,不用重新编译就能进行调试是最理想的情况。一旦重新编译时,就已经改变了可执行文件在内存中的运行轨迹,还有可能改变了它的执行速度(向问题靠近的速度――译注)。这点在重现问题的时候是很致命的,试想,这个问题如果要 4 天时间才能出现岂不是很惨?所以最好是能够在问题现场就调试它。
简单的非法访问陷阱
我们现在来看一个简单的问题――程序因为非法访问而崩溃,这很常见。这是程序运行中发生几率很大的问题。要解决它需要明确以下 3 个问题:
1.  正在尝试访问非法内存的是哪个模块?
2.  被访问的内存从哪来的?
3.  为什么要访问它?
这是解决这类问题的一个指导原则。我在# 2 上使用斜体,是因为它是这里面最重要的一条。在# 2 不太明显的情况下,明确# 1 和# 3 也是很有帮助的。
我写了一个很简单的测试程序来产生上面所说的问题。我的默认调试器是 CDB _NT_SYMBOL_PATH 设置为 Microsoft symbol server
下面是程序运行后所看到的:
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 ,它距 _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中。下面就要看看这块内存具体出自哪个函数。
有很多方法可以达到目的,我们可以将代码反汇编,找到它的出处,也可以使用栈记录找到陷阱发生时栈上存在的函数。我们先使用第一种方法。
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:
77c3f10a53               push    ebx
77c3f10b 8a18             mov     bl,[eax]
如上图所示,我把重要的语句都高亮显示了。即使不懂汇编,你也应该很想知道这是什么意思。我们可以看到,那块内存是从 EAX 中来的。 EAX 是个 CPU 寄存器,但我们可以简单的将它看作变量。 [] C 中的 *MyPointer 相同,是取内容的意思。那 EAX 来自哪里呢?它来自 [EBP+0C] ,它等价于 DWORD *EBP EAX=EBP[3]; 这是因为汇编中没有类型一说, EAX 是一个 32 位寄存器 (DWROD), 这就意味这将一个 DWORD 指针加 3 (或者将指针加上 12 个字节然后转换为 DWORD )。
下面来看 MOV EBP,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]
然后再看 MOVE EBP,ESP ,我们可以将栈看作一个 DWORD 的数组,而 EBP ESP 是两个指向这个数组的指针。下面显示了 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 函数的第二个参数。 OK ,我们还知道, EBP+4 就是返回地址这样就可以定位到调用 _output 的函数并将其反汇编或者,我们也可以使用调用堆栈:
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 (或者是看上去如此)而且正是它调用了 _output 。我们来反汇编一下 printf 。有时候,仅从调用堆栈的情况就能找到陷阱的所在(我将在最后展示)。
Collapse
0:000> u MSVCRT!_output
MSVCRT!_output:
77c 3f 0f 3 55                push     ebp
77c 3f 0f 4 8bec             mov      ebp,esp
77c 3f 0f 6 81ec50020000     sub      esp,0x250
77c 3f 0fc 33c0             xor      eax,eax
77c 3f 0fe 8945d8           mov      [ebp-0x28],eax
77c 3f 101 8945f0           mov      [ebp-0x10],eax
77c 3f 104 8945ec           mov      [ebp-0x14],eax
77c 3f 107 8b450c           mov      eax,[ebp+0xc]
0:000> u
MSVCRT!_output+0x17:
77c 3f 10a 53                push     ebx
77c 3f 10b 8a18             mov      bl,[ eax ]
77c 3f 10d 33c9             xor      ecx,ecx
77c 3f 10f 84db             test     bl,bl
77c 3f 111 0f8445070000     je      MSVCRT!_output+0x769 (77c3
77c 3f 117 56                push     esi
77c 3f 118 57                push     edi
77c 3f 119 8bf8             mov      edi,eax
0:000> u MSVCRT!printf
MSVCRT!printf:
77c 3e658 6a10             push     0x10
77c 3e65a 68e046c177       push     0x77c146e0
77c 3e65f e8606effff       call     MSVCRT!_SEH_prolog (77c354
77c 3e664 bea0acc577       mov      esi,0x77c5aca0
77c 3e669 56                push     esi
77c 3e66a 6a01             push     0x1
77c 3e66c e8bdadffff       call     MSVCRT!_lock_file2 (77c394
77c 3e671 59                pop      ecx
0:000> u
MSVCRT!printf+0x1a:
77c 3e672 59                pop      ecx
77c 3e673 8365fc00         and     dword ptr [ebp-0x4],0x0
77c 3e677 56                push     esi
77c 3e678 e8c7140000       call     MSVCRT!_stbuf (77c3fb44)
77c 3e67d 8945e4           mov      [ebp-0x1c],eax
77c 3e680 8d450c           lea      eax,[ebp+0xc]
77c 3e683 50                push     eax
77c 3e684 ff7508           push     dword ptr [ebp+0x8]
0:000> u
MSVCRT!printf+0x2f:
77c 3e687 56                push     esi
77c 3e688 e8660a0000       call     MSVCRT!_output (77c3f0f3)
我们注意到 _output 的第二个参数是 [ebp+8] 。这里也用到了 PUSH EBP MOV EBP,ESP ,栈的建立方法和前面是一样的(但并不是所有情况下都是这样)。
因此,我们可以确定那块内存来自 printf 的第一个参数。幸运的是, printf 正在我们的代码中调用的。这样我们就找到了问题所在――试图传递一个 NULL 指针。
程序源代码如下:
int main(int argc, char *argv[])
  char *TheLastParameter[100];
 
 sprintf(*TheLastParameter, "The last parameter is %s", argv[argc]);
 printf(*TheLastParameter);
 
 return0;
 }
你可以从程序中看到很多问题。但我们要找的问题正是调用 Printf ,因为我们为它传递了一个 NULL 指针。 *TheLastParamter 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>
斜体部分是 printf 的第一个参数,它是 0 ,而这个函数是我们调用的。尽管这是一个非常简单的情况,我还是试图通过它向大家描绘一下通过回溯跟踪的整个过程,使大家了解关于堆栈的东西,知道堆栈是怎么建立的,堆栈上有些什么以及它们是从哪里来的。因为你不可能总是那么幸运的仅仅依靠 KB 就找到所有的信息。
程序无法正常运行
这也是非常普遍的问题――你的程序无法输出正确信息或一直报错,比如无法创建文件,等等。这类问题很普遍,解决起来也有难有易。一般来说,需要考虑一下 3 个问题:
1.  哪一部分不能工作了?
2.  这部分调用了那些 API 和模块?
3.  什么原因会使这些 API 失效?
这里,我们设计一个这样的场景:我们试图写一个文件,但这个文件却无法打开。代码如下:
 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() 函数中设置了一个断点,然后继续执行。程序会停在刚才设置的断点处,然后键入“ 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:
0040100051               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:
0040100156               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:
0040100257               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:
0040100557               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:
0040100657               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:
0040100957               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:
0040100a57               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:
004010106810304000       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 calldwordptr [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 里面保存了返回值,可以看到 EAX 的值是 0xffffffff ,也就是我们常用的“ Invalid Handle Value ”。而 GetLastError 的返回值会被保存在 fs:34 处。可以将它显示出来:
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>
OK ,终于看到失败的原因了,无法找到文件。但文件明明是有的啊,到底出了什么问题?我们接着来。我们要查看一下 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:
004010106810304000       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 calldwordptr [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>
看到了吧,我们漏掉了“ // ”。于是我们很容易就能修正这个错误。但即便如此, write 函数还是不能用,我们需要进一步调试,重复上面的操作:
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: 0040000000404000   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:
0040100051               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:
0040100156               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:
0040100257               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:
0040100557               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:
0040100657               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:
0040100957               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:
0040100a57               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:
004010106810304000       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 calldwordptr [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)            

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:
0040102657               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:
0040102750               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:
0040102a6820304000       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:
0040102f56               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 calldwordptr [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:
0040103656               push    esi
看最后一行,我们调用了 WriteFile ,但 EAX 0 ,调用失败!再来看看其它变量。第二个参数是正确的,它的长度是 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>
拒绝访问!什么原因导致的?看看我们的代码吧,我们是用 READ 权限打开的文件,根本没有 WRITE 权限。于是,这个问题也很容易解决了:
hFile = CreateFile("c://MyFile.txt", GENERIC_READ, 
        0, NULL, OPEN_EXISTING, 0, NULL);
总结
这一章只是对基本调试技术做了一个简单的介绍。例子也很简单,但它们却展示了很有价值的技术。这只是第一部分,希望大家对此感兴趣,接下来我会写一些更深入的东西。
对一些人来说,这篇文章可能太简单了,对另外一部分人可能太难了。我们不可能一夜成为调试高手,这需要很多很多的练习。所以建议大家坚持使用调试器调试,即使在最简单的问题上也是一样。我敢保证一点,你被调试工具折磨和戏耍的越多,你能学到的就越多。
 
 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值