调试(四)


了解更多有关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 <bruce@perens.com>
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 <bruce@perens.com>
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。

posted @ 2009-05-11 22:51  jlins  阅读(290)  评论(0编辑  收藏  举报