跳转至

Gdb detailed

翻译:

1 节省时间和避免挫败感!

调试能力是你编程技能中不可或缺的一部分。本文档旨在帮助你迈向这一目标的第一步。

2 通用调试策略

2.1 验证

当你的程序中存在错误(bug),显然是因为在某个地方,你认为正确的事情实际上并不正确。换句话说:

找到你的错误是一个验证你认为正确的事情的过程,直到你发现某个不正确的地方。

以下是你可能认为正确的一些例子:

  • 你认为在源文件的某个位置,某个变量具有某个特定值。
  • 你认为在一个特定的 if-then-else 语句中,执行的是“else”部分。
  • 你认为调用某个函数时,该函数正确接收了其参数。

因此,找到错误位置的过程就是验证所有这些假设!如果你认为某个变量在某个时刻应该有某个值,那就检查它!如果你认为上述“else”部分会执行,那就检查它!通常你的假设会被证实,但最终你会发现某个假设不成立——这时你就找到了错误的所在地。

2.2 二分查找

在验证过程中,使用“二分查找”策略。为了解释这一点,假设你的程序是一个长达200行的单一文件,没有函数调用(这是一种糟糕的编程风格,但在这里便于解释)。

假设你有一个数组 x,你认为在程序执行的几乎整个过程中,x[4] = 9。为了验证这一点,先检查第100行时 x[4] 的值。假设值为9,这意味着错误的位置被缩小到第101-200行!现在检查第150行,假设发现 x[4] = -127,这是错误的。因此,错误位置进一步缩小到第101-150行,接下来你会检查第125行,依此类推。

当然,这是一个过于简化的例子,因为你的程序通常会包含函数调用,因此不能简单地通过行数除以2来定位错误,但你可以看到,通过以类似“二分查找”的方式谨慎选择检查点,你可以快速定位错误。

2.3 如果程序甚至无法编译?

大多数编译错误显而易见且容易修复。但在某些情况下,你可能完全不知道错误在哪里。编译器可能会告诉你错误在函数(或在 C++ 或 Java 中的类)定义的最后一行,例如只有 } 的行。这意味着错误的真实位置可能在函数的任何地方。

为了解决这个问题,再次使用二分查找!首先,暂时删除函数的后半部分(或者,如果方便的话,将其注释掉)。在执行此操作时要小心,否则可能会引入更多错误。如果错误信息消失,那么你知道问题出在被删除的那一半;恢复删除的行,然后再删除那一半的后半部分,依此类推,直到你定位到错误。

3 使用调试工具和优秀的文本编辑器!

3.1 不要将 printf()/cout 作为主要调试工具

如上所述,验证过程通常涉及在代码的不同位置和时间点检查变量的值。过去,你可能通过在 C 或 C++ 源文件中添加临时的 printf 或 cout 语句来实现这一点。这非常不便:你需要在添加这些语句后重新编译程序,而且如果想在多个位置检查某个变量的值,可能需要添加大量这样的语句。重新编译并运行程序后,你可能发现需要更多 printf/cout 语句,于是又得再次编译!修复了错误后,你还得删除所有这些 printf/cout 语句,然后再为下一个错误添加新的。这不仅烦人,还会分散注意力,让你难以专注于寻找错误,容易失去思路。

调试工具让你更方便地打印变量值:无需重新编译,你可以让调试工具自动监控你想观察的任何变量。这里的“便利性”非常重要。减少在重新编译等方面的时间,你就有更多时间和精力专注于追踪错误。

更重要的是,调试工具会告诉你程序中发生严重错误(例如“段错误”)的具体位置。此外,它还能告诉你导致段错误的函数是从哪里调用的。这非常有用。

在某些情况下,printf/cout 结合调试工具可能有用,但如果你将调试工具(而非 printf/cout)作为主要调试手段,你会受益匪浅。

3.2 调试工具推荐
3.2.1 无处不在的 gdb(在 Unix 上)

大多数计算机系统都提供了一种或多种调试工具。这些工具可以在调试过程中为你节省大量时间和挫败感。几乎所有 Unix 系统上都有的工具是 gdb。

一旦你熟练使用一种调试工具,学习其他工具会非常简单。因此,尽管我们这里以 gdb 为例,但其原理适用于任何调试工具。(我并不是说 gdb 是最好的调试器,但因为它非常常见,所以我用它作为例子。)

