柚木

调试程序时,设置断点的原理是什么?

对于程序员来说,debug的时间往往比写程序的时间还要长。尤其对我这种专写bug为主的程序员来说,一个好的调试器意味着早点下班和休息。现在方便的调试器很多,有著名的Visual Studio(VS)等IDE,也有免费的Windbg和GDB等等。加个断点也很简单,就是按一下键而已。但你有没有想过,调试器Debugger并不能控制程序的执行顺序,为什么它可以让CPU在需要的地方停住呢?

今天我们就来揭开调试断点的神秘面纱,并通过一个实例来看看调试器实际都做了些什么。调试器能够随心所欲的停止程序的执行,主要通过软件断点和硬件断点两种方式。

软件断点

软件断点在X86系统中就是指令INT 3,它的二进制代码opcode是0xCC。当程序执行到INT 3指令时,会引发软件中断。操作系统的INT 3中断处理器会寻找注册在该进程上的调试处理程序。从而像Windbg和VS等等调试器就有了上下其手的机会。

我们先通过一个例子来看看调试器都倒了什么鬼:

#include 
int main ()
{
// This loop takes some time so that we
// get a chance to examine the address of
// the breakpoint at the second printf
for (int i = 1; i < 100000000; i++)
   printf("Hello World!");
for (int i = 1; i < 10000000; i++)
   printf("Hello World!");
return 0;
} 

这是一个比较傻的Hello World程序。我们用Windbg打开它,并设置一个断点:

这时Windbg会将自己Attach到该程序的进程,通过程序PE文件的debug节找到调试信息。在调试信息里面找到加断点行所在的机器代码,并把头一个字节用WriteProcessMemory()函数换成0xCC(INT 3)。

让我们来验证一下:

推荐点开全屏看,可能更清楚

注意左边是Windbg窗口,右边是用Process view打开的进程空间,左右的红框是对应的。在我们设置断点之前,左右的内容是完全一样的,这里要特别注意printf编译出来的第一个二进制代码0x68。接下来我们设置断点,并开始运行,运行中按stop停下来,看看发生了什么:

我们会发现push操作代码0x68600e2900的第一个字节被windbg换成了0xCC也就是INT 3。这样windbg就可以在执行到这里时被调度。

不一会,windbg的断点到了:

到达断点后,操作符又被还原为0x68,似乎什么都没有发生,用户被蒙在鼓里,是不是很有意思?

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

硬件断点

X86系统提供8个调试寄存器(DR0~DR7)和2个MSR用于硬件调试。其中前四个DR0~DR3是硬件断点寄存器,可以放入内存地址或者IO地址,还可以设置为执行、修改等条件。CPU在执行的到这里并满足条件会自动停下来。

硬件断点十分强大,但缺点是只有四个,这也是为什么所有调试器的硬件断点只能设置4个原因。我们在调试不能修改的ROM时,只能选择这个,所以要省着点用,在一般情况下还是尽量选择软件断点。

还有个INT 1是单步调试命令,这里略过。

其他

Visual Studio有个有趣的特性是debug编译后,会把0xcc(INT 3)填入代码的空隙,这样一旦程序越界就会被VS捕捉而容易发现错误。而0xCCCC在中国的GBK编码是“烫”。有中国程序员翻看内存到代码段会发现很多"烫烫烫",不明所以,以为发生了什么神奇的事情。

有些程序越界也会打出"烫烫烫":

有的用户被吓得够呛,以为计算机过热了,喊烫了,赶紧关机,十分搞笑。

欢迎大家关注本专栏和用微信扫描下方二维码加入微信公众号"UEFIBlog",在那里有最新的文章。同时欢迎大家给本专栏和公众号投稿!