Windows 调试工具入门-2-基本调试操作

一、  调试器命令窗口


1、 简介 
    使用Windows 调试工具进行调试,大部分和调试器之间的交互都是通过调试器命令
窗口来进行的。命令的输入、输出都是在调试器命令窗口中显示出来。对WinDbg 来说,
调试器命令窗口是名为”Command”的窗口;对于KD、CDB 和NTSD 来说,整个命令行窗
口就是调试器命令窗口。这里主要介绍WinDbg 中的调试器命令窗口。 
     一般来说WinDbg 运行之后都会打开一个标题为Command 的子窗口,在没有调试目
标的时候,这个窗口是不能接受输入输出的,这时WinDbg 处于静止模式,只有在打开
调试目标之后,才能够使用它和调试器交互。

窗口分为三个部分: 
  位于上部的面积最大的是命令输出窗口。所有的命令输出、目标程序的调试信
息输出等等都会在里面显示出来。上一篇中介绍的调试器日志中记录的就是显
示在这里的内容。 
  下半部分左边是提示符窗口。这里通过提示符能够快速知道调试器目前的状态。 
上图中0:000>,冒号前的数字表示当前的进程号,同时调试多个进程时,每个
进程都会被指派一个进程号;冒号后的000 表示线程号。 
进行内核调试时,如果是单处理器系统,提示符是kd>的形式;如果是多处理
器系统,则是0: kd>的形式,前面的0 表示处理器号。

提示符还可能是*BUSY*这样的字符串,以表示调试器正忙。也可以通过命令来
自定义提示符。 
  下半部分右边是命令输入窗口。需要执行的命令就在这里输入。 
调试器命令窗口中输入命令时可以使用一些快捷操作: 
  上下方向键可以查找先前的命令。 
  ESC 键用于清除当前行的命令。 
  TAB 键用于自动补完命令。例如一些符号可以只输入一部分,然后通过按下TAB
一次或多次来找到需要的符号。 
  鼠标右键点击命令窗口,可以将剪贴板中的内容粘贴到命令输入框中。 
  直接按下ENTER 键重复上一条命令。这个功能在WinDbg 中可以通过命令来打
开或关闭。 
  如果某条命令产生了很长的输出,可以按下CTRL+BREAK 来中断它。

 

二、  控制调试目标的执行 
这里的控制目标执行,主要是指如何让运行中的目标中断到调试器中,以及控制中断
的目标如何继续执行。 
1.  中断调试目标 
当调试目标处于运行状态时,WinDbg 是不能输入命令或者对它进行操作的。可以通过
按下CTRL+BREAK 或者  点击工具栏的 按钮来中断它。下面我们继续用上一篇中的
TestDebug1 项目来说明。修改TestDebug1.cpp 如下

 

[csharp] view plain copy

  1. #include "stdafx.h"   
  2. #include <stdio.h>   
  3. int main(int argc, char* argv[])   
  4. {   
  5.   int i = 0;   
  6.   while( 1)   
  7.   {   
  8.     printf( "TestDebug1.cpp:%d\r\n", i);   
  9.   }   
  10.   return 0;   
  11. }   

为了方便,这次使用Debug 选项来重新编译它,这样就不用再设置编译选项和WinDbg
选项来查看符号了。使用WinDbg 菜单的File->Open Executable…打开TestDebug1.exe,中断
下来之后F5 继续运行。由于是个死循环,所以目标不会自己停止下来,可以看到WinDbg

的调试器命令窗口一直处于禁用状态。在WinDbg 窗口按下CTRL+BREAK,TestDebug1.exe
就中断到调试器中了,使用u 命令查看当前正在执行的代码,k 命令查看当前调用堆栈:

看调用堆栈和反汇编出来的代码,似乎和TestDebug1.cpp 中的代码没有任何关系,这
是为什么呢? 
注意到底部提示符位置显示的是0:001>,说明这是1 号线程,而正常情况下线程编号
都是从0 开始的。我们继续用~命令来查看被调试进程中的线程信息,出现的是类似这样的
输出:

[csharp] view plain copy

  1. 0:001> ~   
  2.     0    Id: 1998.1358 Suspend: 1 Teb: 7ffde000 Unfrozen   
  3. .    1    Id: 1998.17f8 Suspend: 1 Teb: 7ffdd000 Unfrozen  

