本系列专题,旨在介绍一些非常实用,却不为大多数人所知的调试技巧。灵活运用这些调试技巧,能够轻松解决一些我们经常遇到并为之困惑的问题,大大提高程序调试的效率。
感兴趣的朋友,欢迎关注!
引言 - 程序调试的痛关于程序调试,有人喜欢各种调试工具,有人喜欢用简单直接的log打印。两种方法各有各的优势和不足,大多时候是可以互补的。
在Linux环境下,GDB是各种调试工具中的佼佼者,而printf则是各种日志打印方法中的典型代表。
wrlwbnvr3lx64095708117.jpg
调试问题时候,你遇到过下面的情况吗?
代码添加打印信息进行调试,突然发现添加打印的位置不对,或者别的地方也需要添加打印信息。于是,重新修改源码,重新添加打印,重新编译,重新部署,重新运行,重新调试,重新分析。当我们费了九牛二虎之力把这些都弄好之后,很不幸地又发现了新的问题,然后不得不反复进行这些过程。而且,当问题定位出来之后,我们之前花费很大力气添加的调试信息,还必须从程序中删除掉。
paz2ubpoh4364095708217.jpg
对于简单的程序,这些尚可接受。但是,在大型项目中,单是编译构建过程可能就要几十分钟,甚至数个小时,而部署过程则更为复杂。
你能想象得出,在这样的项目中一直重复这些过程,是一件多么痛苦的事情吗?
那么,有没有一种方法,既不需要修改源码,又能随时在程序中任何地方任意添加打印信息呢?
当然有!GDB的动态打印功能正式为此而生的!
kfmewpywk2264095708317.jpg
GDB Dynamic PrintfGDB提供了Dynamic Printf功能,下文我们称之为动态打印。利用这个功能,我们可以在不修改程序源码的情况下,随时在程序的任何地方添加格式化打印。
如此一来,当然也就不需要重新编译和部署的过程了。
我们先看一个简单的示例,然后再详细介绍它的实现原理,和相关命令的用法。
示例一个简单示例,如下图所示:
kslmit40dcd64095708417.jpg
先编译一下:
gcc -g test.c -o test然后用GDB进行调试:
zfqfssnkrta64095708517.jpg
和期望的一样,程序没有任何打印输出。
现在,我们在第6行、第11行、第14行分别添加一个动态打印断点,用下面的命令:
dprintf 6,"Hello, World!
"
dprintf 11,"i = %d, a = %d, b = %d
",i,a,b
dprintf 14,"Leaving! Bye bye!
"如下图:
gcxaybhxznz64095708617.jpg
稍微解释一下:
第6行的语句会打印一句“Hello, World!”
第11行会把i、a、b的值分别打印出来
第14行打印“Leaving! Bye bye!”
设置好之后,查看一下断点的信息:
zupp14bddgq64095708717.jpg
已经设置成功了。然后,重新运行:
v1yixvqqtzo64095708817.jpg
看到了吧,尽管我们并没有对源码做任何修改,且没有重新编译,但程序仍然按照我们的设置,打印出了我们想要的信息!
是不是很神奇呢?GDB的动态打印功能究竟是如何工作的呢?
GDB 动态打印实现原理在上面的示例中,在设置好动态打印的信息之后,我们可以用info break命令查看所设置的信息。
可见,GDB的动态打印,本质上也是一种特殊的断点。但是,它与一般的断点又有所区别。
一般的断点被触发后,会中断程序执行,然后等待用户操作,并且用户必须输入continue命令让程序恢复执行。
而动态打印断点被触发后,程序也会暂时中断执行,但是不需要等待用户响应,而是直接执行用户预设的格式化打印语句,并自动恢复程序的执行。
GDB动态打印的使用方法设置动态打印的命令是dprintf,格式如下:
dprintf location,format string,arg1,arg2,...dprintf命令和C语言中的printf的用法很相似,支持格式化打印。
相比printf函数,dprintf命令多了一个location参数,用于指定动态打印被触发的位置。
和break命令设置断点时一样,location可以是文件名:行号、函数名、或者具体的地址等。
除了location外,剩余的几个参数,就和printf()函数一致了。format指定字符串打印的格式,后面几个参数指定打印的数据来源。
以上面示例中的命令为例:
dprintf 6,"Hello, World!
"
dprintf 11,"i = %d, a = %d, b = %d
",i,a,b
dprintf 14,"Leaving! Bye bye!
"在功能上等价于下图中右侧的代码:
jie33jc23lr64095708917.jpg
到这里,GDB动态打印的最基本功能就介绍完了。
断点信息丢失怎么办?在实际项目的调试过程中,难免会由于各种原因而必须要反复的调试才能定位出问题的原因,或者彻底理解程序的代码逻辑。
然而,dprintf本质上也是一种断点,因此,当调试结束后,本次调试时设置的断点信息就全部丢失了。如果要再次调试的话,就不得不重新设置一遍。
如果每次调试过程中,只需要设置一两个动态打印的话,那倒也简单。
可是,如果需要设置十几个甚至几十个动态打印呢?难道每次调试都要全部重新设置一遍吗?想想都是一件比较麻烦的事情,对吧?
其实,GDB也有对应的处理方案,很简单就可以解决!
保存和加载GDB断点信息为了解决上面提到的问题,GDB很贴心地提供了对断点信息保存和加载的功能。
GDB中,可以把当前所设置的各种类型的断点信息全部保存在一个脚本文件中。这其中当然也包括dprintf设置的动态打印信息。
只需要执行下面的命令即可:
save breakpoints file_name这条命令会把当前所有的断点信息都保存在file_name指定的文件中。
等下次进行调试时,可以把file_name文件中的断点信息重新加载起来。有两种方法:
启动GDB时使用“-x file_name”参数。
在GDB中执行source file_name命令。
[/ol]下面分别演示一下。
保存断点信息
我们用GDB重新启动上面示例中的test程序:
用dprintf设置好断点。
用info break命令查看一下断点信息。
执行“save breakpoints test.bp”命令,把断点信息保存在test.bp文件中。
[/ol]
gt3rg4bkjdk64095709017.jpg
我们看下一下test.bp中究竟保存了什么内容:
tmtpasaclfx64095709117.jpg
原来,就是我们之前执行的三条dprintf命令,并且是以文本形式存在test.bp中的。
接下来,我们用两种方法分别加载test.bp中的断点信息。
用-x参数加载断点信息
uw42bdi1znv64095709217.jpg
可见,指定-x参数后,GDB在开始调试程序之前,会从指定的文件中把断点信息加载进来,并重新设置在程序中。因此,执行run命令后,程序能够按照我们的预期正常执行动态打印功能。
source命令加载断点信息
3nrmykuuloc64095709317.jpg
GDB把test加载起来之后,info break并没有显示出任何断点信息。然后,我们执行source test.bp命令,GDB会把断点信息从test.bp加载进来,并重新设置在test程序中。
结语由于篇幅所限,本文只是介绍了GDB动态打印的基本功能和使用方法。其实,它还有很多高阶的用法和技巧,以后会再更新文章进行讲解。
程序调试是每个程序员必须要熟练掌握的基本技能,在整个计算机知识体系结构中,它占据着非常重要的地位。
猜你喜欢:
WiFi6+蓝牙+星闪,三合一开发板,真香!
Github上热门 C 语言项目汇总!
嵌入式,可测试性软件设计!
一些低功耗软件设计的要点!
嵌入式 C 保护结构体的方式
实用 | 10分钟教你通过网页点灯
谈谈嵌入式软件的兼容性!
点击阅读原文,查看更多分享。 |