zoukankan      html  css  js  c++  java
  • 烫烫烫”——调试基础断点篇

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

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

    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个。

  • 相关阅读:
    hdu 4027 Can you answer these queries? 线段树
    ZOJ1610 Count the Colors 线段树
    poj 2528 Mayor's posters 离散化 线段树
    hdu 1599 find the mincost route floyd求最小环
    POJ 2686 Traveling by Stagecoach 状压DP
    POJ 1990 MooFest 树状数组
    POJ 2955 Brackets 区间DP
    lightoj 1422 Halloween Costumes 区间DP
    模板 有源汇上下界最小流 loj117
    模板 有源汇上下界最大流 loj116
  • 原文地址:https://www.cnblogs.com/quark/p/4191003.html
Copyright © 2011-2022 走看看