GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

1、引言

在《

GDB高级技巧:边Debug边修复BUG,无需修改代码,无需重新编译》一文中,介绍了使用GDB breakpoint command lists的功能,可以在不修改源码、不重新编译的前提下,修复掉被调试程序中的BUG,从而避免反复修改代码和编译构建的过程,大大提高程序调试的效率。

文中提到了解决一个问题的几种不同的思路,但限于篇幅,只重点讲解了其中的一种。有童鞋希望能够把其它几种也讲解一下,于是便有了此文。

本文会介绍5种不同的方法,解决同一个Bug!

2、本文要解决的问题

如下示例程序中,存在两个BUG:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

图1 示例程序

正常执行时,该程序将出现异常,如下图所示:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

图2 原始程序执行异常

本文将介绍5种方法,在不修改源码、不重新编译的前提下,借助GDB解决掉示例程序中的两个BUG,使其能够正确执行,并得到预期结果。

其最终执行结果如下图所示:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

图3 程序最终可以正常执行

先介绍一些必需的背景知识。

3、背景知识介绍

3.1、x64 CPU函数参数传递

对于C语言,不同的CPU/编译器,其参数传递方式也不尽相同。

我的测试环境是x64CPU,所以仅介绍x64 CPU的函数参数传递规则。

用GCC编译出来的程序,在x64 CPU上,如果程序中没有特殊指定,会优先使用寄存器传递参数,当寄存器不够用的时候,用栈传递。

默认情况下,

前6个参数,从左到右,依次用寄存器RDI(EDI)、RSI(ESI)、RDX(EDX)、RCX(ECX)、R8、R9传递,从第七个函数开始,使用栈传递

注:RDI、RSI、RDX、RCX是64位寄存器,EDI、ESI、EDX、ECX分别是它们低32位寄存器的表示。

如下图简单示例:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

图4 参数传递示例

test()共有8个参数,我们在GDB中把main()函数反汇编一下,看下是如何传递参数的:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

图5 main反汇编

通过反汇编,可以看到,前面6个参数是使用寄存器传递的,而第7、8两个参数是通过栈传递的。

3.2、GDB断点预置命令

注:已阅读过之前那篇文章的童鞋,可以直接跳过3.1小节。当然, 也可以再回顾一下,加深印象!

GDB提供了一种功能,对于指定的断点,GDB允许用户预设一组操作(通常是调试命令),当断点被触发时,GDB会自动执行这组预设的操作。

这个功能,具体用法如下:

1、先设置一个或多个断点,包括breakpoint、watchpoint、catchpoint等各种类型的断点。

2、然后,用commands命令针对一个或多个断点预设一组操作,最后以end结尾即可。格式如下:

<code>commands [id...]
  command-list
end/<code>

其中:

  • commands关键字标记开始预设命令。
  • id是断点的ID,也就是用info break命令查询出来的断点编号。可以省略,也可以是一个或多个断点号。当省略时,表示对最近一次设置的断点有效。
  • command-list是用户预设的一组操作,可以是任意一个或多个GDB支持的命令,甚至是用户自定义的命令。
  • end标记结尾。

设置完成之后,每当id指定的断点被触发时,GDB便会自动执行command-list所指定的一组命令。

是不是很简单呢?善用这个功能,可以完成一些非常强大的功能。本文将利用这个功能,介绍如何在不修改源码、不重新编译的前提下,解决程序中的BUG。

有了这些背景知识之后,下面正式进入正题!

4、解决思路

再看一下示例程序:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

示例程序

包含两个BUG:

第15行和第17行均用sizeof取数组元素的个数,这明显是错误的。会造成do_stuff()中数组访问越界,进而破坏栈内数据,而且main()的for循环打印数组元素时,也会出错。

4.1、第15行的BUG

对于第15行的BUG,其实是传递给do_sutff()数组第二个参数错误。那么,解决这个问题,有两种思路:

  • 在do_sutff()中处理。尽管在main()函数传递给do_stuff()的参数是sizeof(array),但是从do_stuff()的角度来看,要想让它正确执行,只需要在for循环时,迭代次数不要超过数组的实际大小就可以了,也就是说,只要保证i<10,就可以得到正确的结果。
  • 在main()函数中处理。其实也就是,想办法把正确的参数传递给do_stuff()。