3.2.2 ddd:gdb 的更好界面

gdb 是一个基于文本的工具,为它开发了许多图形用户界面(GUI)前端,比如 ddd。我强烈推荐使用像 ddd 这样的基于 GUI 的 gdb 界面;请查看我的调试主页以获取和使用这些工具的方法:http://heather.cs.ucdavis.edu/~matloff/debug.html

通过 ddd 界面使用 gdb 更加简单和愉快。例如,设置断点只需点击鼠标,就会出现一个小的停止标志,表明程序会在那里暂停。

不过,我建议先学习基于文本的版本。

3.2.3 优秀的文本编辑器大有帮助

在长时间的调试会话中,你会花很多时间在输入上。这不仅浪费宝贵时间,更重要的是,它非常分散注意力;在频繁地在文件不同部分或文件之间跳转时,很容易失去思路。

我编写了一些专门为程序员设计的文本编辑技巧,地址是:http://heather.cs.ucdavis.edu/~matloff/progedit.html。例如,它提到你应该充分利用撤销/重做操作。以上述二分查找的例子为例,我们试图找到一个难以捉摸的编译错误。建议是删除函数的一半行,然后再恢复它们。如果你的文本编辑器支持撤销功能,恢复这些删除的行将非常简单。

使用支持子窗口的编辑器非常重要。这让你可以在一个窗口中查看函数定义,同时在另一个窗口中查看对该函数的调用。

通常还会结合其他工具与调试器一起使用。例如,vim 编辑器(vi 的增强版)可以与 gdb 接口;请查看我的 vim 网页:http://heather.cs.ucdavis.edu/~matloff/vim.html。你可以在 vim 中发起编译,如果有编译错误,vim 会带你到发生错误的行。

3.3 集成开发环境

此外,调试器有时是集成开发环境(IDE)的一部分。Unix 上可用的众多 IDE 之一是 Code Crusader:http://heather.cs.ucdavis.edu/~matloff/codecrusader.html

它曾经是公共领域的,但遗憾的是,Code Crusader 即将成为商业产品。

对许多人来说,IDE 的一个主要缺点是无法使用自己喜欢的文本编辑器。很多人希望用同一个编辑器进行编程、写邮件、文字处理等。这既方便,又能让他们开发个人别名/宏库以节省工作。

Code Crusader 的一个显著优势是它允许你使用自己的文本编辑器。据我所知,它与 emacs 的兼容性最好。

3.4 我用什么?

我主要使用通过 ddd 访问的 gdb,并用 vim 作为编辑器。我偶尔也会使用 Code Crusader。

4 如何使用 gdb

4.1 易于学习

在我的调试中,我通常只使用几个 gdb 命令,总共大约四五个。因此,你可以很快学会 gdb。之后,如果你愿意,可以学习一些高级命令。

4.2 基本策略

使用 gdb 的典型流程如下:在启动 gdb 后,我们设置断点,即希望程序暂停执行的位置。每次 gdb 遇到断点时,它会在该点暂停程序执行,让我们有机会检查各个变量的值。

在某些情况下,到达断点后,我们会从该点开始单步执行,这意味着 gdb 会在每一行源代码后暂停。这可能很重要,要么是为了进一步确定某个变量值变化的位置,要么是为了观察执行流程,例如查看 if-then-else 结构中哪些部分被执行。

如前所述,整体策略的另一个组成部分涉及段错误。如果我们在运行程序(不在 gdb 下)时收到“段错误”消息,我们可以在 gdb 下运行程序(可能不设置任何断点)。当段错误发生时,gdb 会告诉我们具体发生的位置,从而精确定位错误。(在某些情况下,段错误可能发生在系统调用中,而非我们编写的函数中,此时可以用 gdb 的 bt 命令确定我们的代码中调用系统调用的位置。)在该点,你应该检查错误发生行中引用的所有数组索引和指针的值。通常你会发现要么数组索引值远远超出范围,要么指针为 0(因此无法引用)。另一个常见错误是函数调用中忘记使用 &,例如 scanf()。还有一种常见错误是你进行了系统调用但失败了,却没有检查其返回值是否有错误代码。

4.3 主要 gdb 命令
4.3.1 启动/退出 gdb

在开始之前,确保编译调试程序时使用了 -g 选项,例如:

bash

cc -g sourcefile.c

如果没有 -g 选项,gdb 将基本无用,因为它无法获取变量名、函数名和行号等信息。

启动 gdb,输入:

bash

gdb filename

其中 filename 是可执行文件,例如你的程序的 a.out。

退出 gdb,输入 q。

4.3.2 r(运行)命令

此命令开始执行你的程序。确保包含任何命令行参数;例如,如果在普通(非调试)运行中你会输入:

bash

a.out < z

那么在 gdb 中你 会输入:

bash

r < z

如果在同一调试会话中多次使用 r 命令,第一次之后无需再次输入命令行参数;旧的参数会默认重复。

4.3.3 l(列出)命令

你可以用此命令列出源文件的一部分。例如,输入:

bash

l 52

将显示第52行及其后续几行(要查看更多行,再次按回车)。

如果有多个源文件,在行号前加上文件名和冒号,例如:

bash

l X.c:52

你也可以在这里指定函数名,此时列表将从函数的第一行开始。

l 命令有助于找到你想设置断点的位置(见下文)。

4.3.4 b(断点)和 c(继续)命令

此命令指定程序在特定行暂停执行。例如:

bash

b 30

表示你希望程序每次到达第30行时停止。

如果有多个源文件,同样在行号前加上文件名和冒号。

暂停在指定行后,想继续执行程序,可以输入 c(继续命令)。

你也可以使用函数名指定断点,表示函数中第一个可执行行。例如:

bash

b main

表示在主程序的第一行停止,这通常是调试的第一步。

你可以通过 disable 命令取消断点。

你还可以设置条件断点。例如:

bash

b 3 Z > 92

告诉 gdb 仅当 Z 超过 92 时在断点 3(之前设置的)停止。

4.3.5 d(显示)和 p(打印)命令

此命令在程序暂停时(例如在断点或执行 n 和 s 命令后)打印指定变量或表达式的值。例如,输入:

bash

disp NC

表示每次程序暂停时,变量 NC 的当前值会自动打印到屏幕上。

如果让 gdb 打印结构体变量,会打印出结构体的各个字段。如果指定数组名,会打印整个数组。

一段时间后,你可能发现显示某个变量或表达式的价值不如之前。如果是这样,可以使用 undisplay 命令取消 disp 命令。

相关命令是 p;它仅打印变量或表达式的值一次。

在这两种情况下,注意全局变量和局部变量的区别。例如,如果你在函数 F 中有局部变量 L,那么当你不在 F 中时,输入:

bash

disp L

会收到错误消息,如“当前上下文中没有变量 L”。

gdb 还允许你以非默认格式打印变量。例如,假设你声明了变量 G:

c

int G;

那么:

bash

p G

将以整数格式打印变量,如:

c

printf("%d\n", G);

但你可能想以十六进制格式打印,例如:

c

printf("%x\n", G);

gdb 允许你通过输入以下命令实现:

bash

p /x G
4.3.6 printf 命令

这个命令更好,因为它的工作方式类似于 C 的同名函数。例如,假设你有两个整数变量 X 和 Y,想打印它们的值,可以给 gdb 输入:

bash

printf "X = %d, Y = %d\n", X, Y
4.3.7 n(下一步)和 s(步进)命令

这些命令告诉 gdb 执行程序的下一行,然后再次暂停。如果该行是函数调用,n 和 s 会产生不同的结果:

如果你使用 s,下一次暂停将在函数的第一行;如果你使用 n,下一次暂停将在函数调用后的下一行(函数会单步执行,但不会在其中暂停)。这非常重要,可以节省很多时间:如果你认为错误不在函数内部,就使用 n,这样你不会浪费时间在函数内单步执行。

当你在函数调用处使用 s 时,gdb 还会告诉你参数的值,这对验证非常有用,如本文档开头所述。

4.3.8 bt(回溯)命令

如果你的程序出现“总线错误”或“段错误”这样的神秘错误消息,bt 命令至少会告诉你错误发生在程序的哪个位置,如果在函数中,还会告诉你该函数是从哪里调用的。这非常有价值。

4.3.9 set 命令

