您的位置:首页 > 其它

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

2017-08-15 01:02 274 查看
一、  调试器命令窗口

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

#include "stdafx.h"   

#include <stdio.h>   

int main(int argc, char* argv[])   

{   

  int i = 0;   

  while( 1)   

  {   

    printf( "TestDebug1.cpp:%d\r\n", i);   

  }   
<
1c53e
li style="border-top:none;border-right:none;border-bottom:none;border-left:3px solid rgb(108,226,108);background-color:rgb(248,248,248);line-height:18px;margin:0px !important;padding:0px 3px 0px 10px !important;list-style-position:outside !important;">
  return 0;   

}   

为了方便,这次使用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

0:001> ~   

    0    Id: 1998.1358 Suspend: 1 Teb: 7ffde000 Unfrozen   

.    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

#include "stdafx.h"   

#include <stdio.h>   

#include <windows.h>   

   

class CTestClass   

{   

public:   

  CTestClass(){};   

  ~CTestClass(){};   

  void SetChar( unsigned char ucChar)   

  {   

    m_ucTestChar = ucChar;   

  }   

protected:   

  unsigned char m_ucTestChar  

[csharp] view
plain copy

};   

   

int main(int argc, char* argv[])   

{   

  typedef int (*pfnTestDllAdd)( int a, int b);   

   

  int i;   

  HMODULE hMod = LoadLibraryA( "TestDebugDll1.dll");   

  pfnTestDllAdd TestDllAdd = (pfnTestDllAdd)::GetProcAddress( hMod, "TestDllAdd");   

  if ( TestDllAdd)   

  {   

    i = TestDllAdd( 1, 2);   

  }   

   

  CTestClass objTestClass;   

  objTestClass.SetChar( 123);   

  return 0;   

}  

    还是使用Debug 选项,重新编译。用WinDbg 打开TestDebug1.exe 后会自动中断到初

始断点。由于是Debug 选项编译的,所以这里可以省去符号路径的设置就能识别符号。 

     bp 命令是最常用的断点命令之一,它可以直接对某个代码地址设置断点。例如我们想

中断到main 函数,可以这样:

[csharp] view
plain copy

0:000> bp TestDebug1!main  

     前面的TestDebug1 明确指定main 符号所在的模块,这样通常可以减少搜索符号的时

间,也避免了相同名字的符号可能造成的冲突。F5 运行,就发现已经中断到main 函数了,

并且源码窗口会自动弹出来。 

    在源码窗口或者返汇编窗口中,可以将光标移动到要设置断点的行并用F9 快捷键来设

置断点。这和Visual Studio 中一样。现在我们在HMODULE hMod = 

LoadLibraryA( "TestDebugDll1.dll");这一行处按下F9  设置一个断点。可以看到源码窗口中将

当前正中断到的断点和未触发的断点用不同的颜色标识出来:



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

[csharp] view
plain copy

0:000> bl   

  0 e 00401030 [C:\Users\NetRoc\Desktop\TestDebug1\TestDebug1.cpp @ 23]        0001   

(0001)    0:**** TestDebug1!main   

  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

bp TestDebug1.exe!CTestClass::SetChar   

bp TestDebug1.exe!CTestClass__SetChar   

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

0:000> !address 400000   

  ProcessParametrs 002c14f0 in range 002c0000 002c4000   

  Environment 002c0808 in range 002c0000 002c4000   

     00400000 : 00400000 - 00001000   

                     Type          01000000 MEM_IMAGE   

                     Protect    00000002 PAGE_READONLY   

                     State        00001000 MEM_COMMIT   

                     Usage        RegionUsageImage  

[csharp] view
plain copy

FullPath TestDebug1.exe   

     这里可以看到指定的地址0x400000 的内存类型、保护属性、拥有该地址的模块等

等。 

  dv 命令可以查看当前作用域下局部变量的类型和值:

[csharp] view
plain copy

0:000> dv   

            argc = 2147328000   

Type information missing error for argv   

Type information missing error for objTestClass   

               i = 1684234849   

Type information missing error for TestDllAdd   

Type information missing error for hMod  

 
    main 函数有些局部变量没有类型信息,这是因为VC6 中默认的Debug 选项编译出

来之后,.pdb 文件中符号信息并不完全。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: