10.4 其他调试工具
除了像gdb这样彻底的调试器外,Linux系统一般还会提供许多能够帮助你完成调试工作的其他工具。其中有的是提供关于程序的静态信息,另外一些则是提供动态分析。
静态分析只能通过程序的源代码提供信息。ctags、cxref和cflow等就是一些静态分析程序,它们可以通过源文件提供有关函数调用和函数所在位置的有用信息。
动态分析提供的是与程序执行过程中的行为有关的信息。prof和gprof等就是一些动态分析程序,它们提供的信息包括已经执行了哪些函数以及这些函数的执行时间。
下面我们将介绍其中一些工具及其输出。虽然这些工具中的大部分都有可以免费获得的版本,但并非在所有的系统中都可以使用所有这些工具。
10.4.1 lint:清理程序中的“垃圾”
早期的UNIX系统提供了工具lint,从本质上看,它只是C语言编译器的一个前端,但增加了一些常识性的测试并可以产生一些警告信息。它可以检测出未经赋值的变量使用、函数的参数未使用等情况。
最新的C语言编译器也可以产生类似的警告信息,但这是以损失编译过程的性能为代价的。lint本身已经落后于C语言的标准化工作了,因为这个工具是基于早期的C语言编译器开发的,它已不能很好地处理ANSI C的语法了。lint有一些适用于UNIX系统的商业版本,在因特网上至少有一个版本是针对Linux系统的,它的名字是splint,过去常把它称为LClint,它是MIT(麻省理工学院)的为正式规范开发工具软件这一项目的组成部分。类lint工具splint可以提供有用的代码审查注释,该软件可以在http://www.splint.org上找到。
下面这个程序是我们前面调试过的示例程序的早期版本(debug0.c):
这个版本在第20行有一个问题:它使用的是操作符&而不是&&。针对这个版本的splint示例输出经过编辑后显示在下面。注意它是如何发现第20行的问题的——程序没有初始化变量s,而且这个不正确的操作符可能会给条件测试带来问题。
splint工具抱怨程序中有老式的(非ANSI标准)函数声明,并且函数返回类型与它们真正的返回值(或没有返回值)不一致。这些虽不影响程序的执行,但应该引起注意。
它还发现了两个真正的漏洞,它们出现在下面这段代码中:
splint发现(前面输出中的阴影部分)第20行使用的变量s未经初始化,并且在该行应该使用更常见的操作符&&而不是现在使用的操作符&。在本例中,&操作符改变了测试的含义,确实是这个程序存在的一个问题。
这些错误都在调试开始之前的代码审查阶段就得到了修复。虽然它们是我们为了演示而有意放在那里的,但在真正的程序中,这些错误可以说是屡见不鲜。
10.4.2 函数调用工具
ctags、cxref和cflow这3个工具构成了X/Open规范的一部分内容,因此,具备软件开发能力的UNIX系统都会有这3个工具。
这些工具以及本章介绍的其他一些工具可能没有被包括在你的Linux发行版中。如果是这样,你就需要在因特网上搜索它们。比较好的搜索网站(对支持RPM软件包格式的Linux发行版来说)是http://rpmfind.net和http://rpm.pbone.net。你还可以尝试一些发行版特定的软件库,如针对openSUSE的http://ftp.gwdg.de/pub/opensuse/、针对Fedora的http://rpm.livna.org和针对Slackware的http://packages.slackware.it/。
1.ctags
ctags为程序中的所有函数创建索引。每个函数对应一个列表,在列表中列出该函数在程序中的调用位置,就像书籍的索引。下面是它的用法:
默认情况下,ctags在当前目录下创建文件tags。在该文件中包含每个输入源文件中声明的每个函数,文件中每行的格式如下所示:
文件中的每行由函数名、声明该函数的文件和一个可以用来在文件中查找该函数定义的正则表达式组成。Emacs等编辑器可以用这类文件来帮助程序员浏览源代码。
此外,还可以使用ctags的-x选项(如果你使用的版本有该选项)在标准输出上列出类似上面格式的内容:
你可以用-f选项将输出重定向到另一个不同的文件中,也可以用-a选项将输出结果附加到一个已有文件的结尾。
2.cxref
cxref程序分析C语言源代码并生成一个交叉引用表。它可以显示每个符号(变量、#define定义和函数)都在程序的哪个位置使用过。它生成的是一个经过排序的列表,每个符号的定义位置用一个星号(*)做标记,如下所示:
在我的机器上,上面的输出结果是在一个应用程序的源代码目录中产生,使用的命令如下所示:
$ cxref .c .h
但这个命令的正确语法格式随版本的不同而不同。请参考系统文档或手册页来了解cxref命令的更多信息。
3.cflow
cflow程序打印出一个函数调用树(function call tree),它显示了函数之间调用的关系。它可以让我们看清楚一个程序的框架结构,理解它的操作流程,了解对某个函数的改动将会产生怎样的影响。有些版本的cflow除了可以处理源代码外,还可以处理目标文件。详细的用法请参考它的手册页。
下面是cflow版本2.0的一些样本输出,该版本可以从因特网上得到,它由Marty Leisner负责维护:
这个输出样本告诉我们:main函数调用show_all_lists函数(以及其他一些函数),show_all_lists又调用了display_list,而display_list本身调用了printf。
这个版本的cflow有一个-i选项,它将产生一个反向的函数调用树。针对每个函数,cflow列出调用它的其他函数。听起来好像很复杂,但实际上很简单,下面是一个样本。
我们可以看出都有哪些函数调用了exit函数,它们是main、show_all_lists和usage。
10.4.3 用prof/gprof产生执行存档
想查找程序的性能问题时,一种常用的技巧是使用执行存档(execution profiling)。它通常需要特殊的编译器选项和辅助程序的支持。程序的执行存档可以显示执行它所花费的时间具体都用在什么操作上了。
编译程序时,给编译器加上-p标志(针对prof程序)或-pg标志(针对gprof程序)就可以创建出profile程序。而prof程序(及其GNU等效程序gprof)可以根据profile程序运行时所产生的执行跟踪文件打印出一个报告。编译命令如下所示:
$cc -pg -o program program.c
程序用特殊版本的C语言函数库链接起来并且将包括监控代码。不同的系统具体实现方法有所不同,但一般都要靠程序的频繁中断来记录执行地点。监控数据将被写入当前目录下的文件mon.out(gprof程序用的是gmon.out)。如下所示:
prof/gprof程序读取监控数据并生成一个报告。程序选项的细节请参考它的手册页。下面是一些(有所删节)gprof程序的输出示例: