首页 > 代码库 > 烫烫烫”——调试基础断点篇

烫烫烫”——调试基础断点篇

很多人都应该见过“烫烫烫”这个神一般存在的字符串,一旦“烫烫烫”出现的时候,就说明你玩坏了——指针越界,访问到了非法内存。

技术分享

那么为啥是“烫烫烫”,跟断点有啥关系?

INT 3

我们在用VC进行调试时,常常会观察到一块刚分配的内存或字符串被填满了“CC”,而0xCCCC正好是“烫”这个汉字的GB2312编码。另外很巧的是 0xCC又正好是INT3指令的机器码。这显然不是什么巧合,而是我们的编译器故意这么做的。至于原因,先看INT3这条指令是干嘛的?

x86架构下提供了一条专门用来支持调试的指令,即INT 3。这条指令的目的是使CPU中断到调试器。

看下面一个简单的例子:

技术分享

在调试状态下执行INT 3指令,程序就会断下来并提示这是一个Break instruction exception(上图右半部分)。并且从上图可以看到INT 3的机器码是0xCC。

 

所以,编译器在调试状态下会把未初始化的缓冲区填充为0xCC(0xCD),目的就是为了因缓冲区溢出等原因程序指针指向了这块区域,遇到INT 3指令而中断到调试器。

 

PS: 在实际的代码中,INT3也是能派上用场的。曾经在调试一个程序的时候,需要在一个宏里面下断点,而通过VS是没法直接在宏里面下断点的。所以当时做了一件事情,就是在宏里面需要断下来的地方加入了INT 3,这样程序一旦跑起来到这个地方就会自动断下来。

软件断点

软件断点是我们最常用的断点,用来在程序代码中设置断点。当代码执行到断点所在行时程序便会断下来,这个时候可以通过调试器观察,修改此时寄存器上下文,内存数据等。

软件断点实现的原因正是通过INT 3指令来实现的

我们在VS,Windbg等调试器中设置一个断点的时候,究竟发生了什么?

l  调试器首先会在内存映射中找到对应的断点位置;

l  将断点位置的第一个字节替换成0xCC,即INT 3,然后将被替换的这个字节保存起来;

l  一旦程序执行到INT 3指令,就会产生一个断点中断到调试器,这就是断点命中;

l  当用户恢复程序运行时,调试器实际做的事情就是恢复INT 3指令替换的那个字节,让程序按照原指令执行。

 

我们做个实验来验证一下:

对于如下代码:进程为breakpoint.exe

技术分享

我们用VS在第10行printf语句设置一个断点,然后将程序在VS下运行起来,主要不要让代码跑到断点处。

这个时候我们用Windbg也挂住breakpoint.exe进程,查看第9行代码的反汇编:

技术分享

可以看到第一个指令就是INT 3,现在我们用VS中同样查看一下第9行汇编:

技术分享

为啥同样的进程状态下,两个调试器看到的指令不一样,因为VS是设置断点的时候存储了被INT 3指令替换的那个字节的内容,所以在UI上展示的时候VS可以还原原始代码的情况,而实际上Windbg展示的才是进程当前真实的指令。原始代码本身是“movesi, esp”指令,机器码是0x8bf4,因为第一个字节0x8b被替换成了0xCC,所以导致windbg下面看到的下一个字节0xf4被解析成了“hlt”指令。

使用INT 3指令产生的断点是依靠插入指令和软件中断机制工作的吗,因此把这类断点称为软件断点。但是软件断点也有局限性:

l  可以让CPU执行到代码的某个地址停下来,但不适用于数据段和I/O空间;

l  对于在ROM中执行程序,无法动态的添加软件断点,因为目标内存是只读的。

l  依赖于中断机制的正常运行,如果中断向量表或者中断描述表没有准备好或者被破坏,软件断点是无法正常工作的。

硬件断点

硬件断点之所以“硬”,是因为硬件断点依赖硬件。英特尔从386开始,增加了调试寄存器和硬件断点的特性。

IA-32架构定义了8个调试寄存器,其中4个用来存储断点地址,2个寄存器保留,1个调试控制寄存器,1个调试状态寄存器。也就是说,最多可以设置4个硬件断点。

硬件断点有什么作用:

l  读写内存中的数据中断;

l  执行内存中的代码中断(作用类似于软件断点);

l  读写I/O端口时中断;

 

我们日常工作中最常用到的硬件断点的场景就是“读写内存中的数据中断”,即监控某个内存地址读写,也叫内存断点。特别是在多线程环境下监控某些全局变量的状态,有时候能起到奇效。

设置一个硬件断点,本质上就是将要监控的地址写入到一个调试寄存器,以及把相关的控制选项写入到控制寄存器。一旦满足调试寄存器中设置的状态,断点就会被触发。看个例子:

技术分享

如上代码,变量flag初始化后并没有被使用,但是打印出来的值却不是0x123。

当然,以上这个例子很容易发现对数组a的访问越界了,而实际项目出问题的代码往往是很难直接看出问题原因的。

我们来调试一下以上代码,看看究竟是什么时候flag的值被修改的。

1.       首先用Windbg启动被调试程序breakpoint.exe;

2.       设置断点到main函数:bpbreakpoint!wmain;

3.       运行程序到断点处:main函数的入口处;

4.       通过dv /V命令查看当前栈帧的局部变量信息;

技术分享

5.       知道了flag变量的地址是002cf9f0,设置一个硬件断点监控地址为002cf9f0的写行为。

技术分享

下面是8个调试寄存器的值:

技术分享

dr0被设置成了我们要监控的地址,dr6,dr7分别是调试状态寄存器,调式控制寄存器。

上图展示了两个断点,第一个是我们之前设置的软件断点,第二个就是硬件断点:参数w表示只监控该地址的写行为,4表示监控长度为4个字节。

6.       继续运行程序,会遇到第一次中断:

技术分享

断下来的这句指令是将栈帧部分填充为0xCCCCCCCC,“烫烫烫”又出现了。另外这段初始化指令只在调试版本中才会有。

7.       继续运行程序,第二次断下来:

技术分享

这次是因为对变量flag赋值为123而断下来,意料之中。

8.       第三次断住:

技术分享

以上指令是将ecx的值5赋值给正好是flag所在的内存地址:ebp + eax * 4 – 20h == ebp– 0x0C。

是第18行代码干的,也就是说因为数组访问越界从而影响到了flag的值!

 

硬件断点功能强大,但是最大的缺点受限于硬件——数量限制,最多4个。

烫烫烫”——调试基础断点篇