工欲善其事,必先利其器。
GDB作为Linux下最常用的调试工具,它的强大,毋庸置疑。但现实中,发现身边很多人对它的一些高级特性却知之甚少,调试问题的时候,经常把大把的时间浪费在一些原本很简单的事情上,效率非常低下。
本文以C语言中的链表节点打印为例,讲解如何通过自己定制GDB调试命令,让一件原本看似非常复杂的事情,变得前所未有的简单。
示例
在这个示例中,创建了一个包含5个节点的链表,每个链表节点的类型定义为:
完整程序如下图所示:
假设我们调试程序时,想把这个链表的所有节点都打印出来,通常大家会怎么做呢?
最原始的做法:逐个节点手动打印
通常,首先想到的,是使用GDB的print命令,从链表头开始,逐个节点去手动打印。简单演示一下。
先编译一下:
gcc -g list.c -o list
然后用GDB进行调试,先在第34行设置一个断点,保证链表已经被创建出来,并且让程序在return之前停下来:
然后,使用print命令,逐个节点去打印:
这种手动打印的方式,在节点个数非常少的情况下,还是非常简单实用的。
但是,如果链表中有几十个或者更多的节点呢?这个时候用手动的方式去打印,这个工作量可想可知会有多大了。
那么,除了手动的方式之外,有没有更好的方式呢?
GDB自定义命令基础
GDB有一个非常强大的功能,它允许用户根据具体场景需求自定义命令。
具体格式如下:
define cmd_name command list end
自定义命令以define开头,后面跟命令的名字,并且以end结尾。define和end中间是命令的具体实现逻辑。
与自定义命令相关的几个GDB内建变量:
$argc 自定义命令的参数个数 $arg0 自定义命令的第一个参数 $arg1 自定义命令的第二个参数 $arg2 自定义命令的第三个参数 $argN 自定义命令的第N+1个参数
比如,我们要实现一个两个整数相加的命令,实现如下:
define add print $arg0 + $arg1 end
执行效果如下图所示:
简单了解GDB自定义命令的使用方法之后,我们现在来解决我们示例中的链表打印问题。
自定义GDB命令:链表打印
假如用C语言实现一个链表打印函数,我们可能会这样实现:
接下来,我们仿照C语言,用GDB自定义命令来实现:
其实和C语言实现的打印函数是很相似的,简单解释一下:
- 第1行 定义链表打印名字为“print-list”
- 第2行 设置一个$list变量,并且把第一个参数赋值给它,也就是链表头指针
- 第3~5 行用while命令循环遍历$list链表
- 第4行 用print命令打印当前$list变量指向的节点
下面,我们来验证一下,这个自定义命令是否有效。
我在之前的文章中介绍过,GDB支持从脚本文件中加载信息,其实,自定义的命令也可以从脚本中加载。
我们先把上面实现的脚本命令存放在一个print-list.gdb文件中,调试时使用source命令把它加载起来即可。如下图所示:
一切工作正常!是不是比逐个节点手动打印,简单多了呢?
这样虽然方便了,可是如果链表中有几百个节点,它会把这些节点全部打印出来。但有的时候,我们想查看的可能只是前面的几个节点,那怎么办呢?
下面,我们来解决这个问题。
GDB自定义命令:打印指定个数的链表节点
其实,要实现这个功能非常简单,只需要给print-list命令增加一个指定节点个数的参数就可以了。
我们重新定义一个print-list-2命令,相比print-list命令,它新引入了一个$count变量,用来接收用户指定的要打印节点的个数。
关键的地方我已经在图中进行了标注,应该还是比较好理解的。
$count初始值为用户指定的节点个数,每次打印一个节点后,$count值减1,当$count值小于等于0时,用loop_break退出循环。
我们仍然把print-list-2命令添加到print-list.gdb文件中,然后重新用GDB进行调试,并加载命令,然后打印3个节点:
可以看到,完全符合预期,print-list-2打印了3个节点后,就执行结束了。
print-list-2命令存在的问题
目前print-list-2命令虽然很方便,但是有一个很大的问题,就是它无法通用。
它主要有两个问题:
- 第2行中,显式地指明了节点类型是type_t
- 第9行中,显示地指明了指向下一个节点的字段名是next
我们再看一下print-list-2的实现,我用红线把问题在图中标注出来:
下面,我们来解决这个问题,最终实现一个更加通用的GDB自定义链表打印命令。
GDB自定义命令:通用的链表打印
这个问题也很好解决,我们只需要能够让用户把节点的类型,和节点结构中指向下一个节点的字段的名字传递给我们的打印命令就可以了。
实现如下图所示:
我们新定义一个print-list-4命令,并且新引入两个参数,$arg2表示节点的数据类型,$arg3表示节点结构中指向下一个节点的字段名。
同样,我们把print-list-4的实现存放到print-list.gdb文件中,然后用GDB重新调试并加载自定义的命令。如下图所示:
到此,我们已经实现了一个相对来说比较通用的GDB自定义链表打印命令了。
下面,为了让我们的自定义命令显得更加正式一些,给它添加上使用说明。
GDB自定义命令:添加使用说明
可以使用document - end命令给GDB自定义命令添加使用说明。
如此以来,我们就可以在GDB中使用help命令查看自定义命令的使用方法了。
给print-list-4命令添加使用说明,如下图所示:
把print-list-4的帮助说明同样添加到print-list.gdb文件中,然后用GDB重新调试程序,并从脚本文件中加载自定义命令信息。
这样,就可以在GDB中用help命令查看print-list-4的使用帮助了。
如下图所示:
到此,基本功能已经全部实现完毕了。
但我们自定义的print-list-4命令名字还是稍显复杂,使用起来稍有不便。
当然,我们可以在定义的时候起一个更加简单的名字,不过,这里我们使用另外一种方法。
GDB自定义命令:命令别名
我们知道,在GDB中,很多命令都有对应的缩写形式。比如break的缩写是b,info的缩写是i,continue的缩写是c等。
在GDB中,可以使用alias命令,给已经存在的命令设置一个命令别名。
下面,给我们自定义的print-list-4命令也设置一个简写简写形式,使用起来更加方便。
alias pl = print-list-4
把这个命令,放在print-list-4命令所在的脚本文件中即可,也就是print-list.gdb文件中。
下面我们用GDB重新调试一下,看一下效果:
我们用help命令查看pl的使用帮助时,GDB自动找到了print-list-4的使用说明。
再看一下使用效果:
好了,现在我们可以用更加容易拼写的pl命令来打印链表信息了!是不是方便好多呢?
本文结束!更多关于程序调试、性能优化、编译器、操作系统等内容请关注微信公众号【原点技术】
欢迎关注微信公众号:【原点技术】,分享真正有用的东西!技术探讨,欢迎添加作者微信:CreCoding
原创文章,未经允许禁止转载,转载请联系作者:CreCoding