断点和单步执行是两个经常使用的调试功能,也是调试器的核心功能。
断点是调试器的最常用技术之一。其基本思想是在某一个位置设置一个陷阱,当CPU执行到此位置时,中断到调试器中,让调试者分析和调试,之后恢复执行。
- 根据设置位置分为:代码断点、数据断点、I/O断点。
- 根据设置方法分为:软件断点和硬件断点。软件断点通常是向指定的代码为止插入专用的断点指令实现。硬件断点通常是通过设置CPU的调试寄存器实现。
单步执行是最早的调试方式之一。就是让应用程序按照某一个步骤单位一步一步执行。一般的做法是,先使用断点功能将程序中断到感兴趣的位置,然后再单步执行关键的代码。
1. 软件断点
1.1 INT3指令
x86 系列处理器从其第一代产品英特尔 8086 开始就提供了一条专门用来支持调试的指令,即 INT 3。简单地说,这条指令的目的就是使 CPU 中断(break)到调试器,以供调试者对执行现场进行各种分析。
当我们调试程序时,可以在可能有问题的地方插入一条 INT 3 指令,使 CPU 执行到这一点时停下来。这便是软件调试中经常用到的断点(breakpoint)功能,因此 INT 3 指令又被称为断点指令。
1.2 在调试器中设置断点
当我们在调试器中对代码的某一行设置断点时,调试器会先把这里的本来指令的第一个字节保存起来,然后写入一条 INT 3 指令。因为 INT 3 指令的机器码为 11001100b(0xCC),仅有一个字节,所以设置和取消断点时也只需要保存和恢复一个字节,这是设计这条指令时须考虑好的。
1.3 断点命中
当 CPU 执行到 INT 3 指令时,由于 INT 3 指令的设计目的就是中断到调试器,因此,CPU 执行这条指令的过程也就是产生断点异常(breakpoint exception,简称#BP)并会保存当前的执行上下文,转去执行异常处理例程的过程。对于 Windows来说,INT 3 异常的处理函数是操作系统的内核函数(KiTrap03)。
内核例程会把这个异常通过调试子系统以调试事件的形式分发给用户模式的调试器,并等待调试器的回复。
在调试器收到调试事件后,它会根据调试事件数据结构中的程序指针得到断点异常的发生位置,然后在自己内部的断点列表中寻找与其匹配的断点记录。如果能找到,则说明这是“自己”设置的断点,执行一系列准备动作后,便允许用户进行交互式调试。如果找不到,就说明导致这个异常的 INT 3 指令不是调试器动态替换进去的,因此会显示的对话框,意思是说一个“用户”插入的断点被触发了。
在调试器下,我们是看不到动态替换到程序中的 INT 3 指令的。大多数调试器的做法是在被调试程序中断到调试器时,会先将所有断点位置被替换为INT 3 的指令恢复成原来的指令,然后再把控制权交给用户。
1.4 恢复执行
当用户结束分析希望恢复被调试程序执行时,调试器通过调试 API 通知调试子系统,这会导致系统内核的异常分发函数返回到异常处理例程,然后异常处理例程通过IRET/IRETD 指令触发一个异常返回动作,使 CPU 恢复执行上下文,从发生异常的位置继续执行。
1.5 断点API
Windows 操作系统提供了 API 供应用程序向自己的代码中插入断点。在用户模式下,可以使用 DebugBreak() API,在内核模式下可以使用 DbgBreakPoint()或者DbgBreakPointWithStatus()。
这些 API 在 x86 平台上其实都只是对 INT 3 指令的简单包装。
1.6 归纳
因为使用 INT 3 指令产生的断点是依靠插入指令和软件中断机制工作的,因此人们习惯把这类断点称为软件断点,软件断点具有如下局限性。
- 属于代码类断点,即可以让 CPU 执行到代码段内的某个地址时停下来,不适用于数据段和 I/O 空间。
- 对于在 ROM(只读存储器)中执行的程序(比如 BIOS 或其他固件程序),无法动态增加软件断点。因为目标内存是只读的,无法动态写入断点指令。这时就要使用我们后面要介绍的硬件断点。
- 在中断向量表或中断描述表(IDT)没有准备好或遭到破坏的情况下,这类断点是无法或不能正常工作的,比如系统刚刚启动时或 IDT 被病毒篡改后,这时只能使用硬件级的调试工具
2. 硬件断点
硬件断点不需要像软件断点那样像代码中插入软件指令,依靠处理器本身的功能便可以实现,所以人们习惯上把使用调试地址寄存器DR0-DR7设置的断点叫做硬件断点。
硬件断点有很多优点,但是也有不足,最明显的就是数量限制,因为只有4个断点地址寄存器。
- DR0-DR3用于存放断点的线性地址
- DR4-DR5保留
- DR6是调试状态寄存器,用于向调试器报告事件的详细信息,以供调试器判断发生的是何种事件
- DR7是调试控制寄存器,用于定义断点的中断条件
3. 单步执行标志
x86处理器引入的PSW寄存器,有一个陷阱标志位,名为Trap Flag,简称TF。
当TF为1时,CPU每执行一条指令便会产生一个调试异常,中断到调试异常处理程序。
调试器的单步执行功能大多是依靠这一机制来实现的。