Debugging Tools for Windows

使用断点

断点位于可执行代码中,它使得操作系统停止程序执行并中断到调试器。 然后,就可以分析目标和执行调试器命令。

可以通过指定虚拟地址、模块和函数偏移或源码文件和行号来设置断点(当在源码模式中时)。如果在某个例程上设置断点并且没有使用偏移,则当运行到这个例程上时就会触发断点。

还有下面一些类型的断点:

如果在用户模式下调试多于一个进程,每个进程都有它自己的断点集合。要查看或修改某个进程的断点,必须将该进程设置为当前进程。关于当前进程的更多信息,查看控制进程和线程

控制断点的方法

使用下面一些方法来控制或显示断点:

每个断点都有一个关联的10进制数字称为断点ID。该数字在各种命令中用于指定断点。

未定断点:BU vs. BP

如果一个断点是设置在某个还未加载的函数名上,则称为延迟虚拟未定断点。 (这些术语可交替使用。) 未定断点没有被关联到任何具体被加载的模块上。每当一个新的模块被加载时,会检查该函数名。如果这个函数出现,调试器计算虚拟断点的实际位置并启用它。

使用bu设置的断点自动被认为是未定断点。如果断点在一个已加载模块中,则会启用并正常生效。但是,如果模块之后被卸载并重新加载,这个断点不会消失。而使用bp设置的断点会立即绑定到某个地址。

bpbu断点有以下三个主要的不同点:

当在WinDbg 反汇编窗口源码窗口中使用鼠标设置断点时,调试器创建的是bu断点。

初始断点

当调试器启动一个新的目标程序时,初始断点在主映像和所有静态加载的DLL被加载、DLL初始化例程被调用之前自动触发。

调试器附加到一个已存在的用户模式程序时,初始断点立即触发

-g 命令行选项使得WinDbg或CDB跳过初始断点。在这时可以自动执行命令。更多信息,查看控制异常和事件

如果想启动新调试目标并在实际的程序即将开始执行的时候中断下来,就不要使用-g选项。应该让初始断点被触发。当调试器激活之后,在mainwinmai函数上设置断点并使用g (Go) 命令。之后所有初始化过程都会运行并且程序在main函数即将执行时停止。

关于内核模式的自动断点的更多信息,查看崩溃和重起目标机

断点中的地址

断点支持几种地址语法,包括虚拟地址、函数偏移和源码行号。例如,可以使用下面的方法之一来设置断点:

0:000> bp 0040108c
0:000> bp main+5c
0:000> bp `source.c:31`

关于这些语法的更多信息,查看数值表达式语法源代码行语法,以及各个命令的主题。

断点的数量

在内核模式下,最多可以使用32个断点。在用户模式下,可以使用任意数量的断点。

数据断点的数量由目标处理器架构决定

方法的断点

如果要在MyClass类的MyMethod方法上设置断点,可以使用两种不同语法:

如果要使用更复杂一些的断点命令,应该使用MASM表达式语法。表达式语法的更多信息,查看表达式求值

使用复杂文本的断点

@!"<chars>" 语法用于在MASM求值器中进行转义,使得符号解析支持任意文本。必须以@!"开始并以引号(")结束。如果不使用该语法,则在MASM表达式的符号名中不能使用空格、大于小于号(<, >)和其他特殊字符。该语法只能用于名字,不能用于参数。模板和重载是符号中需要这种引号的主要原因。也可以使用如下的@!"<chars>" 语法来设置bu 命令。

0:000> bu @!"ExecutableName!std::pair<unsigned int,std::basic_string<unsigned short,std::char_traits<unsigned short>,std::allocator<unsigned short> > >::operator="

这个例子中,ExecutableName 是一个可执行文件的名字。

这种转义语法在C++中比C中更加有用(例如重载的操作符),因为C函数名中不会存在空格(或特殊字符)。但是,该语法在托管代码中也同样重要,因为.NET Framwork中非常多的使用重载。

脚本中的断点

断点ID并不一定要明确指定。相反,可以使用能被计算成整数的数值表达式用作断点ID。使用下面的语法来表明表达式需要被解释成断点。

b?[Expression]

