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

调试是每个程序员都逃不过的宿命!

程序调试是一件非常考验耐心的事情,因为调试过程中经常会需要反复的修改源码,重新编译、重新部署、重新运行,这个过程通常是非常枯燥和繁琐的。尤其对于大型项目,光是编译可能需要几十分钟,甚至几个小时,部署过程则可能更为复杂漫长!

那么,有没有一种更高效的调试手段,可以避免反复修改代码和编译呢?

这个真的有!本文将介绍一种调试技巧,可以一边调试,一边修复Bug,能够在不修改代码、不重新编译的前提下修复BUG,并且验证解决方案,大幅提高调试效率!

先看下最终效果吧!

本文预期效果

如下图,冒泡排序中,有三个常见的BUG:

img

图中已经把三个BUG都标注了出来。编译运行,结果如下:

img

GDB中执行时:

img

不管是正常方式执行,还是在GDB中执行,程序都异常终止,无法得到正常结果。

但是,利用本文介绍的调试技巧,可以利用GDB给这个程序制作一个“热补丁”,在不修改代码、不重新编译的前提下,解决掉程序中的三个BUG,让程序正常执行,并得到预期结果!

最终效果,如下图所示:

img

所有的黑魔法,都在这个补丁文件bubble.fix中!

是不是很有趣呢?下面开始介绍!

关于GDB

我之前写了几篇文章,专门介绍GDB的一些非常实用却鲜为人知的高阶用法,感兴趣的小伙伴可以去翻看下调试系列专题文章。

GDB的基本用法,相信大家都很熟悉了,就不过多介绍了,直接讲重点吧!

Breakpoint Command Lists

GDB支持在断点触发后,自动执行用户预设的一组调试命令。使用方法:

commands [bp_id...]
  command-list
end

其中:

  • commands是GDB内置关键字。
  • bp_idi(info)命令显示出来的断点ID,可以指定多个,也可以不指定。不指定时,默认只对最近一次设置的那个断点有效。
  • command-list是用户预设的一组命令,当bp_id指定的断点被触发时,GDB会自动执行这些命令。
  • end表示结束。

简单来说,就是当bp_id所表示的断点被触发时,GDB会自动执行command-list中所指定的命令。

这个功能适用于各种类型的断点,如breakpoint、watchpoint、catchpoint等。

适用场景举例

利用GDB的breakpoint commands lists这个特性可以做很多有趣的事情,本文仅列举其中的几个。

随时随地printf,不需修改代码和重新编译

我之前写过一篇文章,详细介绍过GDB的动态打印(Dynamic Printf)功能,可以用dprintf命令在代码的任意地方设置动态打印断点,并自动进行格式化打印。相当于在不修改代码,不重新编译的情况下,可以让你随意添加printf打印日志信息。

利用GDB的breakpoint commands lists,可以实现一样的功能,而且除了格式化打印之外,还可以做其它更多的操作,比如dump内存,dump寄存器等。

修改程序执行逻辑

在GDB中可以做很多有趣的事情,比如修改变量、修改寄存器、调用函数等。结合breakpoint command list功能,可以在调试的同时,修改程序执行逻辑,给程序打上"热补丁"。从而可以在调试过程中,快速修复Bug和验证解决方案,避免重新修改代码和重新编译,大大提高程序调试的效率!

这也是本文重点讲解的场景,稍后会演示如何利用这个功能,在调试的过程中,不修改代码,就能修复掉上文冒泡排序程序中的三个Bug。

进行自动化调试,提高调试效率

很多童鞋可能不知道,GDB支持非常强大的脚本功能,除了GDB自己特定的脚本外,它甚至还支持Python脚本!

有了breakpoint commands lists功能,结合GDB支持的脚本功能,以及自定义命令功能,甚至可以实现调试自动化。

其他还有很多非常有趣且实用的功能场景,限于篇幅,不再展开,有机会再写文章专门介绍吧!

接下来,正式开始解决冒泡排序的三个Bug!

给冒泡排序打上"热补丁"

现在,我们利用GDB的breakpoint command lists功能,给文中的冒泡排序程序打上"热补丁",演示如何在不修改源码、不重新编译的前提下,解决掉程序中的三个BUG。

再看一下示例程序:

img

解决第一个BUG

先解决第22行的BUG:数组arr元素个数是10,但是传递给了bubble_sort()的参数却是sizeof(arr),也就是40。

要解决这个BUG,我们只需要把参数修改成正确的值就行了。

我们知道,在x64上,优先采用寄存器传递函数参数。那么,有这几种方式可以选择:

  • • 把断点设置在bubble_sort()入口第一条指令,然后直接修改存放数组长度n的那个寄存器中的值。
  • • 把断点设置在bubble_sort()入口处(不必是第一条指令),在第7行for循环之前,把存放数组长度的变量n的值改掉。
  • • 把断点设置在main()函数第22行,也就是调用bubble_sort()的地方,然后以正确的参数手动调用bubble_sort()函数,并利用GDB的jump命令,跳过第22行代码的执行。

考虑到有些童鞋对x64 CPU不是非常了解,或者对GDB的jump命令不熟悉,我们采用第2种方式。而且,这种方式也更简单通用。

