本系列专题文章,旨在介绍一些非常实用,却不为大多数人所知的调试技巧。灵活运用这些调试技巧,能够轻松解决一些我们经常遇到并为之困惑的问题,大大提高问题定位和调试的效率。
感兴趣的童鞋,欢迎右上角关注!
引言
数据结构和算法是程序设计的灵魂,具有复杂逻辑的软件项目中,必定存在精心设计的复杂数据结构和精妙的算法。
然而,越是复杂的数据结构和算法,往往会给程序调试和代码理解带来不少的挑战。
作为程序员,我们经常要使用调试手段来进行Bug定位,或者理解代码的实现细节和业务逻辑。
![2056a89b0e9bf0dbae670c7c2fe5c1ee.png](https://img-blog.csdnimg.cn/img_convert/2056a89b0e9bf0dbae670c7c2fe5c1ee.png)
使用GDB调试时,经常会遇到一些非常复杂的数据结构。当我们需要查看它的内容时,GDB虽然可以正确输出它各个字段的值,但它的显示结果却不那么友好,甚至十分混乱。
我们看一个例子。
实例:GDB打印复杂数据结构
测试程序如下图所示:
![bca4859aa6aac8f62e6aff31f510e9f5.png](https://img-blog.csdnimg.cn/img_convert/bca4859aa6aac8f62e6aff31f510e9f5.png)
较复杂的数据结构
编译一下:
gcc -g struct.c -o struct
然后用GDB进行调试
gdb ./struct
为了照顾对GDB不熟悉的童鞋,让他们了解GDB的基本调试命令,我简单介绍下操作步骤:
- b(break的缩写)命令在main()函数入口设置断点
- r(run的缩写)命令开始执行程序,程序执行到main函数时,触发断点。
- n(next的缩写)命令进行单步执行。
- 等程序完成变量var的初始化之后,使用p(print的缩写)命令打印变量var的内容。
具体操作如下图所示:
![9ce7a62a6282e4b5c0e80b5af28ffc60.png](https://img-blog.csdnimg.cn/img_convert/9ce7a62a6282e4b5c0e80b5af28ffc60.png)
GDB default print
GDB确实把变量var的每个字段都正确打印了出来,但看着这个格式,是不是觉得比较混乱呢?
其实,在这个案例中,var的类型还是相对比较简单的结构体,只有几个字段而已。在真实项目中,数据结构往往会更加复杂,可能包含几十甚至几百个字段。而且,会有多个不同类型的结构体层层嵌套。可想而知,如果直接以这样的格式打印出来,会是多么的混乱,想找到真正所需要的信息,也是一件比较头疼的事情。
![3fb8c04fb3c179d1f7484c8857ab85ea.png](https://img-blog.csdnimg.cn/img_convert/3fb8c04fb3c179d1f7484c8857ab85ea.png)
那么,如何解决呢?很简单,一个简单的命令就可以搞定!
GDB pretty-print
对于这个问题,其实,GDB已经很贴心地为我们想好了解决方案。它内置了pretty-print的功能,能够以更加直观的方式打印数据结构。
不过,GDB的这个功能是默认关闭的,我们现在把它打开,看一下效果。
用下面这条GDB命令:
set print pretty on
效果如下图所示:
![63c288bf7625e944a731d40b5a574e4b.png](https://img-blog.csdnimg.cn/img_convert/63c288bf7625e944a731d40b5a574e4b.png)
GDB pretty-print
现在,看上去是不是清爽多了呢?每个字段都一目了然。
但是,还不够完美。
如图中所示,var中有两个数组类型的字段,GDB按照顺序把每个元素逐个打印了出来。但是,却很难把某个具体的值和数组索引对应起来。试想一下,如果数组有上百个元素,要找到其中某个特定索引的元素的值,岂不是要从头开始一个一个地数过去?
不用担心,GDB也提前帮我们准备了解决方案。
GDB 打印数组索引
GDB的print命令支持打印数组索引的功能,不过,这个功能也是默认关闭的。
用下面的命令可以打开:
set print array-indexes on
效果如下:
![992c9ec76d381e46c81e45329c98c057.png](https://img-blog.csdnimg.cn/img_convert/992c9ec76d381e46c81e45329c98c057.png)
GDB print array-index
现在一切都清晰多了吧!
到此为止,关于数据结构的打印,已经足够清晰了。
接下来,再补充一个与打印无关的小技巧。
还你一个清爽的启动界面
作为程序员,很多童鞋都是“完美主义者”,有着各种代码洁癖,对工具的使用上,也极力追求简单实用。
你没有注意过,GDB每次启动时,都要打印一大段信息呢?如下图所示:
![a88c4c1478312933080ff087ceac8a3a.png](https://img-blog.csdnimg.cn/img_convert/a88c4c1478312933080ff087ceac8a3a.png)
GDB 启动界面
主要是版本和版权信息,以及一些常规性的帮助提示。绝大多数情况下,对调试没有实际作用,反而还很影响视觉。你每次看到这些信息的时候,有没有觉得很不爽呢?
其实,GDB启动的时候,加上“-q”命令即可去掉这些无用信息。
![72ba986d861bb3bdbfaa6280d108cd2c.png](https://img-blog.csdnimg.cn/img_convert/72ba986d861bb3bdbfaa6280d108cd2c.png)
GDB quiet mode
现在是不是更加清爽了?
GDB启动脚本.gdbinit
我们前面介绍了在使用GDB调试时,通过执行命令打开GDB的pretty-print和打印数组索引的功能。虽然这些命令比较简单易用,但如果每次调试都要执行一遍,还是稍显麻烦的。
我们可以利用GDB的启动脚本.gdbinit来解决这个问题。GDB每次启动时,在开始真正地调试程序之前,都会先执行.gdbinit里面的配置信息。
注:灵活运用.gdbinit脚本,可以实现很多高级调试功能,如自动化调试等,可以大大提高调试效率。我近期会更新文章,专门详细地讲解.gdbinit的使用,感兴趣的童鞋可以右上角关注一下!
现在,在home目录中创建一个.gdbinit文件:
vi ~/.gdbinit
然后,把我们前面介绍的命令输入进去:
![64692d8fc6f5539d5fa892aa4b5b9c83.png](https://img-blog.csdnimg.cn/img_convert/64692d8fc6f5539d5fa892aa4b5b9c83.png)
.gdbinit
保存即可。
然后,为了每次启动GDB的时候去掉那些版本信息,我们可以给GDB设置一个命令别名:
vi ~/.bashrc
在文件的最后面,添加如下信息:
alias gdb="gdb -q"
然后保存退出,再执行:
source ~/.bashrc
现在就全部完成了。
我们看下最终的效果吧:
![049d16b4900eaf34d7cf20755ef136ec.png](https://img-blog.csdnimg.cn/img_convert/049d16b4900eaf34d7cf20755ef136ec.png)
最终效果
结语
作为程序员,程序调试是一项必须要熟练掌握的技能,不仅Bug定位和解决问题时会用到,在理解一些程序逻辑和实现细节时,灵活运用一些调试技巧,也能够达到事半功倍的效果。
![d5ba903b60fb111c76b385678c40c6b4.png](https://img-blog.csdnimg.cn/img_convert/d5ba903b60fb111c76b385678c40c6b4.png)
应一些童鞋的要求,我近期会持续更新一系列专题文章,深入讲解程序调试的方法和技巧,以及Linux下一些调试工具的高阶特性和实现原理。主要内容包括:
调试器的工作原理调试器常见功能的实现方法,如断点、单步执行、调用栈、源码级调试等Linux下常用工具的高级技巧,如GCC、GDB、binutil工具集、sysstat工具集等在无合适工具可用的特殊环境下,如何自己制作调试工具已更新内容:
C语言:GDB调试时遇到宏定义怎么办?一个小技巧帮你一秒钟搞定
C语言:当GDB遇到复杂数据结构,两分钟带你掌握四个高效调试技巧(本篇)
![fe087dc87778b0b6d1dc525ae48874da.png](https://img-blog.csdnimg.cn/img_convert/fe087dc87778b0b6d1dc525ae48874da.png)
对编译、链接、内核等技术感兴趣的童鞋,欢迎围观我正在更新中的另外一个系列专题:
你真的理解"Hello world"吗? 从编译链接到OS内核系列专题(已更新三篇)
看完之后,如果觉得有点收获,不是完全浪费时间的话,别忘了点赞!把知识分享给更多志同道合的人!谢谢!也欢迎留言讨论相关技术问题!
对编译器、OS内核、虚拟化、性能优化等技术感兴趣的童鞋,欢迎右上角关注!