4.2、第17行的BUG

对于第17行的BUG,本质上只要保证for循环迭代次数不超过数组的实际大小就可以了,也就是必须要保证i<10。

下面我们根据这个思路,来解决这两个BUG。为了方便演示,我们先解决第17行的BUG。

5、解决第17行的BUG

上面已经分析过,只要能保证i<10就可以解决第17行的BUG。我们可以这样做:

  • 在第18行设置条件断点,当i==10时触发。
  • 在断点触发时,使用GDB的jump命令,退出循环,跳转到第20行继续执行。

命令如下:

<code>b 18 if i==10
commands
  jump 20
  continue
end/<code>

执行效果如下:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

程序仍然执行异常,这是因为第15行的BUG还没解决。不过,它只打印出来了10个元素,说明第17行的BUG已经解决了。

接下来,解决第15行的BUG。

6、解决第15行的BUG

我们根据上面提到的两种思路,采用5种不同的方法来解决这个BUG。

6.1、在do_stuff()中处理

  • 方法一:在do_stuff()中第一条指令执行前,修改ESI寄存器的值。

前面背景知识介绍过,x64上,优先采用寄存器传递参数,因此do_stuff()的第二个参数,按照顺序应该使用ESI来传递。如下图所示:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

但是,do_stuff()的参数size,本质上也是一个局部变量,存放在栈中,它的初始值是从寄存器ESI中取得的。因此,只要在对变量size初始化前,把ESI寄存器的值修改掉,变量size就能被初始化成正确的值。

因此,我们把断点设置在do_stuff()入口第一条指令处,因为此时局部变量size还没有被初始化,然后把ESI寄存器的值修改为数组array的正确大小:10。

命令如下:

<code>b *do_stuff
commands
  printf "\n ESI = %d\n",$esi
  set $esi=10
  printf "\n ESI = %d\n",$esi
  continue
end/<code>

注意,设置断点时,必须用*do_stuff,只有这样才能把断点设置在do_stuff()的第一条指令处。

另外,为了确定它是否正常工作,我们在修改ESI前后,把ESI的值打印出来。

看一下效果:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

可以看出,ESI的值确实被修改为10,局部变量size也被初始化为10。

然后,对上面的命令作一些优化,并且和前面用来解决第17行BUG的命令结合起来,制作一个“热补丁”脚本文件:

<code>vi test.fix.1/<code>

这样,GDB调试时,只需要加载test.fix.1就可以了,它的内容如下图:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.1

相比上面手动输入的命令,这里做了两点优化:

  1. 加入silent命令,屏蔽断点被触发时的打印信息,避免视觉干扰。
  2. 删除修改ESI前后的打印语句。

现在,我们用GDB重新调试运行test,并用-x参数加载“热补丁”文件test.fix.1,结果如下图:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.1执行结果

程序最终正常运行,并且得到预期结果!

  • 方法二:在do_stuff()中for循环执行之前,修改size的值

我们只要保证,在第5行代码的for循环执行之前,把变量size的值,修改为数组的正确大小10就可以了。

可以在第5行设置断点,断点被触发时,把变量size的值改为10。结合上文修复第17行BUG的操作,完整命令如下:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.2

保存到test.fix.2文件中。执行结果如下:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.2执行结果

正常执行,得到预期结果!

  • 方法三:在do_stuff()中循环中判断如果i==10,则跳出循环

这次,我们不去修改参数size的值,而是在for循环中,判断如果i==10,则跳出循环,对于do_stuff()来说,直接return出去就可以了。

在第7行设置条件断点,当i==10时触发,然后执行return命令,从do_stuff()中返回。

完整命令如下:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.3

保存为文件test.fix.3,执行结果如下图:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.3执行结果

从GDB中运行test,程序正常运行,得到预期结果!

