我,子牙老师,一个手写过操作系统、编程语言、Java虚拟机、docker、Ubuntu系统,玩透Windows内核、Linux内核的...硬核男人
关于gdb调试器,我已经写了三篇硬核文章了《从零手写gdb调试器》《调试器是如何让代码停下来的》《gdb调试器底层实现原理》,今天开启它的第四篇:gdb单步调试代码的底层实现原理
国内关于gdb调试器相关的资料非常少,如果你对调试器底层实现感兴趣,欢迎关注公众号【硬核子牙】,看调试器系列文章。如果你想写一个自己百分百控股的调试器,欢迎学习我的课程《从零手写gdb调试器》
什么是单步调试,就是这三个按钮

从左到右:step over,单步步过。step into,单步步入。step out,单步步出。本篇文章主要谈step into,中间那个。因为step over、step out的实现要依赖step into。step over、step out,后面再写文章细讲
看完本篇文章,你能学到单步步入的全部:
- 单步步入的底层实现原理
- 实战:不依赖Linux内核自实现单步步入
- 调试Linux内核看单步步入在内核层是如何实现的
- Linux内核是在何时清除单步标识的
- 调试Linux内核论证单步标识清除
以下,enjoy
单步步入底层原理
单步步入的底层实现,是依赖CPU实现的。在CPU内部,有一个寄存器叫状态寄存器:elfags,长这样

其中第8位对研究单步调试非常关键:TF位,单步标志位。记住TF这两个单词,等下看Linux内核源码会看到。
这个TF是如何工作的呢?
当CPU执行完一条机器指令后,会瞄一眼自己的EFLAGS寄存器,如果其中的 TF(Trap Flag)位为1,就会触发一个调试异常(#DB),它是异常向量号1,属于硬件异常,由 CPU自动产生

你可能注意到了EFLAGS中的第9位,IF(Interrupt Flag)位,中断使能控制位。如果这位为0,即关闭中断,会影响单步调试吗?答案是不会。这个位只影响中断,不影响异常。这个位一般是1,因为中断是一直在发生的
所以EFALGS的值如果是0x3**,就表示开启了单步调试功能。接下来咱们实战一下
自实现单步步入
为了实战演示,我给我写的调试器增加了两个功能:开启单步、关闭单步,本质就是修改ELFAGS寄存器的第8位

来看下实战效果。我在main函数处下断点,让程序跑到这个位置停下来,当前的CPU所处位置是0x400542

看下elfags寄存器的值

0x246,没有开启单步调试。我如果现在执行continue,程序就运行结束了。我执行step命令,开启单步调试功能

接下来我执行continue,看它是单步执行还是执行结束

可以看到,是单步执行的。再看下TF位的值

发现还在,所以你现在执行continue,它会一直单步走。但是gdb的单步,你会发现它不是一直生效,它只生效一次,那gdb是何时清除TF位的呢?留个问题,后面会讲到
执行step_del清除TF位,再执行continue,让程序执行结束

一切如我们所愿。自己写的调试器,精准控制程序运行,酷不酷?
Linux内核层实现单步步入
gdb是如何实现单步调试的呢?

使用ptrace函数,request=PTRACE_SINGLESTEP,进入Linux内核做了什么呢?就找重点代码,设置EFLAGS的TF位

最终会执行到这个函数

找到核心代码了!
Linux内核清除单步标识
前面说过,gdb的单步调试是一次性的,言外之意就是执行了一次单步,就会清空EFLAGS中的TF位,这个清空的动作是gdb做的还是Linux内核做的呢?
Linux内核做的!什么时候做的呢?其实研究Linux内核需要一种这样的思维:就是如果这个功能你来实现,你会选择在哪个时机做呢?
答案是,1号#DB硬件异常的处理逻辑里。你仔细想想,是不是这里是最合适的?

至此,gdb调试器的单步调试功能的底层实现原理,就全部讲完了。step over、step out,下篇安排。关注公众号【硬核子牙】,看调试器系列硬核文章
最后再打个小广告,如果你想学习手写操作系统及实战Linux内核,如果你对调试器底层实现感兴趣,同时想写一个自己可以百分百控制的调试器,调试你自己写的操作系统,欢迎去我公众号【硬核子牙】看我之前的文章,了解课程详情。