对于程序员来说,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 printffor (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。接下来我们设置断点,并开始运行,那100万个printf让我们有充分的时间,看看发生了什么:
我们会发现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编码是“烫”。有中国程序员翻看内存到代码段会发现很多"烫烫烫",不明所以,以为发生了什么神奇的事情。
有些程序越界也会打出"烫烫烫":
有的用户被吓得够呛,以为计算机过热了,喊烫了,赶紧关机,十分搞笑。