调试(二)

代码监视

正如我们在前面所提到的,当程序并未按我们预期的那样运行时,重读我们的程序是一个好主意。出于本章的目的,我们假设代码已经进行重新检查,并且明显的错误已经进行了处理。

我们可以使用一些工具来帮助我们进行代码检查,编译器就是明显的一个。如果在我们的程序中存在任何语法错误,编译器可以通知我们。

我们在后面还会提到其他的工具,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 <stdio.h>
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 <file>来由一个可执行文件中移除调试信息,而不需要重新编译。

你可能感兴趣的:(调试)