有时使用 gdb 更改程序变量的值非常有用,此命令可以实现。例如,如果有一个 int 变量 x,gdb 命令:

bash

set variable x = 12

会将 x 的值更改为 12。

4.4 call 命令

你可以使用此命令在程序执行期间调用你的程序中的函数。通常,你会为调试目的调用自己编写的函数,例如打印链表。

示例:

bash

(gdb) call x()

确保记得输入括号,即使没有参数。

4.4.1 define 命令

此命令可以节省你的输入。你可以将一个或多个命令组合成一个宏。例如,回顾我们之前的例子:

bash

printf "X = %d, Y = %d\n", X, Y

如果你想在调试会话中频繁使用此命令,可以这样做:

bash

(gdb) define pxy
Type commands for definition of "pxy".
End with a line saying just "end".
>printf "X = %d, Y = %d\n", X, Y
>end

然后你只需输入“pxy”即可调用它。

4.5 在不退出 gdb 的情况下重新编译源代码的效果

如你所知,调试会话包括编译、调试、编辑、重新编译、调试、编辑、重新编译……

关键一点是,在重新编译之前不要退出 gdb。重新编译后,当你发出 r 命令重新运行程序时,gdb 会注意到源文件比它使用的二进制可执行文件更新,因此会自动加载新的二进制文件。由于从头启动 gdb 需要时间,因此在编译之间留在 gdb 中比退出后再重新启动要方便得多。

4.6 gdb 示例

在本节中,我们将通过展示在实际程序上使用 gdb 的脚本文件记录来介绍 gdb。为了区分脚本文件中的行号和 C 源文件中的行号,我在脚本文件的每行行号前加了一个“g”。例如,行 g56 表示脚本文件中的第56行,而不是 C 源文件中的第56行。

首先,使用 cat 显示程序源文件:

bash

g2  mole.matloff% cat Main.c
g3  
g4  
g5  /* 寻找素数的程序
g6  
g7     将(在修复错误后)报告所有小于或等于用户提供的上限的素数列表
g8  
g9     充满错误! */
g10  
g11  
g12  
g13  
g14  #include "Defs.h"
g15  
g16  
g17  int Prime[MaxPrimes],  /* 如果 I 是素数,Prime[I] 将为 1,否则为 0 */
g18      UpperBound;  /* 我们将检查所有小于或等于此数的数字是否为素数 */
g19  
g20  
g21  
g22  main()
g23  
g24  {  int N;
g25  
g26     printf("请输入上限\n");
g27     scanf("%d", UpperBound);
g28  
g29     Prime[2] = 1;
g30  
g31     for (N = 3; N <= UpperBound; N += 2)
g32        CheckPrime();
g33        if (Prime[N]) printf("%d 是素数\n", N);
g34  }
g35   
g36  mole.matloff% 
g37  mole.matloff% 
g38  mole.matloff% 
g39  mole.matloff% cat CheckPrimes.c
g40  cat: CheckPrimes.c: No such file or directory
g41  mole.matloff% cat CheckPrime.c
g42  
g43  
g44  #include "Defs.h"
g45  #include "Externs.h"
g46  
g47  
g48  CheckPrime(K)
g49     int K;
g50  
g51  {  int J;
g52  
g53     /* 计划:检查 J 是否能整除 K,对于所有满足以下条件的 J 值:
g54        
g55           (a) 自身是素数(如果 J 不是素数,无需尝试),并且
g56           (b) 小于或等于 sqrt(K)(如果 K 有一个大于此平方根的除数,
g57               它必然也有一个较小的除数,因此无需检查较大的除数) */
g58     
g59     J = 2;
g60     while (1)  {
g61        if (Prime[J] == 1)
g62           if (K % J == 0)  {
g63              Prime[K] = 0;
g64              return;
g65           }
g66        J++;
g67     }
g68     
g69     /* 如果到达这里,说明 K 没有除数,因此它是素数 */
g70     Prime[K] = 1; 
g71  }
g72   
g73  mole.matloff% 
g74  mole.matloff% 
g75  mole.matloff% 
g76  mole.matloff% cat Defs.h
g77  
g78  #define MaxPrimes 50
g79  mole.matloff% cat Externs.h
g80  
g81  
g82  #include "Defs.h"
g83  
g84  
g85  extern int Prime[MaxPrimes];

