引子在调试程序的时候,你是否遇到过下面这些情况:
本文将采用实例的方式,详细介绍一种针对上面这些情况简单有效的调试方法 - 反向调试 在真正介绍反向调试之前,先看一个例子,来展示下反向调试究竟有多大威力! 反向调试的威力——解决调用者全是问号的难题本文的测试代码,在正常运行时会触发Segmentation fault,并且调用栈显示全是问号(编译时当然已经添加了-g选项!): 使用本文介绍的逆向调试的方法,经过简单的调试,可以在不修改代码,不重新编译的前提下,让调用者显示正常,轻松找到Root Cause! 是不是很神奇?这只是反向调试威力的冰山一角而已!接下来,开始正式介绍反向调试! 什么是反向调试?反向调试是一种高级调试技术,可以让程序已经执行了一段时间后,回退到过去的状态并重新执行。这意味着你可以回到程序执行中的任何点,查看变量的值、堆栈跟踪以及程序执行路径。反向调试可以让我们快速、准确地定位出程序中的错误或异常的根本原因。 简单来说,就是一种可以让程序逻辑逆序执行的调试技术。通过它,你可以随时中断程序正常执行,然后让它逆序执行,让程序回到过去,可以查看任意时间点的任意信息。 先通过一个简单的例子,直观感受一下. 源码如图: 在GDB中启动程序,并停在程序入口处,并在第7行设置断点,以便让程序停下来,让我们进行反向调试 root@ubuntu:ReverseDebugging# gdb test 执行record命令,开始记录程序执行轨迹
执行c(continue)命令,让程序正常执行,然后在第7行触发断点,停下来 (gdb) c 此时,查看a的值,a = 3
执行reverse-next命令,让程序逆向执行一行代码。 (gdb) reverse-next 准确地说,这条命令的作用是让程序回退一行代码的执行效果。也就是回退到刚执行完第5行代码,尚未执行第6行代码时的状态。此时查看a的值,不出意外的话,a的值应该是2,我们来验证一下
果然,第6行代码的的执行效果已经被完全回退了。我们再执行一条reverse-next命令,此时,应该是回退第5行代码的执行效果。 (gdb) reverse-next 此时,程序的状态已经被回退到刚执行完第4行代码,尚未执行第5行代码时的状态,此时,a应该是1,验证一下:
然后,回退第4行代码的执行效果,让程序状态恢复到刚执行完第3行代码,尚未执行第4行代码时的状态: (gdb) reverse-next 到这里,大家应该已经明白了吧,reverse-next的作用是让程序恢复到上一行代码刚执行完时的状态。当然,我们仍然可以让程序以正常顺序执行下去:
再看一下整个调试过程,加深一下理解: 反向调试实现原理反向调试技术的核心原理,简单来说,是在程序运行中,记录每一条指令对程序执行的所有状态变化,包括变量、寄存器、内存的数据变化等,并将这些信息存储在一个history文件中。当需要回溯到过去的状态时,调试器会按照相反的顺序逐条指令恢复这些状态,使得程序的执行状态回到已经被记录的任意时间点。 因此,反向调试还有一个雅称 - Time Travel Debugging GDB反向调试用法GDB作为Linux下的调试神器,很早就支持了反向调试的功能,常用的命令有: 简单解释一下: - reverse-next(rc): 参考next(n), 逆向执行一行代码,遇函数调用不进入 在绝大多数环境下,在使用这些反向调试命令之前,必须要先通过record命令让GDB把程序执行过程中的所有状态信息全部记录下来。 通常是在程序运行起来之后,先设置断点让让程序停下来,然后输入record命令,开启状态信息记录,然后再继续执行。相关常用命令有:
还有其他一些不常用的命令,不再赘述,感兴趣的童鞋可以查看GDB帮助文档。 下面我们来深入研究下,反向调试到底有什么用,该怎么去用。 反向调试有什么用反向调试在定位这些类型的问题时会特别有用:
下面,以调用栈显示全是问号的问题为例,演示一下反向调试的使用方法。 实例:Segmentation fault时调用栈全是问号程序正常执行时,会发生Segmentation fault: 默认生产了core dump,用GDB直接调试core,直接bt: 居然全是问号!(注:编译时已经添加-g选项) 直接在GDB中运行试试看: 仍然全是问号!现在怎么办呢? 是不是有点慌了?淡定!咱们先来分析一下! 分析既然bt显示全是问号,那我们就从这个现象着手进行分析。 我们知道bt(backtrace)的作用是获取程序当前位置的函数调用关系。而函数调用关系,则是根据栈帧结构,从最深层次函数开始,逐层推导出来的。简单来说,就是逐层从栈中获取函数的返回地址,根据返回地址,从ELF的debug信息中找到对应的函数名、代码行号等信息。(函数栈帧结构不是本文重点,篇幅所限,不再赘述,感兴趣的小伙伴可以加我微信CreCoding讨论)。 现在,bt打印出来全是问号,说明GDB无法获取到所需要的信息,也就是说,栈中的数据被破坏了! 那么思考一下,我们该如何定位究竟是哪里把栈中数据给破坏了呢? printf?No!不解释! 直接单步调试吗?对应代码量不大,且逻辑简单的程序,这不失为一种可行的调试手段,但不够高效,往往需要反复猜测,反复重启程序调试才有可能找到真正的root cause。No! 直接数据断点?看起来确实是最简单有效,可问题是,往哪个地址设置断点呢?而且,如果程序正常运行过程中,会反复去修改这个地址里的数据呢?No! 反向调试?我们让程序正常运行,一直到触发segmentation fault。既然已经确定是栈数据被破坏,那么我们直接在栈上设置数据断点,然后让程序逆向执行,第一个触发数据断点的地方,肯定就是把这个错误的数据写到这个栈地址的代码,也就是破坏我们栈数据的地方!Yes!Yes! 思路有了,Let's Go! 用反向调试进行定位重新在GDB中运行程序,用record命令开始记录程序执行运行过程中的状态信息,然后让程序正常执行,直到发生segmentation fault停下来: 查看RSP寄存器,找到当前栈地址,然后在当前栈地址上设置数据断点: 我们直接在RSP地址处设置数据断点。需要解释一下的是,我所用的GDB版本中,在进行反向调试时,硬件数据断点会失效,因此需要通过“set can-use-hw-watchpoints 0”命令强制GDB使用软件断点。不知道最新的GDB版本中这个问题是否已经解决,感兴趣的童鞋可以去试一下。 然后,使用rc(reverse-continue)命令让程序开始逆向执行: 可以看到,我们上一步在栈上设置的数据断点被成功触发了,程序停在了第4行,给数组元素赋值的地方,看一下这个元素的地址: 确实是我们刚才设置的数据断点的地址。此时,再看下调用栈信息呢? 调用栈已经恢复正常!进一步证明,此时触发数据断点的这个地方,也就是第4行,就是破坏我们栈数据的罪魁祸首! 由于接下来都是常规调试,没有什么技术难点,篇幅所限,不再赘述。有疑问的童鞋可以加我微信CreCoding讨论!直接说结论:很容易分析出来,传入bar的数组长度超出了数组的大小,数组访问越界了,踩掉了栈中函数bar的返回地址,在return的时候返回到了一个非法地址,最终导致segmentation fault。 至此,BUG定位结束! 这段测试程序源码如下,建议亲自上手操作一下,加深理解: 调试过程如下: 反向调试的缺点前面介绍过,反向调试的实现原理是,调试器在程序执行每一条指令时,把这条指令所产生的效果记录下来,比如修改了寄存器的值、修改了内存中的值、跳转到另外一个地址等等,然后反向执行的时候,逐条把这些指令产生的效果还原回来,以此达到恢复程序执行状态的效果。 由此可见,这势必会在程序执行过程中产生不小的额外开销,对程序的性能产生比较大的影响。 不过,对于一些对性能不敏感的程序,或者规模不大的程序,可以不考虑这个问题。而对于一下规模较大且对性能非常敏感的程序,其实没有必要在程序执行一开始就记录执行轨迹状态信息,可以考虑分段记录,或者只记录自己感兴趣的程序段的状态信息。 思考题
|
|
来自: 西北望msm66g9f > 《培训》