【转载】GDB高级技巧:边Debug边修复BUG,无需修改代码,无需重新编译
调试是每个程序员都逃不过的宿命!
程序调试是一件非常考验耐心的事情,因为调试过程中经常会需要反复的修改源码,重新编译、重新部署、重新运行,这个过程通常是非常枯燥和繁琐的。尤其对于大型项目,光是编译可能需要几十分钟,甚至几个小时,部署过程则可能更为复杂漫长!
那么,有没有一种更高效的调试手段,可以避免反复修改代码和编译呢?
这个真的有!本文将介绍一种调试技巧,可以一边调试,一边修复Bug,能够在不修改代码、不重新编译的前提下修复BUG,并且验证解决方案,大幅提高调试效率!
先看下最终效果吧!
本文预期效果
如下图,冒泡排序中,有三个常见的BUG:
图中已经把三个BUG都标注了出来。编译运行,结果如下:
GDB中执行时:
不管是正常方式执行,还是在GDB中执行,程序都异常终止,无法得到正常结果。
但是,利用本文介绍的调试技巧,可以利用GDB给这个程序制作一个“热补丁”,在不修改代码、不重新编译的前提下,解决掉程序中的三个BUG,让程序正常执行,并得到预期结果!
最终效果,如下图所示:
所有的黑魔法,都在这个补丁文件bubble.fix
中!
是不是很有趣呢?下面开始介绍!
关于GDB
我之前写了几篇文章,专门介绍GDB的一些非常实用却鲜为人知的高阶用法,感兴趣的小伙伴可以去翻看下调试系列专题文章。
GDB的基本用法,相信大家都很熟悉了,就不过多介绍了,直接讲重点吧!
Breakpoint Command Lists
GDB支持在断点触发后,自动执行用户预设的一组调试命令。使用方法:
commands [bp_id...]
command-list
end
其中:
commands
是GDB内置关键字。bp_id
是i(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。
再看一下示例程序:
解决第一个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命令开始运行程序。结果如下:
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
结果如下图:
从运行结果可以看出,断点被触发后,我们预设的语句被正确执行,变量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
时断点被触发,然后自动把i
和n
的值打印出来,再行return
命令,从bubble_sort()
返回,然后continue
命令自动恢复程序执行。
执行结果如下图:
解决第三个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
执行结果如下图所示:
从图中可以看出,三个断点全部被触发,并且预设的命令都正常执行。最终程序正常结束,我们终于得到了正确的执行结果!
虽然,现在程序可以正常执行了,但每次都要手动输入这么多命令,想想都觉得麻烦!我之前文章介绍过,GDB支持调试脚本,可以从脚本中加载并执行调试命令。
下面,利用GDB脚本,来制作我们的“热补丁”文件。
制作"热补丁"脚本
把上文中用来解决三个BUG的命令保存在一个脚本文件中:
vi bubble.fix
脚本内容如下图:
bubble.fix脚本中的命令,与上文在GDB中直接输入的命令有几个区别:
- • 删除了格式化打印信息。
- • 删除了
commands
后面的断点ID。上文讲过,commands
后面的断点ID可以省略,表示对最近一次设置的断点有效。为了让脚本更加通用,每个commands
都紧跟在break
命令之后,因此直接省略了断点ID。
GDB的脚本可以通过两种方式执行:
- • 启动GDB时,用
-x
参数指定要执行的脚本文件。 - • 启动GDB后,执行
source
命令执行指定的脚本。
下面,我们用第二种方式演示一下,如下图所示:
使用source命令加载并执行bubble.fix,然后用run
命令执行程序,三个断点均被触发,且预设的命令全部被正确执行,最后程序运行正常,得到期望的结果!
我们现在可以利用我们制作的"热补丁"脚本,在不修改代码、不重新编译和部署的前提下,成功修复程序中的BUG!是不是很有趣呢?
不过,做到这种程度,还是有点瑕疵。虽然得到了正确的结果,但程序执行时,总是会打印断点信息,造成视觉干扰,作为典型的"伪完美主义者",这怎么能忍!
最后,我们来解决这个问题,让我们的"热补丁"更加完美!
优化"热补丁"脚本,隐藏断点信息
在预设的命令中,如果第一条命令是silent
,断点被触发的打印信息会被屏蔽掉。
我们把bubble.fix做些修改,把silent
命令加进去,如下图所示:
此外,在最后面加了一个run
命令,这样就不用每次手动执行了。
然后,我们换一种方式来执行:
这样,看起来,清爽多了!
到此,我们终于实现了最终的目标:一边debug,一边修复BUG,并验证解决方案,避免反复修改代码、重新编译和部署、提高调试效率!