这个语法中,必须使用中括号,Expression是任何想要被当作断点ID的值为整数的数值表达式。

这种语法使得调试器脚本可以编程选择断点。下例中,断点会根据自定义伪寄存器的值的不同而变化。

b?[@$t0]

用户空间和系统空间

每个用户模式应用程序在虚拟内存0x00000000 到0x7FFFFFFF 的地址称为用户空间

当WinDbg或CDB在小于0x80000000的地址上下断时,断点是在单个进程的指定的用户空间的地址设置。用户模式调试时,当前进程决定了虚拟地址的意义。更多信息,查看控制进程和线程

在内核模式,可以使用bpbuba 命令和Breakpoints 对话框在用户空间设置断点。必须首先使用.process /i (或在一些内核空间的函数上的指定进程的断点)来将目标切换成当前进程上下文,并使用该进程上下文来指定拥有目标地址空间的用户模式进程。

用户模式的断点总是和设置该断点时进程上下文为激活状态的进程关联起来。如果有用户模式调试器在调试该进程,而还有一个内核模式调试器在调试进程运行的机器,即使断点由内核调试器设置,它中断时也是进入用户模式调试器。这时可以从内核模式调试器中断系统,或使用.breakin (Break to the Kernel Debugger) 命令来将控制权交给内核调试器。

断点伪寄存器(Pseudo-Registers)

如果在某个表达式中想引用某个断点的地址,可以使用一个$bpNumber 语法的伪寄存器Number是断点ID。关于该语法的更多信息,查看伪寄存器语法

设置断点时的风险

当使用内存地址或符号加偏移的方式设置断点时,一定不能将断点设置到一条指令的中间。

例如,有下面一段汇编代码。

770000f1 5e               pop     esi
770000f2 5b               pop     ebx
770000f3 c9               leave
770000f4 c21000           ret     0x10
770000f7 837ddc00         cmp     dword ptr [ebp-0x24],0x0

前三条指令只有1个字节长。但是第四条指令有3字节长。(包含在0x770000F4,0x770000F5, 和0x770000F6三个地址的字节)如果要在该指令上使用bpbuba设置断点,则必须将地址指定为0x770000F4 。

如果使用ba命令在0x770000F5 地址设置了断点,处理器将在该位置设置断点。但是 该断点永远不会被触发,因为处理器认为0x770000F4 才是这条指令的实际地址。

如果使用bpbu命令在 0x770000F5 设置断点,调试器在这个位置会写入断点。但是由于调试器使用如下方法设置断点,它可能造成目标运行错误:

  1. 调试器保存0x770000F5 地址的内容,并用一条断点指令写入该地址。
  2. 如果用调试器显示这段内存的内容,并不会显示被写入的断点指令,而是显示那里原来”应该是”的内容。即调试器显示原来的内存,或者断点设置之后该内存被修改的内容。
  3. 如果使用BC命令删除断点,调试器将断点位置的内存恢复成原始值。

当在0x770000F5设置断点时,调试器保存它的值并写入断点指令。但是当程序运行时到达0x770000F4 时,会将它视为一条多字节指令的第一个字节。处理器将0x770000F4、0x770000F5可能还有后面的一些字节当作一条指令。这会产生各种非正常的行为。

因此,当使用bpbuba 命令设置断点时,要确定断点在合适的地址上。如果使用WinDbg图形界面来添加断点就不用在意这样的情况,因为它会自动选择正确的地址。

断点命令

可以在断点中包含一条命令用于在断点触发时自动执行。

也可以包含一条用于执行的命令字符串。但是,其中任何恢复程序执行的命令(例如gt)都会终止命令列表的执行。

例如下面的命令在MyFunction+0x47中断,写入一个dump文件并恢复执行。

0:000> bu MyFunction+0x47 ".dump c:\mydump.dmp; g" 

注意 如果正在从内核调试器控制用户模式调试器,不要在命令字符串中使用g (Go)。串口的速度可能跟不上该命令,并且不能再中断到CDB中。关于这种情况的更多信息,查看从内核调试器控制用户模式调试器

条件断点

可以设置仅在特定条件下被触发的断点。关于这类断点的更多信息,查看设置条件断点

Build machine: CAPEBUILD