程序断点

debug break point

断点

硬断点

Debug Register

硬件断点是通过位于 CPU 上的一组特殊寄存器来实现的,称为调试寄存器。比如 x86 架构的 CPU 上有 8 个调试寄存器(DR0-DR7),分别用于设置和管理硬件断点。

  • DR0-DR3 负责存储硬件断点的Linear Address。所以最多只能同时使用 4 个硬件断点。
  • DR4 和 DR5 保留使用。
  • DR6 为调试状态寄存器,保存调试异常产生后显示的一些信息
  • DR7 是硬件断点的激活开关,存储着各个断点的触发信息条件。 与软断点不同的是,硬件断点使用 1 号中断(INT1)实现,INT1 一般被用于硬件断点和单步事件。

硬件断点和原理与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef struct _DBG_REG7
{
/*
// 局部断点(L0~3)与全局断点(G0~3)的标记位
*/
unsigned L0 : 1; // 对Dr0保存的地址启用 局部断点
unsigned G0 : 1; // 对Dr0保存的地址启用 全局断点
unsigned L1 : 1; // 对Dr1保存的地址启用 局部断点
unsigned G1 : 1; // 对Dr1保存的地址启用 全局断点
unsigned L2 : 1; // 对Dr2保存的地址启用 局部断点
unsigned G2 : 1; // 对Dr2保存的地址启用 全局断点
unsigned L3 : 1; // 对Dr3保存的地址启用 局部断点
unsigned G3 : 1; // 对Dr3保存的地址启用 全局断点

unsigned LE : 1; // local exception
unsigned GE : 1; // global exception

// 保留字段
unsigned Reserve1 : 3;

// 保护调试寄存器标志位,如果此位为1,则有指令修改条是寄存器时会触发异常
unsigned GD : 1;

// 保留字段
unsigned Reserve2 : 2;

unsigned RW0 : 2; // 设定Dr0指向地址的断点类型
unsigned LEN0 : 2; // 设定Dr0指向地址的断点长度
unsigned RW1 : 2; // 设定Dr1指向地址的断点类型
unsigned LEN1 : 2; // 设定Dr1指向地址的断点长度
unsigned RW2 : 2; // 设定Dr2指向地址的断点类型
unsigned LEN2 : 2; // 设定Dr2指向地址的断点长度
unsigned RW3 : 2; // 设定Dr3指向地址的断点类型
unsigned LEN3 : 2; // 设定Dr3指向地址的断点长度
}DBG_REG7, *PDBG_REG7;

保存DR0-DR3地址所指向位置的断点类型(RW0-RW3)与断点长度(LEN0-LEN3),状态描述如下:

  • RW: 00:执行 01:写入 11:读写
  • LEN: 00:1字节 01:2字节 11:4字节

设置硬件执行断点时,长度只能为1(LEN0-LEN3设置为0时表示长度为1)

设置读写断点时,如果长度为1,地址不需要对齐,如果长度为2,则地址必须是2的整数倍,如果长度为4,则地址必须是4的整数倍。

如果我们想开启一个硬断点

DBG_REG7

  • L0 = 0b1
  • LE = 0b1 在局部exception触发断点
  • RW0 = 0b11
  • LEN0 = 0b11

可以做一下CTF题目:SCTF 2023 Kernel Pwn Sycrop

软断点

需要中断指令 INT3。

当我们在调试器中对代码的某一行设置断点时,调试器会先把这里的本来指令的第一个字节保存起来,然后写入一条 INT 3 指令。因为 INT 3 指令的机器码为 11001100b(0xCC),仅有一个字节,所以设置和取消断点时也只需要保存和恢复一个字节,这是设计这条指令时须考虑好的。

当 CPU 执行到 INT 3 指令时,由于 INT 3 指令的设计目的就是中断到调试器,因此,CPU 执行这条指令的过程也就是产生断点异常(breakpoint exception,简称#BP)并会保存当前的执行上下文,转去执行异常处理例程的过程。

在调试器下,我们是看不到动态替换到程序中的 INT 3 指令的。大多数调试器的做法是在被调试程序中断到调试器时,会先将所有断点位置被替换为INT 3 的指令恢复成原来的指令,然后再把控制权交给用户。

当用户结束分析希望恢复被调试程序执行时,调试器通过调试 API 通知调试子系统,这会导致系统内核的异常分发函数返回到异常处理例程,然后异常处理例程通过IRET/IRETD 指令触发一个异常返回动作,使 CPU 恢复执行上下文,从发生异常的位置继续执行。

gdb

x86处理器引入的PSW寄存器,有一个陷阱标志位,名为Trap Flag,简称TF。

当TF为1时,CPU每执行一条指令便会产生一个调试异常,中断到调试异常处理程序。

调试器的单步执行功能大多是依靠这一机制来实现的。