每一行是一个线程的信息。第一行中,0 表示这个进程的编号;1998.1358 是16 进制数
字,前者是当前进程的进程ID,后者是线程ID;后面的信息是线程状态和Teb 地址。第二
行的线程编号前有一个点号“.”,表示这是当前线程,也就是刚才使用u 和k 命令查看到
的线程。   
我们的代码中并没有任何创建线程的操作,为什么会多出一个线程来呢?这是由于
WinDbg 中断运行中的调试目标的方式造成的。按下CTRL+BREAK 之后,WinDbg 会在调试目
标的进程中创建一个远线程,并在这个远线程中执行ntdll!DbgBreakPoint 函数,即上面u
命令所显示出来的内容。它会在目标进程中产生一次int3 异常,这个异常被WinDbg 捕获,
所以TestDebug1.exe 就中断到调试器中了。因此,当采用CTRL+BREAK 这种方式中断目标之
后,看到的代码是在这个远线程中的,如果要查看调试目标正在执行的代码就需要切换当
前线程。可以使用~Thread s 命令。如下:

这里就可以清楚看到在main 函数中的print 调用产生的调用堆栈了。 
除了采用CTRL+BREAK 这样直接中断运行中目标的方式之外,当调试目标发生异常、退
出或者遭遇断点等事件时,也会自动中断到调试器中。这时就不会出现额外的线程了。内
核调试时中断目标机的操作和用户模式下一样


2.  控制目标的执行 
调试目标中断之后,就可以通过单步或者跟踪指令来控制它执行了。 
WinDbg 中的单步操作快捷键和Visual Studio 调试器中相同。也是F5 运行、F10 逐过程
单步、F11 逐语句单步。需要注意的是,单步的定义在汇编模式调试和源码模式调试时是不
一样的。汇编模式调试时,每次单步执行一条指令;源码模式调试时,每次单步执行一行
源码。点击工具栏上的 按钮或使用l-t 命令来启用汇编模式;点击工具栏上的 或使
用l+t 命令来启用源码模式。 
控制目标执行的命令分为三大类。g*类的命令用于直接运行目标、p*类的命令用于单
步执行、t*类的命令类似p*命令,但是当遇到call 指令时会跟踪进去。下面是这些命令的
列表,摘自WinDbg 帮助文档:


 

 

三、  使用断点 
     合理、巧妙的设置断点是软件调试中的一门艺术,好的断点能使调试工作事办功倍。
WinDbg 中提供了丰富的断点命令,下面通过示例对这些命令进行简单的介绍。 
      在上面的项目中,添加了一个dll 项目,名为TestDebugDll1。修改一下上面的
TestDebug1.cpp 如下(整个项目可以下载附件):

[csharp] view plain copy

  1. #include "stdafx.h"   
  2. #include <stdio.h>   
  3. #include <windows.h>   
  4.    
  5. class CTestClass   
  6. {   
  7. public:   
  8.   CTestClass(){};   
  9.   ~CTestClass(){};   
  10.   void SetChar( unsigned char ucChar)   
  11.   {   
  12.     m_ucTestChar = ucChar;   
  13.   }   
  14. protected:   
  15.   unsigned char m_ucTestChar  

[csharp] view plain copy

  1. };   
  2.    
  3. int main(int argc, char* argv[])   
  4. {   
  5.   typedef int (*pfnTestDllAdd)( int a, int b);   
  6.    
  7.   int i;   
  8.   HMODULE hMod = LoadLibraryA( "TestDebugDll1.dll");   
  9.   pfnTestDllAdd TestDllAdd = (pfnTestDllAdd)::GetProcAddress( hMod, "TestDllAdd");   
  10.   if ( TestDllAdd)   
  11.   {   
  12.     i = TestDllAdd( 1, 2);   
  13.   }   
  14.    
  15.   CTestClass objTestClass;   
  16.   objTestClass.SetChar( 123);   
  17.   return 0;   
  18. }  


    还是使用Debug 选项,重新编译。用WinDbg 打开TestDebug1.exe 后会自动中断到初
始断点。由于是Debug 选项编译的,所以这里可以省去符号路径的设置就能识别符号。 
     bp 命令是最常用的断点命令之一,它可以直接对某个代码地址设置断点。例如我们想
中断到main 函数,可以这样:

[csharp] view plain copy

  1. 0:000> bp TestDebug1!main  

     前面的TestDebug1 明确指定main 符号所在的模块,这样通常可以减少搜索符号的时
