程序断点的原理

  • 调试器 Debugger 并不能控制程序的执行顺序,它之所以可以让 CPU 在需要的地方停住(随心所欲的停止程序的执行),主要通过软件断点硬件断点两种方式。

软件断点

  • 软件断点在 x86 / x32 / x64 系统中就是指令 INT 3 ,它的二进制代码 opcode0xCC 。当程序执行到 INT 3 指令时,会引发软件中断。操作系统的 INT 3中断处理器 会寻找注册在该进程上的调试处理程序,从而像 WindbgVS 等等调试器就有了上下其手的机会。
  • 通过一个例子说明:

    1
    2
    3
    4
    5
    6
    #include <iostream>

    int main()
    {
    printf("Hello World"); // 这里断点
    }
  • 此时调试器会将目标地址的指令替换为 int 3 指令(机器码 0xCC ),等到达断点的时候会还原目标地址的指令。

    程序断点的原理

  • 一般情况下,调试器维护了一大组调试断点,在并把他们都换成了 INT 3 。在被调度回来后,会都填回去,并通过现在的地址判断是到了那个断点。软件断点没有数目限制

硬件断点

  • x86 / x32 / x64 系统中提供 8 个调试寄存器(DR0 ~ DR7)和多个 MSR 用于硬件调试(MSR 数量取决于 ​​CPU 型号)。其中前四个 DR0 ~ DR3 是硬件断点寄存器,可以放入内存地址或者IO地址,还可以设置为执行、修改等条件。CPU在执行的到这里并满足条件会自动停下来。
  • 硬件断点十分强大,但缺点是只有四个,这也是为什么所有调试器的硬件断点只能设置 4 个的原因。我们在调试不能修改的 ROM 时,只能选择这个,所以要省着点用,在一般情况下还是尽量选择软件断点。
  • 补充:​​
    1. 调试控制寄存器 (DR7)​​:这个寄存器至关重要,它负责定义每个硬件断点的具体​​行为方式​​。需要通过设置 DR7 中的相应位域来为 DR0-DR3 中的每个地址配置:
      • ​触发条件 (R/Wx)​​: 指定在什么操作下触发断点,例如执行指令(00)、写入数据(01)、或读写数据(11)等。
      • ​​长度 (LENx)​​: 指定监控的内存区域大小,例如 1 字节、2 字节或 4 字节(在 64 位模式下还支持 8 字节)。
      • 启用开关 (Lx/Gx)​​: 控制每个断点是仅在当前任务有效(局部,Lx),还是对所有任务都有效(全局,Gx)。
    2. 调试状态寄存器 (DR6):​当某个硬件断点被触发,CPU陷入调试异常时,这个寄存器会记录​​哪个断点引起了异常​​以及其他相关状态信息(例如,是由 DR0 还是 DR1 触发的),帮助调试器判断异常原因。
    3. DR4DR5​:这两个寄存器的功能由控制寄存器 CR4 中的 DE(调试扩展)位 决定。当 CR4.DE = 1 时,对 DR4DR5 的访问会产生无效操作码异常(#UD),它们被视为保留。当 CR4.DE = 0 时,它们则被映射为对 DR6DR7 的别名访问,此举主要是为了与早期处理器兼容。

单步执行 (TF标志) 和任务切换断点的机制略。