行 g5-g10 的注释说明了程序的目标,即寻找素数。

好了,让我们开始。首先编译程序:

bash

g92  mole.matloff% make
g93  cc -g -c Main.c
g94  cc -g -c CheckPrime.c
g95  "CheckPrime.c", line 31: warning: statement not reached
g96  cc -g -o FindPrimes Main.o CheckPrime.o

关于 CheckPrime.c 第31行的警告听起来不妙(确实如此),但我们先忽略它,看看会发生什么。运行程序:

bash

g100  mole.matloff% FindPrimes
g101  请输入上限
g102  20
g103  段错误

这听起来很可怕,但实际上这种错误通常是最容易修复的。第一步是确定错误发生的位置;gdb 可以为我们做到这一点:我们进入 gdb,然后重新运行程序以重现错误:

bash

g104  mole.matloff% gdb FindPrimes
g105  GDB 是免费软件,你可以在特定条件下分发其副本;
g106   输入“show copying”查看条件。
g107  GDB 没有任何担保;输入“show warranty”查看详情
g108  GDB 4.7,版权 1992 自由软件基金会...

(gdb)

我现在使用 gdb 的 r 命令运行程序(如果程序有命令行参数,我会在 r 后输入它们):

bash

g109  (gdb) r
g110  启动程序:/tmp_mnt/lion/d/guest/matloff/tmp/FindPrimes 
g111  请输入上限
g112  20
g113  
g114  程序收到信号 11,段错误
g115  0xf773cb88 in _doscan ()

错误发生在 _doscan() 函数中。这不是我的函数,因此它一定是被我使用的 C 库函数之一调用的,即 printf() 或 scanf()。根据名称 _doscan,看起来像是后者,但无论如何,要找出答案,我们可以使用 gdb 的 bt(“回溯”)命令,查看 _doscan() 是从哪里调用的:

bash

g116  (gdb) bt
g117  #0  0xf773cb88 in _doscan ()
g118  #1  0xf773c2e8 in _doscan ()
g119  #2  0xf773b9dc in scanf ()
g120  #3  0x22dc in main () at Main.c:25

果然!它确实是从 scanf() 调用的,而 scanf() 又是在 Main.c 第25行从 main() 调用的。

由于 scanf() 是一个 C 库函数,假设它已经经过充分调试,因此错误可能不在 scanf() 中。错误一定出在我们对 Main.c 第25行 scanf() 的调用中。

让我们看看那个调用。使用 gdb 的 l(“列出”)命令,列出 Main.c 中导致错误的行及其周围的行:

bash

