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

引言

注:本文介绍的调试技巧非常实用,请耐心看下去,相信会有收获的!

工欲善其事,必先利其器。

熟悉并善用手中的调试工具,很多时候,会带来事半功倍的效果!

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语言 链表打印

接下来,我们仿照C语言,用GDB自定义命令来实现:

GDB 链表打印命令

其实和C语言实现的打印函数是很相似的,简单解释一下:

  • 第1行 定义链表打印名字为“print-list”
  • 第2行 设置一个$list变量,并且把第一个参数赋值给它,也就是链表头指针
  • 第3~5 行用while命令循环遍历$list链表
  • 第4行 用print命令打印当前$list变量指向的节点

下面,我们来验证一下,这个自定义命令是否有效。

我在之前的文章中介绍过,GDB支持从脚本文件中加载信息,其实,自定义的命令也可以从脚本中加载。

我们先把上面实现的脚本命令存放在一个print-list.gdb文件中,调试时使用source命令把它加载起来即可。如下图所示:

print-list 命令执行

一切工作正常!是不是比逐个节点手动打印,简单多了呢?

这样虽然方便了,可是如果链表中有几百个节点,它会把这些节点全部打印出来。但有的时候,我们想查看的可能只是前面的几个节点,那怎么办呢?

下面,我们来解决这个问题。

GDB自定义命令:打印指定个数的链表节点

其实,要实现这个功能非常简单,只需要给print-list命令增加一个指定节点个数的参数就可以了。

print-list-2命令

我们重新定义一个print-list-2命令,相比print-list命令,它新引入了一个$count变量,用来接收用户指定的要打印节点的个数。

关键的地方我已经在图中进行了标注,应该还是比较好理解的。

$count初始值为用户指定的节点个数,每次打印一个节点后,$count值减1,当$count值小于等于0时,用loop_break退出循环。

我们仍然把print-list-2命令添加到print-list.gdb文件中,然后重新用GDB进行调试,并加载命令,然后打印3个节点:

print-list-2 命令执行

可以看到,完全符合预期,print-list-2打印了3个节点后,就执行结束了。

print-list-2命令存在的问题

目前print-list-2命令虽然很方便,但是有一个很大的问题,就是它无法通用。

它主要有两个问题:

  • 第2行中,显式地指明了节点类型是type_t
  • 第9行中,显示地知名了指向下一个节点的字段名是next

我们再看一下print-list-2的实现,我用红线把问题在图中标注出来:

print-list-2 的问题

下面,我们来解决这个问题,最终实现一个更加通用的GDB自定义链表打印命令。

GDB自定义命令:通用的链表打印

这个问题也很好解决,我们只需要能够让用户把节点的类型,和节点结构中指向下一个节点的字段的名字传递给我们的打印命令就可以了。

实现如下图所示:

print-list-4 GDB自定义命令

我们新定义一个print-list-4命令,并且新引入两个参数,$arg2表示节点的数据类型,$arg3表示节点结构中指向下一个节点的字段名。

同样,我们把print-list-4的实现存放到print-list.gdb文件中,然后用GDB重新调试并加载自定义的命令。如下图所示:

print-list-4 执行

到此,我们已经实现了一个相对来说比较通用的GDB自定义链表打印命令了。

下面,为了让我们的自定义命令显得更加正式一些,给它添加上使用说明。

GDB自定义命令:添加使用说明

可以使用document - end命令给GDB自定义命令添加使用说明。

如此以来,我们就可以在GDB中使用help命令查看自定义命令的使用方法了。

给print-list-4命令添加使用说明,如下图所示:

print-list-4 使用说明

把print-list-4的帮助说明同样添加到print-list.gdb文件中,然后用GDB重新调试程序,并从脚本文件中加载自定义命令信息。

这样,就可以在GDB中用help命令查看print-list-4的使用帮助了。

如下图所示:

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重新调试一下,看一下效果:

print-list-4 命令别名

我们用help命令查看pl的使用帮助时,GDB自动找到了print-list-4的使用说明。

再看一下使用效果:

pl 命令执行

好了,现在我们可以用更加容易拼写的pl命令来打印链表信息了!是不是方便好多呢?

结语

本文以C语言的链表打印为例,展示了使用GDB的自定义命令,可以把一件原本非常复杂的事情,变得如此简单。

其实,GDB自定义命令的用途还远不止如此,它可以完成很多非常实用的功能,比如控制程序执行流,自动化调试等各种强大的功能。但由于篇幅有限,本文不再展开介绍。

本系列专题是应有些朋友的要求,旨在介绍一些非常简单实用,又相对比较高阶的调试技巧。同时也会对调试器的实现原理进行详细讲解,还会讲解一些常见问题的定位方法和思路。感兴趣的朋友可以关注一下!

本文是本系列专题的第四篇,感兴趣的朋友可以去看下其它几篇,相信你会有收获的!

已更新内容:

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

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

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



原创不易,如果觉得有用的话,别忘了点赞,谢谢!

对文中的内容有什么疑问或者不同见解的朋友,欢迎留言讨论!

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

原创声明:本文原创内容,未经允许,禁止转载!部分图片来源网络,如有侵权,请通知删除!

举报
评论 0