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

来源:互联网 发布:mac 4k 字体太小 编辑:程序博客网 时间:2024/06/05 03:04

一、  调试器命令窗口


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 文件中符号信息并不完全。

原创粉丝点击