g121  (gdb) l Main.c:25
g122  20      main()
g123  21      
g124  22      {  int N;
g125  23      
g126  24         printf("请输入上限\n");
g127  25         scanf("%d", UpperBound);
g128  26      
g129  27         Prime[2] = 1;
g130  28      
g131  29         for (N = 3; N <= UpperBound; N += 2)

是的,一个著名的“C 初学者错误”——我们在 UpperBound 前忘记了 &!Main.c 的第25行应该是:

c

scanf("%d", &UpperBound);

因此,在另一个窗口(希望是 X11 窗口,但如果你只使用基于文本的终端,可以使用 GNU screen 程序获得窗口),我们将修复 Main.c 的第25行并重新编译。注意,在此过程中我们不退出 gdb,因为 gdb 加载需要时间。为了做到这一点,我们必须先告诉 gdb 释放我们的可执行文件:

bash

(gdb) kill

(否则,当我们尝试重新编译程序时,链接器 ld 会告诉我们可执行文件“忙碌”,无法替换。)

修复并重新编译 Main.c 后,下次我们给 gdb 输入 run 命令:

bash

(gdb) r

gdb 将自动加载新编译的可执行文件(它会注意到我们重新编译了,因为它会看到我们的 .c 源文件比可执行文件新)。注意,我们无需再次声明命令行参数(如果有),因为 gdb 会记住它们。它还会记住我们的断点,因此无需再次设置。(gdb 会自动更新这些断点的行号,以适应我们修改源文件时发生的行号变化。)

Main.c 现在如下:

bash

g137  mole.matloff% cat Main.c
g138  
g139  
g140  /* 寻找素数的程序
g141  
g142     将(在修复错误后)报告所有小于或等于用户提供的上限的素数列表
g143  
g144     充满错误! */
g145  
g146  
g147  
g148  #include "Defs.h"
g149  
g150  
g151  int Prime[MaxPrimes],  /* 如果 I 是素数,Prime[I] 将为 1,否则为 0 */
g152      UpperBound;  /* 我们将检查所有小于或等于此数的数字是否为素数 */
g153  
g154  
g155  main()
g156  
g157  {  int N;
g158  
g159     printf("请输入上限\n");
g160     scanf("%d", &UpperBound);
g161  
g162     Prime[2] = 1;
g163  
g164     for (N = 3; N <= UpperBound; N += 2)
g165        CheckPrime();
g166        if (Prime[N]) printf("%d 是素数\n", N);
g167  }

现在再次运行程序(不在 gdb 中,尽管我们在另一个窗口中仍保持 gdb 打开):

bash

g174  mole.matloff% !F
g175  FindPrimes
g176  请输入上限
g177  20
g178  段错误

别灰心!让我们看看这个新的段错误发生在哪儿。

bash

g188  (gdb) r
g189  启动程序:/tmp_mnt/lion/d/guest/matloff/tmp/FindPrimes 
g190  请输入上限
g191  20
g192  
g193  程序收到信号 11,段错误
g194  0x2388 in CheckPrime (K=1) at CheckPrime.c:21
g195  21            if (Prime[J] == 1)

记住,如前所述,段错误最常见的原因之一是数组索引严重错误。因此,我们应该高度怀疑这里的 J,并使用 gdb 的 p(“打印”)命令检查其值:

bash

g207  (gdb) p J
g208  $1 = 4024

哇!记住,我只设置了 Prime 数组包含 50 个整数,而我们却试图访问 Prime[4024]!2

因此,gdb 精确地指出了我们错误的确切来源——这一行 J 的值太大了。现在我们需要确定为什么 J 这么大。让我们使用 gdb 的 l 命令查看整个函数:

bash

g196  (gdb) l CheckPrime.c:12
g53     /* 计划:检查 J 是否能整除 K,对于所有满足以下条件的 J 值:
g54
g55           (a) 自身是素数(如果 J 不是素数,无需尝试),并且
g56           (b) 小于或等于 sqrt(K)(如果 K 有一个大于此平方根的除数,
g57               它必然也有一个较小的除数,因此无需检查较大的除数) */
g58        
g200  19         J = 2;
g201  20         while (1)  {
g202  21            if (Prime[J] == 1)
g203  22               if (K % J == 0)  {
g204  23                  Prime[K] = 0;
g205  24                  return;
g206  25               }

看看 g56-g58 行的注释。我们应该在 J 达到 sqrt(K) 后停止搜索。然而,从 g201-g206 行可以看出,我们从未进行此检查,因此 J 不断增大,最终达到 4024,触发了段错误。

修复这个问题后,新的 CheckPrime.c 如下:

bash

g214  mole.matloff% cat CheckPrime.c
g215  
g216  
g217  #include "Defs.h"
g218  #include "Externs.h"
g219  
g220  
g221  CheckPrime(K)
g222     int K;
g223  
g224  {  int J;
g225  
g226     /* 计划:检查 J 是否能整除 K,对于所有满足以下条件的 J 值:
g227        
g228           (a) 自身是素数(如果 J 不是素数,无需尝试),并且
g229           (b) 小于或等于 sqrt(K)(如果 K 有一个大于此平方根的除数,
g230               它必然也有一个较小的除数,因此无需检查较大的除数) */
g231     
g232     for (J = 2; J*J <= K; J++)  
g233        if (Prime[J] == 1)
g234           if (K % J == 0)  {
g235              Prime[K] = 0;
g236              return;
g237           }
g238     
g239     /* 如果到达这里,说明 K 没有除数,因此它是素数 */
g240     Prime[K] = 1; 
g241  }

好了,再试一次:

bash

g248  mole.matloff% !F
g249  FindPrimes
g250  请输入上限
g251  20
g252  mole.matloff%

什么?到 20 没有报告任何素数?这不对。让我们使用 gdb 逐步执行程序。我们将在 main() 的开头暂停并查看情况。为此,我们设置一个“断点”,即 gdb 将暂停程序执行的地方,以便我们在继续执行前评估情况:

bash

g261  (gdb) b main
g262  断点 1 在 0x22b4:文件 Main.c,第24行。

因此,gdb 将在我们每次到达 Main.c 第24行时暂停程序执行。这是断点 1;我们可能(也将会)设置其他断点,因此需要编号来区分它们,例如指定要取消哪个断点。

现在使用 r 命令运行程序:

bash

g286  (gdb) r
g287  启动程序:/tmp_mnt/lion/d/guest/matloff/tmp/FindPrimes 
g288  
g289  断点 1,main () at Main.c:24
g290  24         printf("请输入上限\n");

如计划,gdb 在 main() 的第一行(第24行)停止。现在我们将逐行执行程序,使用 gdb 的 n(“下一步”)命令:

bash

g291  (gdb) n
g292  请输入上限
g293  25         scanf("%d", &UpperBound);

发生的情况是,gdb 如请求执行了 Main.c 的第24行——printf() 调用的消息出现在 g292 行——现在再次暂停,在 Main.c 的第25行,显示该行(脚本文件的 g293 行)。

好的,再次输入 n 执行第25行:

bash

g294  (gdb) n
g295  20
g296  27         Prime[2] = 1;

由于第25行是 scanf() 调用,在脚本文件的 g295 行,gdb 等待我们的输入,我们输入了 20。然后 gdb 执行了 scanf() 调用,并再次暂停,现在在 Main.c 的第27行(脚本文件的 g296 行)。

现在让我们检查 UpperBound 是否被正确读取。我们认为它是正确的,但记住,调试的基本原则是无论如何都要检查。为此,我们将使用 gdb 的 p(“打印”)命令:

bash

g297  (gdb) p UpperBound
g298  $1 = 20

好的,没问题。所以,继续逐行执行程序,使用 n:

bash

g299  (gdb) n
g300  29         for (N = 3; N <= UpperBound; N += 2)

此外,让我们使用 gdb 的 disp(“显示”)命令跟踪 N 的值。disp 类似于 p,但 disp 会在每次程序暂停时打印变量值,而 p 只打印一次。

bash

g301  (gdb) disp N
g302  1: N = 0
g303  (gdb) n
g304  30            CheckPrime();
g305  1: N = 3
g306  (gdb) n
g307  29         for (N = 3; N <= UpperBound; N += 2)
g308  1: N = 3

嘿,这是怎么回事?执行第30行后,程序又回到了第29行——跳过了第31行。以下是循环的样子:

c

29   for (N = 3; N <= UpperBound; N += 2)
30      CheckPrime();
31      if (Prime[N]) printf("%d 是素数\n", N);

哎呀!我们忘记了大括号。因此,只有第30行(而不是第30和31行)构成了循环体。难怪第31行没有被执行。

修复后,Main.c 如下:

bash

g314  mole.matloff% cat Main.c
g315  
g316  
g317  /* 寻找素数的程序
g318  
g319     将(在修复错误后)报告所有小于或等于用户提供的上限的素数列表
g320  
g321     充满错误! */
g322  
g323  
g324  
g325  #include "Defs.h"
g326  
g327  
g328  int Prime[MaxPrimes],  /* 如果 I 是素数,Prime[I] 将为 1,否则为 0 */
g329      UpperBound;  /* 我们将检查所有小于或等于此数的数字是否为素数 */
g330  
g331  
g332  main()
g333  
g334  {  int N;
g335  
g336     printf("请输入上限\n");
g337     scanf("%d", &UpperBound);
g338  
g339     Prime[2] = 1;
g340  
g341     for (N = 3; N <= UpperBound; N += 2)  {
g342        CheckPrime();
g343        if (Prime[N]) printf("%d 是素数\n", N);
g344     }
g345  }

好的,再试一次:

bash

g352  mole.matloff% !F
g353  FindPrimes
g354  请输入上限
g355  20
g356  mole.matloff%

仍然没有输出!好吧,我们现在需要更详细地逐行执行程序。上次我们没有逐行执行 CheckPrime() 函数,所以现在需要这样做:

bash

g586  (gdb) l Main.c:1
g587  1       
g588  2       
g589  3       /* 寻找素数的程序
g590  4       
g591  5          将(在修复错误后)报告所有小于或等于用户提供的上限的素数列表
g592  (gdb) 
g593  6          */
g594  7       
g595  8          充满错误! */
g596  9       
g597  10      
g598  11      
g599  12      #include "Defs.h"
g600  13      
g601  14      
g602  15      int Prime[MaxPrimes],  /* 如果 I 是素数,Prime[I] 将为 1,否则为 0 */
g603  (gdb) 
g604  16          UpperBound;  /* 我们将检查所有小于或等于此数的数字是否为素数 */
g605  17      
g606  18      
g607  19      
g608  20      main()
g609  21      
g610  22      {  int N;
g611  23      
g612  24         printf("请输入上限\n");
g613  25         scanf("%d", &UpperBound);
g614  (gdb) 
g615  26      
g616  27         Prime[2] = 1;
g617  28      
g618  29         for (N = 3; N <= UpperBound; N += 2)  {
g619  30            CheckPrime();
g620  31            if (Prime[N]) printf("%d 是素数\n", N);
g621  32         }
g622  33      }
g623  34       
g624  (gdb) b 30
g625  断点 1 在 0x2308:文件 Main.c,第30行。

我们在这里为 CheckPrime 调用设置了一个断点。

现在运行程序:

bash

g626  (gdb) r
g627  启动程序:/tmp_mnt/lion/d/guest/matloff/tmp/FindPrimes 
g628  请输入上限
g629  20
g630  
g631  断点 1,main () at Main.c:30
g632  30            CheckPrime();

gdb 按请求在第30行停止。现在,我们将使用 s(“步进”)而不是 n 命令。s 与 n 相同,只是它会进入函数,而不是像 n 那样跳过函数:

bash

g633  (gdb) s
g634  CheckPrime (K=1) at CheckPrime.c:19
g635  19         for (J = 2; J*J <= K; J++)

果然,s 让我们进入了 CheckPrime() 的第一行。

gdb 提供的另一个服务是告诉我们函数参数的值,在这里是 K = 1。但这不对——我们不应该检查 1 是否为素数。因此,gdb 为我们发现了另一个错误。

事实上,我们的计划是检查从 3 到 UpperBound 的数字是否为素数:main() 中的 for 循环如下:

c

for (N = 3; N <= UpperBound; N += 2)

那么 CheckPrime() 的调用呢?以下是 main() 中的整个循环:

c

29         for (N = 3; N <= UpperBound; N += 2)  {
30            CheckPrime();
31            if (Prime[N]) printf("%d 是素数\n", N);
32         }

看第30行——我们忘记了参数!这一行应该是:

c

30            CheckPrime(N);

修复后,再次运行程序:

bash

g699  mole.matloff% !F
g700  FindPrimes
g701  请输入上限
g702  20
g703  3 是素数
g704  5 是素数
g705  7 是素数
g706  11 是素数
g707  13 是素数
g708  17 是素数
g709  19 是素数

好的,程序现在似乎正常工作了。

4.7 gdb 文档

你可以购买一本印刷的手册,但可能不值得,因为所有文档都可以在网上获取。首先,你可以通过输入 h(“帮助”)获得命令类别的概览,然后通过输入 h name 获取任何类别或单个命令的信息,其中 name 是类别或单个命令的名称。例如,要获取有关使用断点的信息,输入:

bash

h breakpoints

脚注:

  1. 这意味着你应该确保使用一个支持这些操作的优秀编辑器。
  2. 请非常非常小心地注意:大多数 C 编译器——包括我们在这里使用的 Unix 编译器——不会生成对数组越界的检查。事实上,一个“适度”的越界,例如尝试访问 Prime[57],不会产生段错误。我们尝试访问 Prime[4024] 导致段错误的原因是,这导致我们试图访问不属于我们的内存,即属于机器上其他用户的内存。机器的虚拟内存硬件检测到了这一点。

顺便说一句,这里的 $1 只是表示我们以后可以用这个名称引用 4024。

  1. 我们可以简单地输入 b CheckPrime,这会在 CheckPrime() 的第一行设置断点,但这样做让我们有机会看到 s 命令的工作方式。顺便说一句,注意 g592、g603 和 g614 行;在这里我们只是按了回车,这导致 gdb 为我们列出更多行。