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
如果没有 -g 选项,gdb 将基本无用,因为它无法获取变量名、函数名和行号等信息。
启动 gdb,输入:
bash
其中 filename 是可执行文件,例如你的程序的 a.out。
退出 gdb,输入 q。
4.3.2 r(运行)命令¶
此命令开始执行你的程序。确保包含任何命令行参数;例如,如果在普通(非调试)运行中你会输入:
bash
那么在 gdb 中你 会输入:
bash
如果在同一调试会话中多次使用 r 命令,第一次之后无需再次输入命令行参数;旧的参数会默认重复。
4.3.3 l(列出)命令¶
你可以用此命令列出源文件的一部分。例如,输入:
bash
将显示第52行及其后续几行(要查看更多行,再次按回车)。
如果有多个源文件,在行号前加上文件名和冒号,例如:
bash
你也可以在这里指定函数名,此时列表将从函数的第一行开始。
l 命令有助于找到你想设置断点的位置(见下文)。
4.3.4 b(断点)和 c(继续)命令¶
此命令指定程序在特定行暂停执行。例如:
bash
表示你希望程序每次到达第30行时停止。
如果有多个源文件,同样在行号前加上文件名和冒号。
暂停在指定行后,想继续执行程序,可以输入 c(继续命令)。
你也可以使用函数名指定断点,表示函数中第一个可执行行。例如:
bash
表示在主程序的第一行停止,这通常是调试的第一步。
你可以通过 disable 命令取消断点。
你还可以设置条件断点。例如:
bash
告诉 gdb 仅当 Z 超过 92 时在断点 3(之前设置的)停止。
4.3.5 d(显示)和 p(打印)命令¶
此命令在程序暂停时(例如在断点或执行 n 和 s 命令后)打印指定变量或表达式的值。例如,输入:
bash
表示每次程序暂停时,变量 NC 的当前值会自动打印到屏幕上。
如果让 gdb 打印结构体变量,会打印出结构体的各个字段。如果指定数组名,会打印整个数组。
一段时间后,你可能发现显示某个变量或表达式的价值不如之前。如果是这样,可以使用 undisplay 命令取消 disp 命令。
相关命令是 p;它仅打印变量或表达式的值一次。
在这两种情况下,注意全局变量和局部变量的区别。例如,如果你在函数 F 中有局部变量 L,那么当你不在 F 中时,输入:
bash
会收到错误消息,如“当前上下文中没有变量 L”。
gdb 还允许你以非默认格式打印变量。例如,假设你声明了变量 G:
c
那么:
bash
将以整数格式打印变量,如:
c
但你可能想以十六进制格式打印,例如:
c
gdb 允许你通过输入以下命令实现:
bash
4.3.6 printf 命令¶
这个命令更好,因为它的工作方式类似于 C 的同名函数。例如,假设你有两个整数变量 X 和 Y,想打印它们的值,可以给 gdb 输入:
bash
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
会将 x 的值更改为 12。
4.4 call 命令¶
你可以使用此命令在程序执行期间调用你的程序中的函数。通常,你会为调试目的调用自己编写的函数,例如打印链表。
示例:
bash
确保记得输入括号,即使没有参数。
4.4.1 define 命令¶
此命令可以节省你的输入。你可以将一个或多个命令组合成一个宏。例如,回顾我们之前的例子:
bash
如果你想在调试会话中频繁使用此命令,可以这样做:
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
这听起来很可怕,但实际上这种错误通常是最容易修复的。第一步是确定错误发生的位置;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
因此,在另一个窗口(希望是 X11 窗口,但如果你只使用基于文本的终端,可以使用 GNU screen 程序获得窗口),我们将修复 Main.c 的第25行并重新编译。注意,在此过程中我们不退出 gdb,因为 gdb 加载需要时间。为了做到这一点,我们必须先告诉 gdb 释放我们的可执行文件:
bash
(否则,当我们尝试重新编译程序时,链接器 ld 会告诉我们可执行文件“忙碌”,无法替换。)
修复并重新编译 Main.c 后,下次我们给 gdb 输入 run 命令:
bash
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
别灰心!让我们看看这个新的段错误发生在哪儿。
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
哇!记住,我只设置了 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
什么?到 20 没有报告任何素数?这不对。让我们使用 gdb 逐步执行程序。我们将在 main() 的开头暂停并查看情况。为此,我们设置一个“断点”,即 gdb 将暂停程序执行的地方,以便我们在继续执行前评估情况:
bash
因此,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
发生的情况是,gdb 如请求执行了 Main.c 的第24行——printf() 调用的消息出现在 g292 行——现在再次暂停,在 Main.c 的第25行,显示该行(脚本文件的 g293 行)。
好的,再次输入 n 执行第25行:
bash
由于第25行是 scanf() 调用,在脚本文件的 g295 行,gdb 等待我们的输入,我们输入了 20。然后 gdb 执行了 scanf() 调用,并再次暂停,现在在 Main.c 的第27行(脚本文件的 g296 行)。
现在让我们检查 UpperBound 是否被正确读取。我们认为它是正确的,但记住,调试的基本原则是无论如何都要检查。为此,我们将使用 gdb 的 p(“打印”)命令:
bash
好的,没问题。所以,继续逐行执行程序,使用 n:
bash
此外,让我们使用 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
哎呀!我们忘记了大括号。因此,只有第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
仍然没有输出!好吧,我们现在需要更详细地逐行执行程序。上次我们没有逐行执行 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
果然,s 让我们进入了 CheckPrime() 的第一行。
gdb 提供的另一个服务是告诉我们函数参数的值,在这里是 K = 1。但这不对——我们不应该检查 1 是否为素数。因此,gdb 为我们发现了另一个错误。
事实上,我们的计划是检查从 3 到 UpperBound 的数字是否为素数:main() 中的 for 循环如下:
c
那么 CheckPrime() 的调用呢?以下是 main() 中的整个循环:
c
29 for (N = 3; N <= UpperBound; N += 2) {
30 CheckPrime();
31 if (Prime[N]) printf("%d 是素数\n", N);
32 }
看第30行——我们忘记了参数!这一行应该是:
c
修复后,再次运行程序:
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
脚注:
- 这意味着你应该确保使用一个支持这些操作的优秀编辑器。
- 请非常非常小心地注意:大多数 C 编译器——包括我们在这里使用的 Unix 编译器——不会生成对数组越界的检查。事实上,一个“适度”的越界,例如尝试访问 Prime[57],不会产生段错误。我们尝试访问 Prime[4024] 导致段错误的原因是,这导致我们试图访问不属于我们的内存,即属于机器上其他用户的内存。机器的虚拟内存硬件检测到了这一点。
顺便说一句,这里的 $1 只是表示我们以后可以用这个名称引用 4024。
- 我们可以简单地输入 b CheckPrime,这会在 CheckPrime() 的第一行设置断点,但这样做让我们有机会看到 s 命令的工作方式。顺便说一句,注意 g592、g603 和 g614 行;在这里我们只是按了回车,这导致 gdb 为我们列出更多行。