6.2、在main()函数中处理

  • 方法四:在main()调用do_stuff()函数前修改ESI寄存器的值

在方法一种,我们是在do_stuff()函数中修改了ESI寄存器的值,使得局部变量size被初始化成正确的数组大小。

这次,我们尝试在main()函数中调用do_stuff()函数的时候,就把ESI寄存器修改掉,从而确保最终传递给do_stuff()的参数是正确的。

首先,要确认main()函数中,真正调用do_stuff()函数的指令地址。

有些童鞋可能会有疑问,为什么一定要知道指令地址呢?不是可以直接用下面的命令来设置断点吗?

<code>b 15/<code>

实际上,这样是不行的,我们在GDB中看一下:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

main() 反汇编

从反汇编可以看到,如果我们直接在第15行设置断点,终止程序会断在偏移量为100的那条指令。但此时,main()还没有给ESI寄存器赋值,也就是还没有给do_stuff()开始传参数,因此,即便我们在这里把ESI的值改为10,等执行到偏移量为104的指令时,ESI寄存器仍然会被覆盖为0x28,也就是sizeof(array)。

从汇编看,真正调用do_stuff()的地方是偏移量为112的那条指令,所以,我们在那条指令执行前,把ESI寄存器的值设置为10,就可以了。

完整命令如下:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.4

其中,b *main+112,表示在以main函数为起始地址,向后偏移112的位置设置断点,也就是真正调用do_stuff()的那条callq指令。

保存为文件test.fix.4,执行效果如下:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.4执行结果

程序在GDB中正常执行,得到预期结果!

  • 方法五:在main函数中跳过do_stuff()调用,然后用正确的参数手动执行do_stuff()

在main()函数第15行代码是这样调用do_stuff()的:

<code>do_stuff(array, sizeof(array)); /* BUG: 取数组元素个数不能用sizeof *//<code>

我们知道它传递的参数是错误的。既然如此,干脆就不要让它执行这句代码,我们自己来手动调用do_stuff()函数,并且给它传递正确的参数就可以了:

<code>call do_stuff(array, 10)/<code>

要达到这个目的,只需要在第15行代码处设置断点,等断点触发后,手动调用do_stuff(),然后跳转到第17行继续执行。

完整操作命令如下:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.5

保存到文件test.fix.5,然后执行,结果如下图:

GDB高级技巧:同一个Bug,5种解决方案,不修改源码,不重新编译

test.fix.5执行结果

程序正常执行结束,得到预期结果!

结语

由于之前一篇文章已经介绍过GDB断点预设命令的用法,因此本文介绍相对简单,有不明白的童鞋,建议先去阅读一下那篇文章,或者留言讨论!

本文旨在介绍几种解决问题的思路,每种方法都有其各自适用的场景。

最后留一个思考题:

如果被调试程序编译时,没有加-g选项,也就是没有调试信息,文中的5种方法,哪种比较适合呢?或者有其它更好的方法吗?欢迎留言讨论!

本文是调试系列专题的第八篇,对程序调试技术感兴趣的童鞋,强烈建议阅读一下其它已更新的文字,一定会有所收获!

GDB高级技巧:边Debug边修复BUG,无需修改代码,无需重新编译

段错误(segmentation fault ):9种实用调试方法,你用过几种?

GDB动态打印:让你随时随地printf,不需修改代码,不需重新编译

调试引入的不确定性:必现的BUG神秘消失,断点改变代码执行逻辑

Linux调试技巧:GDB自定义命令,按需定制适合自己的调试工具

C语言:当GDB遇到复杂数据结构,两分钟带你掌握四个高效调试技巧

C语言:GDB调试时遇到宏定义怎么办?一个小技巧帮你一秒钟搞定

原创不易,别忘了转发点赞,给作者继续更新的动力!同时,也把知识分享给更多志同道合的朋友,谢谢!

对编译器、OS内核、性能调优、虚拟化等技术感兴趣的童鞋,欢迎右上角关注!

有任何疑问、建议,欢迎留言讨论!

版权声明:原创文章,未经允许,禁止转载。


分享到:


相關文章: