一口气看完45个寄存器,CPU核心技术大揭秘( 三 )


WP: 是否开启内存写保护 , 若开启 , 对只读页面尝试写入时将触发异常 , 这一机制常常被用来实现写时复制功能
PE: 是否开启保护模式
除了CR0 , 另一个值得关注的寄存器是CR3 , 它保存了当前进程所使用的虚拟地址空间的页目录地址 , 可以说是整个虚拟地址翻译中的顶级指挥棒 , 在进程空间切换的时候 , CR3也将同步切换 。
调试寄存器在x86/x64CPU内部 , 还有一组用于支持软件调试的寄存器 。
调试 , 对于我们程序员是家常便饭 , 必备技能 。 但你想过你的程序能够被调试背后的原理吗?
程序能够被调试 , 关键在于能够被中断执行和恢复执行 , 被中断的地方就是我们设置的断点 。 那程序是如何能在遇到断点的时候停下来呢?
一口气看完45个寄存器,CPU核心技术大揭秘文章插图
对于一些解释执行(PHP、Python、JavaScript)或虚拟机执行(Java)的高级语言 , 这很容易办到 , 因为它们的执行都在解释器/虚拟机的掌控之中 。
而对于像C、C++这样的“底层”编程语言 , 程序代码是直接编译成CPU的机器指令来执行的 , 这就需要CPU来提供对于调试的支持了 。
对于通常的断点 , 也就是程序执行到某个位置下就停下来 , 这种断点实现的方式 , 在x86/x64上 , 是利用了一条软中断指令:int 3来进行实现的 。
注意 , 这里的int不是指高级语言里面的整数 , 而是表示interrupt中断的意思 , 是一条汇编指令 , int 3则表示中断向量号为3的中断 。
在我们使用调试器下断点时 , 调试器将会把对应位置的原来的指令替换为一个int 3指令 , 机器码为0xCC 。 这个动作对我们是透明的 , 我们在调试器中看到的依然是原来的指令 , 但实际上内存中已经不是原来的指令了 。
顺便提一句 , 两个0xCC是汉字【烫】的编码 , 在一些编译器里 , 会给线程的栈中填充大量的0xCC , 如果程序出错的时候 , 我们经常会看到很多烫烫烫出现 , 就是这个原因 。
一口气看完45个寄存器,CPU核心技术大揭秘文章插图
言归正传 , CPU在执行这条int 3指令时 , 将自动触发中断处理流程(虽然这实际上不是一个真正的中断) , CPU将取出IDTR寄存器指向的中断描述符表IDT的第3项 , 执行里面的中断处理函数 。
而这个中断描述符表 , 早在操作系统启动之初 , 就已经提前安排好了 , 所以执行这条指令后 , 操作系统的中断处理函数将介入 , 来处理这一事件 。
后面的过程就多了 , 简单来说 , 操作系统会把触发这一事件的进程冻结起来 , 随后将这一事件发送到调试器 , 调试器拿到之后就知道目标进程触发断点了 。 这个时候 , 咱们程序员就能通过调试器的UI交互界面或者命令行调试接口来调试目标进程 , 查看堆栈、查看内存、变量都随你 。
如果我们要继续运行 , 调试器将会把之前修改的int 3指令给恢复回去 , 然后告知操作系统:我处理完了 , 把目标进程解冻吧!
上面简单描述了一下普通断点的实现原理 。 现在思考一个场景:我们发现一个bug , 某个全局整数型变量的值老是莫名其妙被修改 , 但你发现有很多线程 , 很多函数都有可能会去修改这个变量 , 你想找出到底谁干的 , 怎么办?
这个时候上面的普通断点就没办法了 , 你需要一种新的断点:硬件断点 。
这时候就该本小节的主人公调试寄存器登场表演了 。
一口气看完45个寄存器,CPU核心技术大揭秘文章插图
在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在执行代码的过程中 , 如果满足条件 , 将自动中断下来 。
回答前面提出的问题 , 想要找出是谁偷偷修改了全局整形变量 , 只需要通过调试器设置一个硬件写入断点即可 。