言归正传,CPU在执行这条int 3指令时,将自动触发中断处理流程(虽然这实际上不是一个真正的中断),CPU将取出IDTR寄存器指向的中断描述符表IDT的第3项,执行里面的中断处理函数。
而这个中断描述符表,早在操作系统启动之初,就已经提前安排好了,所以执行这条指令后,操作系统的中断处理函数将介入,来处理这一事件。
后面的过程就多了,简单来说,操作系统会把触发这一事件的进程冻结起来,随后将这一事件发送到调试器,调试器拿到之后就知道目标进程触发断点了。这个时候,咱们程序员就能通过调试器的UI交互界面或者命令行调试接口来调试目标进程,查看堆栈、查看内存、变量都随你。
如果我们要继续运行,调试器将会把之前修改的int 3指令给恢复回去,然后告知操作系统:我处理完了,把目标进程解冻吧!
上面简单描述了一下普通断点的实现原理。现在思考一个场景:我们发现一个bug,某个全局整数型变量的值老是莫名其妙被修改,但你发现有很多线程,很多函数都有可能会去修改这个变量,你想找出到底谁干的,怎么办?
这个时候上面的普通断点就没办法了,你需要一种新的断点:硬件断点。
这时候就该本小节的主人公调试寄存器登场表演了。
在x86架构CPU内部,提供了8个调试寄存器DR0~DR7。
DR0~DR3:这是四个用于存储地址的寄存器
DR4~DR5:这两个有点特殊,受前面提到的CR4寄存器中的标志位DE位控制,如果CR4的DE位是1,则DR4、DR5是不可访问的,访问将触发异常。如果CR4的DE位是0,则DR4和DR5将会变成DR6和DR7的别名,相当于做了一个软链接。这样做是为了将DR4、DR5保留,以便将来扩展调试功能时使用。
DR6:这个寄存器中存储了硬件断点触发后的一些状态信息
DR7:调试控制寄存器,这里面记录了对DR0-DR3这四个寄存器中存储地址的中断方式(是对地址的读,还是写,还是执行)、数据长度(1/2/4个字节)以及作用范围等信息
通过调试器的接口设置硬件断点后,CPU在执行代码的过程中,如果满足条件,将自动中断下来。
回答前面提出的问题,想要找出是谁偷偷修改了全局整形变量,只需要通过调试器设置一个硬件写入断点即可。
描述符寄存器所谓描述符,其实就是一个数据结构,用来记录一些信息,‘描述’一个东西。把很多个描述符排列在一起,组成一个表,就成了描述符表。再使用一个寄存器来指向这个表,这个寄存器就是描述符寄存器。
在x86/x64系列CPU中,有三个非常重要的描述符寄存器,它们分别存储了三个地址,指向了三个非常重要的描述符表。
gdtr: 全局描述符表寄存器,前面提到,CPU现在使用的是段+分页结合的内存管理方式,那系统总共有那些分段呢?这就存储在一个叫全局描述符表(GDT)的表格中,并用gdtr寄存器指向这个表。这个表中的每一项都描述了一个内存段的信息。
ldtr: 局部描述符表寄存器,这个寄存器和上面的gdtr一样,同样指向的是一个段描述符表(LDT)。不同的是,GDT是全局唯一,LDT是局部使用的,可以创建多个,随着任务段切换而切换(下文介绍任务寄存器会提到)。