间,也避免了相同名字的符号可能造成的冲突。F5 运行,就发现已经中断到main 函数了,
并且源码窗口会自动弹出来。 
    在源码窗口或者返汇编窗口中,可以将光标移动到要设置断点的行并用F9 快捷键来设
置断点。这和Visual Studio 中一样。现在我们在HMODULE hMod = 
LoadLibraryA( "TestDebugDll1.dll");
这一行处按下F9  设置一个断点。可以看到源码窗口中将
当前正中断到的断点和未触发的断点用不同的颜色标识出来:

bl 命令用于查看已存在的断点

[csharp] view plain copy

  1. 0:000> bl   
  2.   0 e 00401030 [C:\Users\NetRoc\Desktop\TestDebug1\TestDebug1.cpp @ 23]        0001   
  3. (0001)    0:**** TestDebug1!main   
  4.   1 e 0040105d [C:\Users\NetRoc\Desktop\TestDebug1\TestDebug1.cpp @ 27]  


        如上面命令输出中的第二行,1 表示断点ID。当使用bd 命令禁用断点、be 命令重新启
用断点或者其他命令来操作这个断点时,都需要用到这个ID;第二个“e”表示断点是启用
的,如果是“d”则表示当前被禁用,如果带“u”则说明是后面将要介绍的未定断点;第
三列的0040105d 是该断点的地址;后面的内容是断点所在的源文件和行号。 
      有时候我们想要设置断点的模块还没有被加载到内存中,如这个例子中的
TestDebugDll1.dll,只有在调用了LoadLibrary 之后才会加载进来。如果使用bp 来对这个模
块中的函数设置断点,会找不到符号,这时就会被调试器自动转变成用bu 命令来设置的未
定断点。bu 可以对还不能识别的符号设置断点,当系统中有新模块加载进来时,调试器会

对未定断点再次进行识别,如果找到了匹配的符号则会设置它。现在我们首先用bc 命令删
除上面的1 号断点,然后用bu TestDebugDll1!TestDllAdd 命令对TestDebugDll1.exe 中的
TestDllAdd 函数设置未定断点,结果如下:

 

     第一个bl 命令可以看到我们之前设置的两个断点,然后bc 命令将1 号断点删除。接下
来使用了一次bp 命令,系统提示找不到TestDebugDll1!TestDllAdd,将断点自动转换成未定
断点。第三次,使用bu 命令对TestDebugDll1!TestDllAdd 成功设置了未定断点。最后查看存
在的断点有三个。0 号是最开始的断点,1 号是bp 命令失败后WinDbg 自动转换的断点,2
号是bu 命令设置的。 
    接下来的程序会加载TestDebugDll1.dll 并调用TestDllAdd 函数,我们F5 继续:


     调试器自动打开了TestDebugDll1.dll 的源文件,并且发现中断在TestDllAdd 函数开头。

 
     下面我们再试验一下对类成员函数下断和内存访问断点。继续上面的调试会话,源码
中有一个类成员函数CTestClass:: SetChar(),可以直接使用符号对它设置断点。下面几条命
令等效:

[csharp] view plain copy

  1. bp TestDebug1.exe!CTestClass::SetChar   
  2. bp TestDebug1.exe!CTestClass__SetChar   
  3. bp @@C++(TestDebug1.exe!CTestClass::SetChar)   


     Windows 调试工具支持两种语法的表达式:MASM 语法和C++语法。如果没有特别指明
的话,默认是使用MASM 表达式语法。一般来说,MASM 语法的表达式用来表示地址比较
方便,而C++表达式用来表示结构或者类成员比较方便。可以通过@@C++(…)或者
@@masm(…)来包含表达式以明确指明所使用的语法。当使用MASM 语法时,可以用双冒
号(::)或者双下划线(__)来表示类成员;但是使用C++语法时则只能使用双冒号。


     用上面的命令之一对CTestClass::SetChar 设置断点并F5 运行,可以看到成功中断到了
CTestClass::SetChar 函数处。

 

 

     ba 命令用于设置访问断点。访问断点可以在某个内存地址处的数据被读取、写入或者