我们先在bubble_sort()函数设置断点,然后利用commands命令预设一条命令,把变量n的值修改为10。命令如下:

b bubble_sort
commands 1
  set var n=10
end

设置完之后,用run命令开始运行程序。结果如下:

img

bubble_sort()处的断点被触发后,程序暂停,用p(print)命令查看变量n的值,已经被修改成了正确的值:10。

可见,我们的设置是有效的。

断点触发后,让程序自动恢复执行

bubble_sort()处断点被触发,程序停了下来,修改完变量n的值后,怎么自动恢复执行呢?

很简单,只需要在预设的命令中添加一个continue命令就可以了。为了证明我们的设置确实是生效的,在修改变量n的前后,各添加一个格式化打印语句,把变量n的值打印出来:

b bubble_sort
commands 1
  printf "The original value of n is %d\n",n
  set var n=10
  printf "Current value of n is %d\n",n
  continue
end

结果如下图:

img

从运行结果可以看出,断点被触发后,我们预设的语句被正确执行,变量n的值被修改为10,然后程序自动恢复执行。虽然最终程序不会发生segfault了,但打印出来的排序结果仍然是错的!不着急,还有两个BUG没解决呢!

到此,第一个BUG已经解决了。

解决第二个BUG

下面,开始解决第7行的数组访问越界BUG:数组的元素个数是n,但是bubble_sort()中第一个for循环的终止条件是i<=n,明显会造成访问越界,正确的条件应该是i<n

要解决这个BUG也很简单,只需要在执行第8行代码之前,判断如果i的值等于n,就跳出循环。对于这个简单的程序,我们直接从bubble_sort()函数return就可以了。

命令如下:

b 8 if i==n
command 2
  printf "Current i = %d, n = %d\n",i,n
  return
  continue
end

在第8行设置条件断点,当i==n时断点被触发,然后自动把in的值打印出来,再行return命令,从bubble_sort()返回,然后continue命令自动恢复程序执行。

执行结果如下图:

img

解决第三个BUG

下面,解决最后一个BUG,第23行数组访问越界错误:数组arr的长度应该是10,不是sizeof(arr)

解决思路与第二个BUG类似,在第24行设置条件断点,当i==10时触发断点,然后用jump命令跳出循环,让程序跳转到第26行继续执行。命令如下:

b 24 if i==10
commands 3
  printf "i=%d, exit from for loop!\n",i
  jump 26
  continue
end

执行结果如下图所示:

img

从图中可以看出,三个断点全部被触发,并且预设的命令都正常执行。最终程序正常结束,我们终于得到了正确的执行结果!

虽然,现在程序可以正常执行了,但每次都要手动输入这么多命令,想想都觉得麻烦!我之前文章介绍过,GDB支持调试脚本,可以从脚本中加载并执行调试命令。

下面,利用GDB脚本,来制作我们的“热补丁”文件。

制作"热补丁"脚本

把上文中用来解决三个BUG的命令保存在一个脚本文件中:

vi bubble.fix

脚本内容如下图:

img

bubble.fix脚本中的命令,与上文在GDB中直接输入的命令有几个区别:

  • • 删除了格式化打印信息。
  • • 删除了commands后面的断点ID。上文讲过,commands后面的断点ID可以省略,表示对最近一次设置的断点有效。为了让脚本更加通用,每个commands都紧跟在break命令之后,因此直接省略了断点ID。

GDB的脚本可以通过两种方式执行:

  • • 启动GDB时,用-x参数指定要执行的脚本文件。
  • • 启动GDB后,执行source命令执行指定的脚本。

下面,我们用第二种方式演示一下,如下图所示:

img

使用source命令加载并执行bubble.fix,然后用run命令执行程序,三个断点均被触发,且预设的命令全部被正确执行,最后程序运行正常,得到期望的结果!

我们现在可以利用我们制作的"热补丁"脚本,在不修改代码、不重新编译和部署的前提下,成功修复程序中的BUG!是不是很有趣呢?

不过,做到这种程度,还是有点瑕疵。虽然得到了正确的结果,但程序执行时,总是会打印断点信息,造成视觉干扰,作为典型的"伪完美主义者",这怎么能忍!

最后,我们来解决这个问题,让我们的"热补丁"更加完美!

优化"热补丁"脚本,隐藏断点信息

在预设的命令中,如果第一条命令是silent,断点被触发的打印信息会被屏蔽掉。

我们把bubble.fix做些修改,把silent命令加进去,如下图所示:

img

此外,在最后面加了一个run命令,这样就不用每次手动执行了。

然后,我们换一种方式来执行:

img

这样,看起来,清爽多了!

到此,我们终于实现了最终的目标:一边debug,一边修复BUG,并验证解决方案,避免反复修改代码、重新编译和部署、提高调试效率!

原文链接:https://zhuanlan.zhihu.com/p/698084327?utm_campaign=shareopn&utm_medium=social&utm_psn=1782817623383240705&utm_source=wechat_session

posted @ 2024-06-10 12:57  学习,积累,成长  阅读(122)  评论(0编辑  收藏  举报