65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用和理解 Windows 控制台调试器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (5投票s)

2008年12月23日

CPOL

6分钟阅读

viewsIcon

48529

帮助初学者开始调试的文章

引言

为了解析cdb.exe调试器提示符,我们需要输入命令来弄清楚调试器输出的一些细节。此外,当我们设置符号路径时,我们必须记住下载的符号仅适用于 Microsoft 设备驱动程序和系统关键组件。这意味着如果我们要在自己的代码上使用cdb.exe(或图形化WinDbg.exe)调试器,我们必须使用一个编译器开关,将调试信息包含在编译后的可执行文件中。在此前提下,我们可以用 C 语言编写一个小程序来解析调试器。下面是一个简单的程序,它编写了一个全局函数,该函数由 main 函数调用,以便在标准控制台输出中生成一个 string

c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>type con > hello.c
#include <stdio.h>  
#include <string.h>
salute(char *temp1,char *temp2){ 
char name[400]; 
strcpy(name, temp2); 
printf("Hello %s %s\n", temp1, name); 
}
main(int argc, char * argv[]){ 
salute(argv[1], argv[2]); 
printf("Bye %s %s\n", argv[1], argv[2]); 
} 

现在我们使用 Microsoft Visual Studio C++ Express Edition 的cl.exe编译器,并带/Zi开关来编译这段代码,以获取调试信息。

C:\PROGRA~1\MICROS~1.0\VC\bin>cl /Zi hello.c
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

hello.c
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:hello.exe
/debug
hello.obj

我们运行程序:C:\....\bin>hello Silly Willy

c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>hello Silly Willy

Hello Silly Willy

Bye Silly Willy

运行该程序后,我们需要找到一种简单的方法将此代码及其调试信息传输到C:\Program files\Debugging Tools forWindows目录。当然,也可以设置一个环境变量。

set PATH=%PATH%;.;C:\Program Files\Microsoft Visual Studio 9.0\VC\bin

Capture.JPG

使用更基本的方法,我们将这些文件复制并粘贴到包含调试器的目录中,在那里我们将设置符号路径以下载符号并将它们本地缓存到一个名为C:\symbols的目录中。

符号和符号服务器

符号将函数名和参数与编译后的可执行文件中的偏移量连接起来。获取符号的一种方法是使用 Microsoft 的符号服务器,并按需获取符号。Windows 调试器通过提供symsrv.dll使此操作变得容易,您可以使用它来设置本地符号缓存,并指定按需获取新符号的位置。这可以通过环境变量_NT_SYMBOL_PATH来完成。您需要设置此环境变量,以便调试器知道在哪里查找符号。如果您已经拥有本地所需的所有符号,您可以像这样简单地将变量设置为该目录:

C:\Program Files\Debugging Tools For Windows>set _NT_SYMBOL_PATH=c:\symbols 

如果您更喜欢使用符号服务器,语法如下:

C:\Program Files\Debugging Tools For Windows>
set_NT_SYMBOL_PATH=symsrv*symsrv.dll*c:\symbols*
http://msdl.microsoft.com/download/symbols

用户模式调试器输出

C:\PROGRA~1\Debugging Tools for Windows>cdb.exe Hello Silly Willy

Microsoft (R) Windows Debugger Version 6.8.0004.0 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: Hello Silly Willy
Symbol search path is: srv*c:\Symbols*http://msdl.microsoft.com/download/symbols

Executable search path is:
ModLoad: 00400000 00427000   hello.exe
ModLoad: 76ea0000 76fc7000   ntdll.dll
ModLoad: 75a90000 75b6b000   C:\Windows\system32\kernel32.dll
(27c.ff4): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0012fb08 edx=76ef9a94 esi=fffffffe edi=76efb6f8
eip=76ee7dfe esp=0012fb20 ebp=0012fb50 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!DbgBreakPoint:
76ee7dfe cc              int     3

请注意,第一行包含生成最后一个调试器事件的进程和线程标识符。对于调试的整体优秀文档,强烈建议读者研究 Mario Hewardt 和 Daniel Pravat 的《高级 Windows 调试》(Advanced Windows Debugging)。最后一个调试器事件显示了标识符 (27c.ff4) 以及事件描述、一个断点指令和一个异常代码 80000003。调试器在第一次机会时处理了该事件,在用户代码中的正常异常处理之前。接下来的两个命令是不言自明的。

0:000> vertarget
Windows Version 6001 (Service Pack 1) MP (2 procs) Free x86 compatible
Product: WinNt, suite: SingleUserTS
kernel32.dll version: 6.0.6001.18000 (longhorn_rtm.080118-1840)
Debug session time: Tue Dec 23 16:53:26.860 2008 (GMT-5)
System Uptime: 0 days 1:48:23.592
Process Uptime: 0 days 0:00:06.037
  Kernel time: 0 days 0:00:00.000
  User time: 0 days 0:00:00.015


0:000> .lastevent
Last event: 6f8.5a0: Break instruction exception - code 80000003 (first chance)
  debugger time: Tue Dec 23 16:53:20.854 2008 (GMT-5)

正如我们所见,调试器在断点处停止。堆栈跟踪将显示原因。

0:000> k
ChildEBP RetAddr
0012fb1c 76f2e214 ntdll!DbgBreakPoint
0012fb50 76f12ef5 ntdll!LdrpDoDebuggerBreak+0x31
0012fc94 76ed1235 ntdll!LdrpInitializeProcess+0x1132
0012fd00 76ede2b7 ntdll!_LdrpInitialize+0xf2
0012fd10 00000000 ntdll!LdrInitializeThunk+0x10

查看顶部指令,我们可以看到 Windows 调试器在初始化之后、执行开始之前中断。这实际上是一个方便的断点,因为程序已加载;现在我们可以在程序执行开始之前设置任何我们想要的断点。让我们在main上设置一个断点。

0:000> bm hello!main
*** WARNING: Unable to verify checksum for hello.exe
  1: 00401070 @!"hello!main"


0:000> bl
 1 e 00401070     0001 (0001)  0:**** hello!main

现在要做的是让我们的程序运行到ntdll.dll初始化之后,到达我们的main 函数。

0:000> g
Breakpoint 1 hit
eax=008f1d78 ebx=7ffda000 ecx=00000001 edx=76ef9a94 esi=00000000 edi=00000000
eip=00401070 esp=0012ff44 ebp=0012ff88 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
hello!main:
00401070 55              push    ebp
0:000> k
ChildEBP RetAddr
0012ff40 004014b2 hello!main
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b

在任何反汇编过程中,数字 55 通常表示程序入口。也就是说,操作码 55 表示源代码执行开始的位置。如果您曾经处理过 Linux 或汇编,您会认识到指令push ebp 是所谓的函数序言的开始。此时,应注意 Windows 调试工具。如果您阅读了debugging.chm中“源代码模式调试”部分,您会发现.lines 命令将修改堆栈跟踪以显示当前正在执行的行。

0:000> .lines
ne number information will be loaded
0:000> k
ChildEBP RetAddr
0012ff40 004014b2 hello!main [c:\program files\Microsoft visual studio 9.0\vc\bi
n\hello.c @ 8]
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb [f:\dd\vctools\crt_bld\self_x86\c
rt\src\crt0.c @ 266]
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> g

要继续执行此断点,我们的程序将完成执行。

0:000> g
Hello Silly Willy
Bye Silly Willy
eax=00000000 ebx=00000001 ecx=00000000 edx=000000fe esi=008f1a34 edi=008f1a38
eip=76ef9a94 esp=0012fec0 ebp=0012fed0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!KiFastSystemCallRet:
76ef9a94 c3              ret

0:000> k
ChildEBP RetAddr
0012febc 76ef9134 ntdll!KiFastSystemCallRet
0012fec0 76eca869 ntdll!NtTerminateProcess+0xc
0012fed0 75ab3b68 ntdll!RtlExitUserProcess+0x7a
0012fee4 00402cd8 kernel32!ExitProcess+0x12
0012fee4 00402cd8 hello!__crtExitProcess+0x17 [f:\dd\vctools\crt_bld\self_x86\cr
t\src\crt0dat.c @ 731]
0012fef0 00402f3c hello!__crtExitProcess+0x17 [f:\dd\vctools\crt_bld\self_x86\cr
t\src\crt0dat.c @ 731]
0012ff34 00402f66 hello!doexit+0x113 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt
0dat.c @ 644]
0012ff48 004014c4 hello!exit+0x11 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0da
t.c @ 412]
0012ff88 75ad4911 hello!__tmainCRTStartup+0x10d [f:\dd\vctools\crt_bld\self_x86\
crt\src\crt0.c @ 272]
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> q
quit:

检查调试器

此时的一个好练习是定位被调试应用程序正在使用的数据。为此,我们将启动调试器并在main 函数和salute 函数上设置断点。

C:\PROGRA~1\Debugging Tools for Windows>cdb.exe Hello Silly Willy

Microsoft (R) Windows Debugger Version 6.8.0004.0 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: Hello Silly Willy
Symbol search path is: srv*c:\Symbols*http://msdl.microsoft.com/download/symbols

Executable search path is:
ModLoad: 00400000 00427000   hello.exe
ModLoad: 76ea0000 76fc7000   ntdll.dll
ModLoad: 75a90000 75b6b000   C:\Windows\system32\kernel32.dll
(564.e0c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0012fb08 edx=76ef9a94 esi=fffffffe edi=76efb6f8
eip=76ee7dfe esp=0012fb20 ebp=0012fb50 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!DbgBreakPoint:
76ee7dfe cc              int     3
0:000> k
ChildEBP RetAddr
0012fb1c 76f2e214 ntdll!DbgBreakPoint
0012fb50 76f12ef5 ntdll!LdrpDoDebuggerBreak+0x31
0012fc94 76ed1235 ntdll!LdrpInitializeProcess+0x1132
0012fd00 76ede2b7 ntdll!_LdrpInitialize+0xf2
0012fd10 00000000 ntdll!LdrInitializeThunk+0x10
0:000> bm hello!main
*** WARNING: Unable to verify checksum for hello.exe
  1: 00401070 @!"hello!main"
0:000> bm hello!*salute*
  2: 00401020 @!"hello!salute"

在 C 语言中,(int argc, char *argv[]) 是命令行参数,它们充当原型以传递给main 来启动程序。Int argc 是参数的数量,而argv[] 是指向这些参数的指针的向量。也就是说,我们不是通过环境来调用程序,而是将这些变量传递给main 来启动我们的程序。因此,Argv[0]将是程序名,即hello.exe

0:000> g
Breakpoint 1 hit
eax=001c1d78 ebx=7ffdb000 ecx=00000001 edx=76ef9a94 esi=00000000 edi=00000000
eip=00401070 esp=0012ff44 ebp=0012ff88 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
hello!main:
00401070 55              push    ebp

查看源代码,我们可以确定main 应该通过argc 命令字符串计数器和argv(指向字符串数组)接收了用于启动程序的命令行。为了验证这一点,我们将使用dv 列出局部变量,然后使用dt db 在内存中进行探测以查找这些变量的值。

0:000> dv /V
0012ff48 @ebp+0x08            argc = 3
0012ff4c @ebp+0x0c            argv = 0x001c1d38
0:000> dt argv
Local var @ 0x12ff4c Type char**
0x001c1d38
 -> 0x001c1d48  "Hello"

dv 输出中,我们看到argc argv 确实是局部变量,argc 存储在局部ebp之后 8 个字节,而argv 存储在ebp+0xc处。dt 命令显示argv 的数据类型是指向字符指针的指针。地址0x001c1d48 保存指向0x001c1d48 的指针,该地址是数据实际所在的位置。

0:000> db 0x001c1d48
001c1d48  48 65 6c 6c 6f 00 53 69-6c 6c 79 00 57 69 6c 6c 79 Hello.Silly.Willy

所以我们继续前进,直到我们到达salute 函数。

0:000> g
Breakpoint 2 hit
eax=001c1d4e ebx=7ffdb000 ecx=001c1d54 edx=001c1d38 esi=00000000 edi=00000000
eip=00401020 esp=0012ff34 ebp=0012ff40 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
hello!salute:
00401020 55              push    ebp
0:000> kp
ChildEBP RetAddr
0012ff30 00401086 hello!salute(char * temp1 = 0x001c1d4e "Silly", char * temp2 =
 0x001c1d54 "Willy")
0012ff40 004014b2 hello!main(int argc = 3, char ** argv = 0x001c1d38)+0x16
0012ff88 75ad4911 hello!__tmainCRTStartup(void)+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b

查看堆栈跟踪(或代码),salute 函数接收两个参数:Silly Willy。因此,如果一个函数接收两个参数,那么它们应该出现在堆栈上,因为在调用时,它必须将其参数压入堆栈。这意味着我们查看局部变量并映射它们。

0:000> dv /V
0012ff38 @ebp+0x08           temp1 = 0x001c1d4e "Silly"
0012ff3c @ebp+0x0c           temp2 = 0x001c1d54 "Willy"
0012fd98 @ebp-0x198            name = char [400] " s"

变量名是`ebp`以上 0x198 。要将此十六进制转换为十进制,请使用.formats 命令。

.0:000> .formats 198
Evaluate expression:
  Hex:     00000198
  Decimal: 408
  Octal:   00000000630
  Binary:  00000000 00000000 00000001 10011000
  Chars:   ....
  Time:    Wed Dec 31 19:06:48 1969
  Float:   low 5.7173e-043 high 0
  Double:  2.01579e-321

0:000> pr
hello!salute+0xe:
0040102e 33c5            mov     ebp,esp
0:000> p
hello!salute+0x10:
00401030 8945fc          sub esp, 198h

0:000> pr
eax=0020201e ebx=7ffdf000 ecx=00202024 edx=00202008 esi=00000000 edi=00000000
eip=00401029 esp=0012fd98 ebp=0012ff30 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
hello!salute+0x9:
00401029 a120204200      mov     eax,dword ptr [hello!__security_cookie (0042202
0)] ds:0023:00422020=ec0c1ced

我们在堆栈顶部(esp,或上面“dv /V”命令中“name”变量所在位置的地址 `0012fd98`)找到函数变量名,它会占用接下来的 408 个字节。因此,我们将这些字节添加到堆栈指针中。

0:000> .formats esp+198
Evaluate expression:
  Hex:     0012ff30
  Decimal: 1244976
  Octal:   00004577460
  Binary:  00000000 00010010 11111111 00110000
  Chars:   ...0
  Time:    Thu Jan 15 04:49:36 1970
  Float:   low 1.74458e-039 high 0
  Double:  6.151e-318

注意值0012ff30。这个值应该看起来很熟悉,因为它是“push”到堆栈上并初始保存的ebp的值。我们知道寄存器ebp 是一个 32 位或 4 字节指针。所以让我们看看后面是什么。

0:000> dd esp+198+4 l1
0012ff34  00401086

命令l1代表长度为一。现在如果我们做一个堆栈跟踪,返回地址应该匹配:返回地址00401086 匹配,表示堆栈已重新构建。

ChildEBP RetAddr
0012ff30 00401086 hello!salute+0x13
0012ff40 004014b2 hello!main+0x16
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b

0:000> dd esp+198+4+4 l1
0012ff38  001c1d4e

0:000> db 001c1d4e
001c1d4e  53 69 6c 6c 79 00 57 69-6c 6c 79 00 ab ab ab ab  Silly.Willy.....

使用 Cdb.exe 反汇编

要进行反汇编,只需在一个断点处使用uf 指令,例如全局函数。

0:000> uf hello!salute
hello!salute:
00401020 55              push    ebp
00401021 8bec            mov     ebp,esp
00401023 81ec98010000    sub     esp,198h
00401029 a120204200      mov     eax,dword ptr [hello!__security_cookie (0042202
0)]
0040102e 33c5            xor     eax,ebp
00401030 8945fc          mov     dword ptr [ebp-4],eax
00401033 8b450c          mov     eax,dword ptr [ebp+0Ch]
00401036 50              push    eax
00401037 8d8d68feffff    lea     ecx,[ebp-198h]
0040103d 51              push    ecx
0040103e e8fd010000      call    hello!strcpy (00401240)
00401043 83c408          add     esp,8
00401046 8d9568feffff    lea     edx,[ebp-198h]
0040104c 52              push    edx
0040104d 8b4508          mov     eax,dword ptr [ebp+8]
00401050 50              push    eax
00401051 6800204200      push    offset hello!__rtc_tzz  (hello+0x22000) (
00422000)
00401056 e86f000000      call    hello!printf (004010ca)
0040105b 83c40c          add     esp,0Ch
0040105e 8b4dfc          mov     ecx,dword ptr [ebp-4]
00401061 33cd            xor     ecx,ebp
00401063 e8d0020000      call    hello!__security_check_cookie (00401338)
00401068 8be5            mov     esp,ebp
0040106a 5d              pop     ebp
0040106b c3              ret

希望本文能帮助那些在调试以及获取被调试编译源代码应用程序所使用的过程中遇到的困难。

参考文献

  • 《高级 Windows 调试》,作者:Mario Hewardt 和 Daniel Pravat
  • 《道德黑客》,作者:Shon Harris、Allen Harper、Chris Eagle 和 Jonathan Ness

历史

  • 2008 年 12 月 23 日:初次发布
© . All rights reserved.