执行的时候中断下来。首先用.restart 命令重新启动调试目标,并且用前面的方法之一中断
到源代码中HMODULE hMod = LoadLibraryA( "TestDebugDll1.dll");这一行处。我们看到后面的
代码对局部变量i 有赋值操作。我们继续试着使用C++语法来使用命令,输入ba w4 
@@C++(&i)命令。“&i”在C++语法中表示变量i 的地址,“w”表示写入操作,“4”表示
只处理&i 地址处4 字节的写入操作。F5 运行,程序被成功中断下来:

 

     输出中有几处值得注意的地方。第一个bl 可以看到,之前已经存在了一个ID 为1 的断
点,然后我们又使用ba 设置了一个断点。在第二次bl 输出重可以看到新加的断点ID 为0、
“w”表示是一个写断点、“4”表示写入的数据长度、要监控的内存地址为“0012ff38”。
G 命令之后,0 号断点被触发,也就是刚才设置的数据断点。但是下面显示的当前指令却没
有访问到我们设置断点的0x0012ff38。这里又涉及到WinDbg 数据断点实现的原理。来通过
VC 的窗口看一看相关代码和对应的汇编代码:

 

 

      图中的mov dword ptr [ebp-10h],eax 才是对i 赋值。但是断点触发后却中断到了赋值之
后的下一条指令。 
    WinDbg 的数据断点是通过CPU 硬件断点实现的。而DRx 寄存器所设置的内存访问断点
属于陷阱(Trap)而不是错误(Fault),CPU 对陷阱的处理是执行完该条指令后触发异常。因此
WinDbg 只能在之后的一条指令处断下来。 
    ba 命令支持的断点种类有以下几个:

 

     e 选项所指定的数据长度必须是1,即只能指定e1。r/w 选项支持1、2、4 的数据长度,
在X64 机器上可以支持8。 
      断点命令中可以设置一条或多条命令,当断点被触发时会自动执行它。接着上面的调
试会话,使用下面的命令

 

 

       这里使用了bp CTestClass::SetChar “.echo This is the test string”命令。.echo 是调试器命
令的关键字,用于向调试器命令窗口输出一串字符串。这个命令的结果就是,在
CTestClass:SetChar 成员函数设置断点,并且在中断的时候执行.echo This is the test string 命
令。可以看到,g 命令重新运行程序之后,断点触发时调试器命令窗口中出现了这个字符串。 
      WinDbg 的条件断点也是采用这种方式的。通过“命令的命令”配合.if 这样的命令关键
字,就可以实现灵活多样的条件断点。

 

四、  访问内存和寄存器 
WinDbg 可以通过命令或者GUI 界面来访问内存和寄存器。常用的几条命令如下: 
  以d 开头的d*系列命令用于查看内存值。命令的第二个字符用于指定按何种数据
类型查看该内存中的数据,如db 是按BYTE 类型查看,dd 是按DWORD 类型查看。

 

 

     重新中断到TestDebug1.exe 的main 函数处。用db 400000 命令查看PE 文件头的内
容,在右边会自动列出对应的ASCII 字符。直接使用d 命令会按照上一次d*命令的方
式来查看。如果不带地址参数,则从上一次显示结束的地方继续显示。 
  ?表达式求值命令常常用来查看符号所代表的值。 
  e*命令可以将值写入内存。命令第二个字符的定义和d*一样,用于指定数据类型。
可以用一条命令按照顺序向指定地址写入多个值。

 

 

      首先使用? i 命令,它可以显示符号i 对应的值,即局部变量i 的地址。命令输出的
等号两边分别是10 进制数字和16 进制数字。然后使用db 0012ff78 查看变量i 处的内
存内容,目前的值是0x0012ffc4。eb 0012ff78 'a' 'b' 'c' 'd'命令会在从0012ff78 开始的地
址处依次写入后面的数值,命令执行时WinDbg 会像C/C++一样自动将单引号中的ASCII
字符转换为数字。最后,再通过db 命令查看内存,可以看到刚才的“abcd”已经写入
了。 
  r 命令用于查看或者修改寄存器和伪寄存器。Windows 调试工具定义了一些伪寄存
器,他们不是机器上实际的寄存器,而是根据调试环境不同自动变化的值。详细
可以查看帮助文档中的伪寄存器语法。 
  dt 命令用于查看结构。参考下面的命令序列:

 

     首先用上一篇中介绍过的.symfix 和.reload 命令加载Windows 符号,$peb 是一个伪
