http://blog.csdn.net/mylxiaoyi/archive/2009/05/11/4169326.aspx
调试(一) 收藏 第十章 调试 所有的软件都会存在缺陷,通常每100 行代码就会存在2到5个缺陷。这些错误通常会使得程序和库并不会预期的表现,通常会使得一个程序的行为并不会如预想的那样。Bug跟踪,标识以及修复会占用程序软件开发过程中的大量时间。 在这一章,我们讨论软件缺陷,并且会考虑一些工具与技术用于跟踪特定的错误行为。这不同于测试(在各种条件下验证程序行为的任务),尽管测试与调试是相关联的,而且许多bug就是在测试过程中发现的。 我们会讨论下列主题: 错误类型 通常的调试技术 使用GDB与其他工具进行调试 断言 内存使用调试 错误类型 bug 通常是由下列一些原因引起的,而其中的每一个都指出一个检测与修复的方法: 规范错误:如果一个程序没有进行正确的规范,毫无疑问,这个程序并不会表现出预期的行为。即使是世界上时优秀的程序员有时也会编写出错误的程序。在我们开始编程(或是设计)之前,要保证我们清楚的知道与了解我们的程序需要做什么。我们可以通过查看需求和与使用程序的用户所达成的协议来检测与修复许多(如果不是所有)的规范错误。 设计错误:任何规模的程序都需要创建之前进行设计。通常坐在电脑前,直接输入源码,并且希望程序第一次就正确工作,这样是不够的。我们需要花些时间来考虑如何组织我们的程序,我们需要使用哪些数据结构,以及如何使用他们。试着进行详细的设计,因为这样以后就可以省去许多重新编写的痛苦。 编码错误:当然,每个人都会出输入错误。由我们的设计创建源代码的过程是一个不完美的过程。这也是许多bug滋生的地方。当我们在程序中遇到一个bug时,不要忽视简单重读源代码或是请其他人来阅读源代码从而修复bug的可能性。令人惊奇的一件事是就通守与其他人讨论实现我们可以检测并修复许多bug。 试着在纸上执行程序核心,这个过程被称之为干运行(dry running)。对于许多重要的例程,一步步写下输入的值并且计算输出。我们并不必须使用计算机进行调试,而且有时就是计算机引起的问题。即使是那些编写库,编译器,以及操作系统的人也会出错。另一方面,不要急于责备工具;很有可能是在一个新的程序中存在bug,而不是存在于编译器中。 通常的调试技术 有许多不的方法可以用来调试与测试一个通常的Linux程序。我们通常运行程序并且查看发生了什么。如果程序不能工作,我们需要决定对其做些什么。我们可以修改程序并且再次运行,我们可以尝试获得程序内部运行的更多信息,或是我们可以直接监视程序的运行。调试的五个步骤为: 测试:发现存在哪些缺陷或是bug 稳定化:使得bug重新出现 本地化:标识相关的代码行 修正:修正代码 验证:保证修正正常工作 一个带有bug的程序 下面我们来看一下带有 bug的程序。在本章的讨论中,我们将会尝试对其进行调试。这个程序是在一个大型软件系统的开发过程中编写的。其目的就是测试一个函数,sort,其作用是在一个item类型的结构数组上实现一个冒泡排序算法。这些项目以其成员key升序的顺序进行排列。这个程序在一个例子数组上调用sort进行测试。在实际的工作中我们绝不会使用这种排序算法,因为其效率实在是太低了。我们在这里使用他是因为他很短小,理解相对简单,而且很容易出错。事实上,标准C库具有一个名为qsort的函数可以实现所要求的任务。 不幸的是,代码很难阅读,没有注释,而且原始程序也不可得了。我们不得不自己与其挣扎,我们由基本的例程debug1.c开始。 /* 1 */ typedef struct { /* 2 */ char *data; /* 3 */ int key; /* 4 */ } item; /* 5 */ /* 6 */ item
/* 8 */ {“neil”, 4}, /* 9 */ {“john”, 2}, /* 10 */ {“rick”, 5}, /* 11 */ {“alex”, 1}, /* 12 */ }; /* 13 */ /* 14 */ sort(a,n) /* 15 */ item *a; /* 16 */ { /* 17 */ int i = 0, j = 0; /* 18 */ int s = 1; /* 19 */ /* 20 */ for(; i < n && s != 0; i++) { /* 21 */ s = 0; /* 22 */ for(j = 0; j < n; j++) { /* 23 */ if(a[j].key > a[j+1].key) { /* 24 */ item t = a[j]; /* 25 */ a[j] = a[j+1]; /* 26 */ a[j+1] = t; /* 27 */ s++; /* 28 */ } /* 29 */ } /* 30 */ n--; /* 31 */ } /* 32 */ } /* 33 */ /* 34 */ main() /* 35 */ { /* 36 */ sort(array,5); /* 37 */ } 我们试着编译这个程序: $ cc -o debug1 debug1.c 编译成功,没有错误或是警告报告。 在我们运行这个程序之前,我们需要添加一些代码来输出结果。否则,我们就不知道程序是否进行了工作。我们会添加一些额外的代码在排序结束之后显示数组。我们称这个新版本为debug2.c。 /* 34 */ main() /* 35 */ { /* 36 */ int i; /* 37 */ sort(array,5); /* 38 */ for(i = 0; i < 5; i++) /* 39 */ printf(“array[%d] = {%s, %d}/n”, /* 40 */ i, array[i].data, array[i].key); /* 41 */ } /* 34 */ main() /* 35 */ { /* 36 */ int i; /* 37 */ sort(array,5); /* 38 */ for(i = 0; i < 5; i++) /* 39 */ printf(“array[%d] = {%s, %d}/n”, /* 40 */ i, array[i].data, array[i].key); /* 41 */ } 严格来说这些额外的代码并不算是程序修正的一部分。我们添加这些代码仅是为测试。我们必须非常小心不要在我们的测试代码中引入额外的bug。现在再次编译并且运行程序。 $ cc -o debug2 debug2.c $ ./debug2 当我们这样做时发生了什么依赖于我们的Linux平台以及我们所进行的设置。在作者的系统上,我们会得到下面的输出信息: array[0] = {john, 2} array[1] = {alex, 1} array[2] = {(null), -1} array[3] = {bill, 3} array[4] = {neil, 4} 但是在另一个作者的系统(运行一个不同的内核),我们会得到下面的信息: Segmentation fault 在我们的Linux系统上,我们会看到其中的一个信息或是另一上不同的结果。我们希望得看到下面的信息: array[0] = {alex, 1} array[1] = {john, 2} array[2] = {bill, 3} array[3] = {neil, 4} array[4] = {rick, 5} 很明显,在代码中存在一个严重的问题。如果这个程序可以运行,那么他就不能对数组进行正确的排序,而如果程序结束并提示内存错误,那么是系统向程序发送了一个信号表明系统已经检测到一个非法的内存访问并且提前结束了程序的运行以防止内存被破坏。 操作系统检测非法内存访问的能力依赖于其硬件的配置以及内存管理系统的精巧实现。在大多数系统上,由操作系统分配给程序的内存远大于实际正在使用的内存。如果非法内存访问发生了这块内存区域,硬件也许就不能检测非法访问。这就是为什么并不是所有的 Linux版本以及Unix产生内存错误的原因。 注:一些库函数,例如printf,也会阻止某些条件下的非法访问,例如使用一个空指针。 当我们跟踪数组访问问题时,通常增加数组元素的数量是一个好主意,因为这会增加错误数。如果我们读取超过数组字节结束处一个字节,我们也许就会消耗掉这些内存,因为分配给程序的内存将会达到操作系统特定的边界,通常为8K。 如果我们增加数组元素的数量,在这个例子中可以通过修改item成员data为一个4096字符的数组来做到,对于不存在的数组元素的访问也许就会是超出已分配的内存地址。每一个数组元素为4K大小,所以我们非正常使用的内存可以为0到4K。 如果我们这样修改,并将其结果称之为debug3.c,我们就会在两个作者的Linux版本上得到内存错误的信息。 /* 2 */ char data[4096]; $ cc -o debug3 debug3.c $ ./debug3 Segmentation fault (core dumped) 也有可能某些 Linux或是Unix版本仍然不会产生内存错误信息。当ANSI C标准检测到未定义行为时,他会允许程序执行任何动作。当然看上去似乎是我们编写了一个非正常的C程序,而一个非正常的C程序可以执行任何奇怪的行为。正如我们将会看到的,错误类型就落入了未定义行为的类别。 调试(二) 收藏 代码监视 正如我们在前面所提到的,当程序并未按我们预期的那样运行时,重读我们的程序是一个好主意。出于本章的目的,我们假设代码已经进行重新检查,并且明显的错误已经进行了处理。 我们可以使用一些工具来帮助我们进行代码检查,编译器就是明显的一个。如果在我们的程序中存在任何语法错误,编译器可以通知我们。 我们在后面还会提到其他的工具,lint与Splint。与编译器类似,他们会分析代码并且报告不正确的代码。 监视 监视就是为了收集更多程序运行的行为信息而在程序添加的代码。正如在我们的例子中所做的,我们通常会添加printf调用来输出程序运行过程中不同阶段的变量值。我们通常可以添加多个printf调用,但是我们必须清楚的是程序必须经过修改并且在程序修改后要进行编译,当然,当bug被修复后我们需要移除这些代码。 在这里我们有两个监视工具可用。第一种方法使用C预处理器来选择性的包含监视代码,从而我们只需要重新编译程序来包含或是排除调试代码。我们可以使用如下的结构来简单做到: #ifdef DEBUG printf(“variable x has value = %d/n”, x); #endif 我们可以使用编译器选项-DDEBUG编译程序来定义DEBUG符号并且包含这些额外的代码或是不带这个编译选项来排除这些代码。我们可以使用更为复杂的数字调试宏,如下所示: #define BASIC_DEBUG 1 #define EXTRA_DEBUG 2 #define SUPER_DEBUG 4 #if (DEBUG & EXTRA_DEBUG) printf... #endif 在这种情况下,我们必须总是定义DEBUG宏,但是我们可以设置他来代表一个调试信息集合,或者一个详细级别。在这个例子中,编译器选项-DDEBUG=5将会允许 BASIC_DEBUG与SUPER_DEBUG,但不是EXTRA_DEBUG。标记-DDEBUG=0将会禁止所有的调试信息。相对应的,包含下面的代码就排除了在不需要调试的情况下在命令行指定DEBUG的需要: #ifndef DEBUG #define DEBUG 0 #endif C 预处理器定义的一些宏有助于调试信息。这些宏会进行扩展给出有关当前编译的一些信息。 宏 描述 __LINE__ 表示当前行号的十进制常数 __FILE__ 表示当前文件名的字符串 __DATE__ 以"Mmm dd yyyy"格式表示的当前日期 __TIME__ 以"hh:mm:ss"格式表示的当前时间 注意,这些符号都是以两个下划线为前缀和后缀的。这是标准预处理器的通常做法,而我们应该小心避免选择会造成冲突的符号。在上面描述中的术语"当前"是指预处理器执行的时间,也就是编译器运行与文件处理的时间与日期。 试验--调试信息 下面是程序cinfo.c,这个程序会允许调试的情况下输出其编译信息。 #include
int main() { #ifdef DEBUG printf(“Compiled: “ __DATE__ “ at “ __TIME__ “/n”); printf(“This is line %d of file %s/n”, __LINE__, __FILE__); #endif printf(“hello world/n”); exit(0); } 当我们在打开调试(使用 -DDEBUG)的情况下编译这个程序,我们可以看到编译信息。 $ cc -o cinfo -DDEBUG cinfo.c $ ./cinfo Compiled: Mar 1 2003 at 18:17:32 This is line 7 of file cinfo.c hello world $ 工作原理 当编译器编译时,其C预处理器部分会记录当前行号与文件。当遇到__LINE__与__FILE__时会将其替换为当前的变量值。日期与时间的用法与其相类似。因为 __DATE__与__TIME__是字符串,我们可以使用printf格式化字符将他们合并,因为ANSI C将合并的字符串看作一个字符串。 不重新编译而调试 在我们继续之前,很值得指出一点:有一个方法可以使用printf函数帮助调试而不使用#ifdef DEBUG技术,而后者需要一个程序在可以使用之前必须进行重新编译。 这个方法是添加一个全局变量作为调试标记,允许在命令行使用-d选项,从而使用用户即使在程序发布之后也可以选择开头调试,并添加一个调试记录函数。现在我们就可以在我们的程序代码中添加如下的代码: if (debug) { sprintf(msg, ...) write_debug(msg) } 如果程序并不是实际使用我们可以将调试信息输出到stderr,或是使用syslog函数所提供的日志功能。 如果我们添加此类的跟踪代码为解决开发过程中的问题,只需要将他们留下那里就可以了。假如我们多加小心,这是相当安全的。当程序发布时我们就可以感受到这样做的好处;如果用户遇到问题,他们可以使用调试模式来运行,并且为我们诊断错误。与程序仅是输入内存错误信息不同,这样做可以报告程序此时实际做什么,而不仅是用户正是做什么。其中的区别是很明显的。 这个方法有一个明显的缺点;程序要比需要的大得多。在大多数情况下,这是比实际更为明显的一个问题。程序的规模将会大出20%到30%,但是在多数情况下这并不会对性能有什么实际的影响。差的情能来自由功能规模的巨大变化。 执行控制 让我们回到我们的例子程序。我们的程序有一个bug。我们可以修改程序添加一些额外的代码来输出程序运行时的变量值,或是我们可以使用一个调试器来控制程序的执行并且在处理执行时查看其状态。 在商业Unix系统,依据其提供者有大量的调试器可用。通常的调试有adb,sdb,与dbx。更为复杂的调试器允许我们在源代码级别详细的查看程序的状态。对于sdb,dbx是如此,而对于GNU调试器也是如此,后者可以用在Linux系统上。还存在 gdb的前端,从而会使得gdb更为友好;xxgdb,tgdb,以及ddd就是这样的程序。一些IDE,例如我们在第9章所看到的,也提供了调试程序或是gdb的前端。Emacs编辑也具有一个实用程序允许我们在程序上运行gdb,设置断点,以及查看当前执行的源代码等。 要准备一个程序用于调试,我们需要使用一个或是多个特殊的编译选项来编译程序。这些选项会指示编译在程序中包含额外的调试信息。这些信息包括符号与行号信息,调试器可以使用这些信息向用户显示程序执行到了何处。 -g标记是编译一个程序用于调试时最常用到的选项。我们必须这个选项来编译每一个需要进行调试的源文件,同时也要用于链接器,从而可以使用标准C库的特殊版本来在库函数中提供调试支持。编译器程序会自动向链接器传递这些信息。调试也可以用于并不是为此目的而编译的库,但是具有更少的灵活性。 调试信息会使用可执行程序时间加倍变长。尽管可执行程序变大(而且需要更多的磁盘空间),程序运行所需要的内存数量是一样的。通常在我们发布程序之前移除这些调试信息是一个好主意,但是只有在我们调试之后才可以这样。 注:我们可以通过运行strip 来由一个可执行文件中移除调试信息,而不需要重新编译。
了解更多有关gdb的内容
GNU调试器是一 个强大的工具,可以提供大量的有关运行程序内部状态的信息。在支持一个名叫硬件断点(hardware breakpoint)的实用程序的系统上,我们可以使用gdb来实时的查看变量的改变。硬件断点是某些CPU的一个特性;如果出现特定的条件,通常是在 指定区域的内存访问,这些处理器能够自动停止。相对应的,gdb可以使用watch表达式。这就意味着,出于性能考虑,当一个表达式具有一个特定的值 时,gdb可以停止这个程序,而不论计算发生在程序中的哪个位置。
断点可以使用计数以及条件进行设置,从而他们只在一定的次数之后或是当 满足一个条件时才会被引发。
gdb也能够将其本身附在已经运行的程序中。这对于我们调试客户端/服务器系统时是非常有用的,因为我们可以 调试一个正在运行的行为不当的服务器进程,而不需要停止与重启服务器。例如,我们可以使用gcc -O -g选项来编译我们的程序,从而得到优化与调试的好处。不足之处就是优化也许会重新组织代码,所以当我们分步执行时,我们也许会发出我们自身的跳转来达到 与原始源代码相同的效果。
我们也可以使用gdb来调试已经崩溃的程序。Linux与Unix经常会在一个程序失败时在一个名为core的 文件中生成一个核心转储信息。这是一个程序内存的图象并且会包含失败时全局变量的值。我们可以使用gdb来查看当程序崩溃时程序运行到哪里。查看gdb手 册页我们可以得到更为详细的信息。
gdb以GPL许可证发布并且绝大多数的Unix系统都会支持他。我们强烈建议了解gdb。
更 多的调试工具
除了强大的调试工具,例如gdb,Linux系统通常还会提供一些其他们的我们可以用于诊治调试进程的工 具。其中的一些会提供关于一个程序的静态信息;其他的会提供动态分析。
静态分析只由程序源代码提供信息。例如ctags,cxref,与 cflow这样的程序与源代码一同工作,并且会提供有关函数调用与位置的有用信息。
动态会析会提供有关一个程序在执行过程中如何动作的信 息。例如prof与gprof这样的程序会提供有关执行了哪个函数并且执行多长时间的信息。
下面我们来看一下其中的一些工具及其输出。并 不是所有的这些工具都可以在所有的系统上得到,尽管其中的一些都有自由版本。
Lint:由我们的程序中移除Fluff
原 始的Unix系统提供了一个名为lint的实用程序。他实质是一个C编译的前端,带有一个测试设计来适应一些常识并且生成警告。他会检测变量在设计之前在 哪里使用以及哪里函数参数没有被使用,以及其他的一些情况。
更为现代的C编译器可以编译性能为代价提供类似的警告。lint本身已经被C 标准所超越。因为这个工具是基于早期的C编译器,他并不能处理所有的ANSI语法。有一些商业版本的lint可以用于Unix,而且在网络上至少有一个名 为splint可以用于Linux。这就是过去所知的LClint,他是MIT一个工程的一部分,来生成用于通常规范的工具。一个类似于lint的工具 splint可以提供查看注释的有用代码。splint可以在htt://www.splin.org处得到。
下面是一个编辑过的 splint例子输出,这是运行我们在前面调试的例子程序的早期版本中所产生的输出:
neil@beast:~/BLP3/chapter10> splint -strict debug0.c
Splint 3.0.1.6 --- 27 Mar 2002
debug0.c:14:22: Old style function declaration
Function definition is in old style syntax. Standard prototype syntax is
preferred. (Use -oldstyle to inhibit warning)
debug0.c: (in function sort)
debug0.c:20:31: Variable s used before definition
An rvalue is used that may not be initialized to a value on some execution
path. (Use -usedef to inhibit warning)
debug0.c:20:23: Left operand of & is not unsigned value (boolean):
i < n & s != 0
An operand to a bitwise operator is not an unsigned values. This may have
unexpected results depending on the signed representations. (Use
-bitwisesigned to inhibit warning)
debug0.c:20:23: Test expression for for not boolean, type unsigned int:
i < n & s != 0
Test expression type is not boolean or int. (Use -predboolint to inhibit
warning)
debug0.c:20:23: Operands of & are non-integer (boolean) (in post loop test):
i < n & s != 0
A primitive operation does not type check strictly. (Use -strictops to
inhibit warning)
debug0.c:32:14: Path with no return in function declared to return int
There is a path through a function declared to return a value on which there
is no return statement. This means the execution may fall through without
returning a meaningful result to the caller. (Use -noret to inhibit warning)
debug0.c:34:13: Function main declared without parameter list
A function declaration does not have a parameter list. (Use -noparams to
inhibit warning)
debug0.c: (in function main)
debug0.c:36:17: Return value (type int) ignored: sort(array, 5)
Result returned by function call is not used. If this is intended, can cast
result to (void) to eliminate message. (Use -retvalint to inhibit warning)
debug0.c:37:14: Path with no return in function declared to return int
debug0.c:14:13: Function exported but not used outside debug0: sort
debug0.c:15:17: Definition of sort
Finished checking --- 22 code warnings
$
这个程序报告旧风格的函数定义以及函数返回类型与他们实际返回 类型之间的不一致。这些并不会影响程序的操作,但是应该注意。
他还在下面的代码片段中检测到两个实在的bug:
/* 18 */ int s;
/* 19 */
/* 20 */ for(; i < n & s != 0; i++) {
/* 21 */ s = 0;
splint已经确定在20行使用了变量s,但是并没有进行初始化,而且操作符&已 经被更为通常的&&所替代。在这个例子中,操作符优先级修改了测试的意义并且是程序的一个问题。
所有这些错误都在调试开 始之前在代码查看中被修正。尽管这个例子有一个故意演示的目的,但是这些错误真实世界的程序中经常会出现的。
函数调用工 具
三个实用程序-ctags,cxref与cflow-形成了X/Open规范的部分,所以必须在具有软件开发功能的 Unix分枝系统上提供。
ctags
ctags程序创建函数索引。对于每一个函数, 我们都会得到一个他在何处使用的列表,与书的索引类似。
ctags [-a] [-f filename] sourcefile sourcefile ...
ctags -x sourcefile sourcefile ...
默认情况下,ctags在 当前目录下创建一个名为tags的目录,其中包括在输入源文件码中所声明的每一个函数,如下面的格式
announce app_ui.c /^static void announce(void) /
文件中的每一行由一个函数名,其声明所在的文件,以及一个可以用在文件 中查找到函数定义所用的正则表达式所组成。一些编辑器,例如Emacs可以使用这种类型的文件在源码中遍历。
相对应的,通过使用 ctags的-x选项,我们可以在标准输出上产生类似格式的输出:
find_cat 403 app_ui.c static cdc_entry find_cat(
我们可以通过使用-f filename选项将输出重定向到另一个不同的文件中,或是通过指定-a选项将其添加到一个已经存在的文件中。
cxref
cxref 程序分析C源代码并且生成一个交叉引用。他显示了每一个符号在程序中何处被提到。他使用标记星号的每一个符号定义位置生成一个排序列表,如下所示:
SYMBOL FILE FUNCTION LINE
BASENID prog.c — *12 *96 124 126 146 156 166
BINSIZE prog.c — *30 197 198 199 206
BUFMAX prog.c — *44 45 90
BUFSIZ /usr/include/stdio.h — *4
EOF /usr/include/stdio.h — *27
argc prog.c — 36
prog.c main *37 61 81
argv prog.c — 36
prog.c main *38 61
calldata prog.c — *5
prog.c main 64 188
calls prog.c — *19
prog.c main 54
在作者的机子上,前面的输入在程序的源码目录中使用下面的命令来生成的:
$ cxref *.c *.h
但是实际的语法因为版本的不同而不同。查看我们系统的文档或是man手册可以得到更多的信息。
cflow
cflow 程序会输出一个函数调用树,这是一个显示函数调用关系的图表。这对于查看程序结构来了解他是如何操作的以及了解对于一个函数有哪些影响是十分有用的。一些 版本的cflow可以同时作用于目标文件与源代码。查看手册页我们可以了解更为详细的操作。
下面是由一个cflow版本(cflow- 2.0)所获得的例子输出,这个版本的cflow版本是由Marty Leisner维护的,并且可以网上得到。
1 file_ungetc {prcc.c 997}
2 main {prcc.c 70}
3 getopt {}
4 show_all_lists {prcc.c 1070}
5 display_list {prcc.c 1056}
6 printf {}
7 exit {}
8 exit {}
9 usage {prcc.c 59}
10 fprintf {}
11 exit {}
从这个输出中我们可以看到main函数调用 show_all_lists,而show_all_lists调用display_list,display_list本身调用printf。
这 个版本cflow的一个选项就是-i,这会生成一个反转的流程图。对于每一个函数,cflow列出调用他的其他函数。这听起来有些复杂,但是实际上并不是 这样。下面是一个例子。
19 display_list {prcc.c 1056}
20 show_all_lists {prcc.c 1070}
21 exit {}
22 main {prcc.c 70}
23 show_all_lists {prcc.c 1070}
24 usage {prcc.c 59}
...
74 printf {}
75 display_list {prcc.c 1056}
76 maketag {prcc.c 487}
77 show_all_lists {prcc.c 1070}
78 main {prcc.c 70}
...
99 usage {prcc.c 59}
100 main {prcc.c 70}
例如,这告诉我们调用exit的函数有main,show_all_lists与usage。
使 用prof/gprof执行性能测试
当我们试着追踪一个程序的性能问题时一个十分有用的技术就是执行性能测试 (execution profiling)。通常被特殊的编译器选项以及辅助程序所支持,一个程序的性能显示他在哪里花费时间。
prof 程序(以及其GNU版本gprof)会由性能测试程序运行时所生成的执行追踪文件中输出报告。一个可执行的性能测试是由指定-p选项(对prof)或是 -pg选项(对gprof)所生成的:
$ cc -pg -o program program.c
这个程序是使用一个 特殊版本的C库进行链接的并且被修改来包含监视代码。对于不同的系统结果也许不同,但是通常是由安排频繁被中断的程序以及记录执行位置来做到的。监视数据 被写入当前目录中的一个文件,mon.out(对于gprof为gmon.out)。
$ ./program
$ ls -ls
2 -rw-r--r-- 1 neil users 1294 Feb 4 11:48 gmon.out
prof/gprof程序 读取这些监视数据并且生成一个报告。查看其手册页可以详细了解其程序选项。下面以gprof输出作为一个例子:
cumulative self self total
time seconds seconds calls ms/call ms/call name
18.5 0.10 0.10 8664 0.01 0.03 _doscan [4]
18.5 0.20 0.10 mcount (60)
14.8 0.28 0.08 43320 0.00 0.00 _number [5]
9.3 0.33 0.05 8664 0.01 0.01 _format_arg [6]
7.4 0.37 0.04 112632 0.00 0.00 _ungetc [8]
7.4 0.41 0.04 8757 0.00 0.00 _memccpy [9]
7.4 0.45 0.04 1 40.00 390.02 _main [2]
3.7 0.47 0.02 53 0.38 0.38 _read [12]
3.7 0.49 0.02 w4str [10]
1.9 0.50 0.01 26034 0.00 0.00 _strlen [16]
1.9 0.51 0.01 8664 0.00 0.00 strncmp [17]
断言
在 程序的开发过程中,通常使用条件编译的方法引入调试代码,例如printf,但是在一个发布的系统中保留这些信息是不实际的。然而,经常的情况是问题出现 与不正确的假设相关的程序操作过程中,而不是代码错误。这些都是"不会发生"的事件。例如,一个函数也许是在认为其输入参数总是在一定范围下而编写的。如 果给他传递一些不正确的数据,也许整个系统就会崩溃。
对于这些情况,系统的内部逻辑在哪里需要验证,X/Open提供了assert宏, 可以用来测试一个假设是否正确,如果不正确则会停止程序。
#include <assert.h>
void assert(int expression)
assert宏会计算表达式的值,如果不为零,则会向标准错误上输出一些诊断信息,并且调 用abort来结束程序。
头文件assert.h依据NDEBUG的定义来定义宏。如果头文件被处理时定义了NDEBUG,assert 实质上被定义为空。这就意味着我们可以通过使用-DNDEBUG在编译时关闭断言或是在包含assert.h文件之前包含下面这行:
#define NDEBUG
这种方法的使用是assert的一个问题。如果我们在测试中使用assert,但是却对生产代码而关闭,比起我们测试时的 代码,我们的生产代码就不会太安全。在生产代码中保留断言开启状态并不是通常的选择,我们希望我们的代码向用户显示一条不友好的错误assert failed与一个停止的程序吗?我们也许会认为最好是编写我们自己的检测断言的错误追踪例程,而不必在我们的生产代码中完全禁止。
我们 同时要小心在assert断言没有临界效果。例如,如果我们在一个临界效果中调用一个函数,如果移除了断言,在生产代码中就不会出现这个效果。
试 验--assert
下面的程序assert.c定义了一个必须传递正值参数的函数。通过使用一个断言可以避免不正常参 数的可能。
在包含assert.h头文件和检测参数是否为正的平方根函数之后,我们可以编写如下的函数:
#include <stdio.h>
#include <math.h>
#include <assert.h>
double my_sqrt(double x)
{
assert(x >= 0.0);
return sqrt(x);
}
int main()
{
printf(“sqrt +2 = %g/n”, my_sqrt(2.0));
printf(“sqrt -2 = %g/n”, my_sqrt(-2.0));
exit(0);
}
当我们运行这个程序时,我们就会看到当我们传递一个非法值时就会违背这个断言。事实上的断言失败的消息格式会 因系统的不同而不同。
$ cc -o assert assert.c -lm
$ ./assert
sqrt +2 = 1.41421
assert: assert.c:7: my_sqrt: Assertion `x >= 0.0’ failed.
Aborted
$
工 作原理
当我们试着使用一个负数来调用函数my_sqrt时,断言就会失败。assert宏会提供违背断言的文件和行 号,以及失败的条件。程序以一个退出陷井结束。这是assert调用abort的结果。
如果我们使用-DNDEBUG选项来编译这个程 序,断言就会被编译在外,而当我们由my_sqrt中调用sqrt函数时我们就会得到一个算术错误。
$ cc -o assert -DNDEBUG assert.c -lm
$ ./assert
sqrt +2 = 1.41421
Floating point exception
$
一些最近的算术库版本会返回一个NaN(Not a Number)值来表示一个不可用的结果。
sqrt –2 = nan
内存调试
富 含bug而且难于跟踪调试的一个区域就是动态内存分配。如果我们编译一个使用malloc与free来分配内存的程序,很重要的一点就是我们要跟踪我们所 分配的内存块,并且保证不要使用已经释放的内存块。
通常,内存是由malloc分配并且赋给一个指针变量的。如果指针变量被修改了,而又 没有其他的指针来指向这个内存块,他就会变为不可访问的内存块。这就是一个内存泄露,而且会使得我们程序尺寸变大。如果我们泄露了大量的内存,那么我们的 系统就会变慢并且会最终用尽内存。
如果我们在超出一个分配的内存块的结束部分(或是在一个内存块的开始部分)写入数据,我们很有可能会破 坏malloc库来跟踪分配所用的数据结构。在这种情况下,在将来的某个时刻,调用malloc,或者甚至是free,就会引起段错误,而我们的程序就会 崩溃。跟踪错误发生的精确点是非常困难的,因为很可能他在引起崩溃的事件发生以前很一段时间就已经发生了。
不必奇怪的是,有一些工具,商 业或是自由的,可以有助于处理这两种问题类型。例如,有许多不同的malloc与free版本,其中的一些包含额外的代码在分配与回收上进行检测尝试检测 一个内存块被释放两次或是其他一些滥用类型的情况。
ElectricFence
ElectricFence 库是由Bruce Perens开发的,并且在一些Linux发行版本中作为一个可选的组件来提供,例如RedHat,而且已经可以在网络上获得。他尝试使用Linux的虚 拟内存工具来保护malloc与free所使用的内存,从而在内存被破坏时终止程序。
试验--ElectricFence
下 面的程序,efence.c,使用malloc分配一个内存块,然后在超出块结束处写入数据。让我们看一下会发生什么情况。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *ptr = (char *) malloc(1024);
ptr[0] = 0;
/* Now write beyond the block */
ptr[1024] = 0;
exit(0);
}
当 我们编译运行这个程序时,我们并不会看到进一步的行为。然而,似乎malloc所分配的内存区域有一些问题,而我们实际上已经遇到了麻烦。
$ cc -o efence efence.c
$ ./efence
$
然而,如果我们使用ElectricFence 库,libefence.a来链接这个程序,我们就会得到一个即时的响应。
$ cc -o efence efence.c -lefence
$ ./efence
Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens <[email protected]>
Segmentation fault
$
在 调试器下运行可以定位这个问题:
$ cc -g -o efence efence.c -lefence
$ gdb efence
(gdb) run
Starting program: /home/neil/BLP3/chapter10/efence
[New Thread 1024 (LWP 1869)]
Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens <[email protected]>
Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 1024 (LWP 1869)]
0x080484ad in main () at efence.c:10
10 ptr[1024] = 0;
(gdb)
工 作原理
Electric替换malloc并且将函数与计算机处理器的虚拟内存特性相关联来阻止非法的内存访问。当这样 的访问发生时,就会抛出一个段错误信息从而可以终止程序。
valgrind
valgrind 是一个可以检测我们已经讨论过的许多问题的工具。事实上,他可以检测数据访问错误与内存泄露。也许他并没有被包含在我们的Linux发行版本中,但是我们 可以在http://developer.kde.org/~sewardj处得到。
程序并不需要使用valgrind重新编译,而我们 甚至可以调用一个正在运行的程序的内存访问。他很值得一看,他已经用在主要的开发上,包含KDE版本3。
试验 --valgrind
下面的程序,checker.c,分配一些内存,读取超过那块内存限制的位置,在其结束处之外写 入数据,然后使其不能访问。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *ptr = (char *) malloc(1024);
char ch;
/* Uninitialized read */
ch = ptr[1024];
/* Write beyond the block */
ptr[1024] = 0;
/* Orphan the block */
ptr = 0;
exit(0);
}
要使用 valgrind,我们只需要简单的运行valgrind命令,传递我们希望检测的选项,其后是使用其参数运行的程序。
当我们使用 valgrind来运行我们的程序时,我们可以看到诊断出许多问题:
$ valgrind --leak-check=yes -v ./checker
==3436== valgrind-1.0.4, a memory error detector for x86 GNU/Linux.
==3436== Copyright (C) 2000-2002, and GNU GPL’d, by Julian Seward.
==3436== Estimated CPU clock rate is 452 MHz
==3436== For more details, rerun with: -v
==3436==
==3436== Invalid read of size 1
==3436== at 0x8048397: main (checker.c:10)
==3436== by 0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436== by 0x80482D1: exit@@GLIBC_2.0 (in /home/neil/BLP3/chapter10/checker)
==3436== Address 0x42AD1424 is 0 bytes after a block of size 1024 alloc’d
==3436== at 0x4003CA75: malloc (vg_clientfuncs.c:100)
==3436== by 0x8048389: main (checker.c:6)
==3436== by 0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436== by 0x80482D1: exit@@GLIBC_2.0 (in /home/neil/BLP3/chapter10/checker)
==3436==
==3436== Invalid write of size 1
==3436== at 0x80483A4: main (checker.c:13)
==3436== by 0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436== by 0x80482D1: exit@@GLIBC_2.0 (in /home/neil/BLP3/chapter10/checker)
==3436== Address 0x42AD1424 is 0 bytes after a block of size 1024 alloc’d
==3436== at 0x4003CA75: malloc (vg_clientfuncs.c:100)
==3436== by 0x8048389: main (checker.c:6)
==3436== by 0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436== by 0x80482D1: exit@@GLIBC_2.0 (in /home/neil/BLP3/chapter10/checker)
==3436==
==3436== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
==3436== malloc/free: in use at exit: 1024 bytes in 1 blocks.
==3436== malloc/free: 1 allocs, 0 frees, 1024 bytes allocated.
==3436== For counts of detected errors, rerun with: -v
==3436== searching for pointers to 1 not-freed blocks.
==3436== checked 3468724 bytes.
==3436==
==3436== definitely lost: 1024 bytes in 1 blocks.
==3436== possibly lost: 0 bytes in 0 blocks.
==3436== still reachable: 0 bytes in 0 blocks.
==3436==
==3436== 1024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==3436== at 0x4003CA75: malloc (vg_clientfuncs.c:100)
==3436== by 0x8048389: main (checker.c:6)
==3436== by 0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436== by 0x80482D1: exit@@GLIBC_2.0 (in /home/neil/BLP3/chapter10/checker)
==3436==
==3436== LEAK SUMMARY:
==3436== definitely lost: 1024 bytes in 1 blocks.
==3436== possibly lost: 0 bytes in 0 blocks.
==3436== still reachable: 0 bytes in 0 blocks.
==3436== Reachable blocks (those to which a pointer was found) are not shown.
==3436== To see them, rerun with: --show-reachable=yes
==3436== $
这里我们可以看到错误的读取与写入已经被捕获,而所关注的内存块 与他们被分配的位置相关联。我们可以使用调试器在出错点断开程序。
valgrind有许多选项,包含特定的错误类型表达式与内存泄露检 测。要检测我们的例子泄露,我们必须使用一个传递给valgrind的选项。当程序结束时要检测内存泄露,我们需要指定 --leak-check=yes。我们可以使用valgrind --help得到一个选项列表。
工作原理
我 们的程序在valgrind的控制下执行,这会检测我们程序所执行的各种动作,并且执行许多检测,包括内存访问。如果程序访问一个已分配的内存块并且访问 是非法的,valgrind就会输出一条信息。在程序结束时,一个垃圾收集例程就会运行来检测是否在存在分配的内存块没有被释放。这些孤儿内存也会被报 告。
小结
在这一章,我们了解了一些调试工具与技术。Linux提供了一些强大的工具 可以用于由程序中移除缺陷。我们使用gdb来消除程序中的bug,并且了解了如cflow与splint这样的数据分析工具。最后我们了解了当我们使用动 态分配内存时会出现的问题,以及一些用于类似问题诊断的工具,例如ElectricFence与valgrind。