寄存器,调试器将它定义为当前进程的进程环境块地址。使用?或者r 命令都能看到它
的内容。进程环境块是一个nt!_PEB 结构,所以可以用dt 来显示出当前进程的PEB 内
容。 
  !address 扩展命令可以显示指定的内存地址的信息。接着上面的调试会话,对PE
文件头使用!address 看看:

[csharp] view plain copy

  1. 0:000> !address 400000   
  2.   ProcessParametrs 002c14f0 in range 002c0000 002c4000   
  3.   Environment 002c0808 in range 002c0000 002c4000   
  4.      00400000 : 00400000 - 00001000   
  5.                      Type          01000000 MEM_IMAGE   
  6.                      Protect    00000002 PAGE_READONLY   
  7.                      State        00001000 MEM_COMMIT   
  8.                      Usage        RegionUsageImage  

[csharp] view plain copy

  1. FullPath TestDebug1.exe   


     这里可以看到指定的地址0x400000 的内存类型、保护属性、拥有该地址的模块等
等。 
  dv 命令可以查看当前作用域下局部变量的类型和值:

[csharp] view plain copy

  1. 0:000> dv   
  2.             argc = 2147328000   
  3. Type information missing error for argv   
  4. Type information missing error for objTestClass   
  5.                i = 1684234849   
  6. Type information missing error for TestDllAdd   
  7. Type information missing error for hMod  


 

    main 函数有些局部变量没有类型信息,这是因为VC6 中默认的Debug 选项编译出
来之后,.pdb 文件中符号信息并不完全。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
包含以下工具 accesschk.exe accesschk64.exe AccessEnum.exe AdExplorer.chm ADExplorer.exe ADInsight.chm ADInsight.exe adrestore.exe Autologon.exe autoruns.chm Autoruns.exe Autoruns64.exe autorunsc.exe autorunsc64.exe Bginfo.exe Cacheset.exe Clockres.exe Clockres64.exe Contig.exe Contig64.exe Coreinfo.exe ctrl2cap.amd.sys ctrl2cap.exe ctrl2cap.nt4.sys ctrl2cap.nt5.sys dbgview.chm Dbgview.exe Desktops.exe Disk2vhd.chm disk2vhd.exe diskext.exe diskext64.exe Diskmon.exe DISKMON.HLP DiskView.exe DMON.SYS du.exe du64.exe efsdump.exe Eula.txt FindLinks.exe FindLinks64.exe handle.exe handle64.exe hex2dec.exe hex2dec64.exe junction.exe junction64.exe ldmdump.exe Listdlls.exe Listdlls64.exe livekd.exe LoadOrd.exe LoadOrd64.exe LoadOrdC.exe LoadOrdC64.exe logonsessions.exe logonsessions64.exe movefile.exe movefile64.exe notmyfault.exe notmyfault64.exe notmyfaultc.exe notmyfaultc64.exe ntfsinfo.exe ntfsinfo64.exe pagedfrg.exe pagedfrg.hlp pendmoves.exe pendmoves64.exe pipelist.exe pipelist64.exe PORTMON.CNT portmon.exe PORTMON.HLP procdump.exe procdump64.exe procexp.chm procexp.exe procmon.chm Procmon.exe PsExec.exe PsExec64.exe psfile.exe psfile64.exe PsGetsid.exe PsGetsid64.exe PsInfo.exe PsInfo64.exe pskill.exe pskill64.exe pslist.exe pslist64.exe PsLoggedon.exe PsLoggedon64.exe psloglist.exe pspasswd.exe pspasswd64.exe psping.exe psping64.exe PsService.exe PsService64.exe psshutdown.exe pssuspend.exe pssuspend64.exe Pstools.chm psversion.txt RAMMap.exe readme.txt RegDelNull.exe RegDelNull64.exe regjump.exe RootkitRevealer.chm RootkitRevealer.exe ru.exe ru64.exe sdelete.exe sdelete64.exe ShareEnum.exe ShellRunas.exe sigcheck.exe sigcheck64.exe streams.exe streams64.exe strings.exe strings64.exe sync.exe sync64.exe Sysmon.exe Sysmon64.exe Tcpvcon.exe tcpview.chm Tcpview.exe TCPVIEW.HLP Testlimit.exe Testlimit64.exe Vmmap.chm vmmap.exe Volumeid.exe Volumeid64.exe whois.exe whois64.exe Winobj.exe WINOBJ.HLP ZoomIt.exe
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值