了解 gdb

作者:Michael Loukides

您可能需要调试器有很多原因——最明显的原因是您是一名程序员,并且您编写了一个无法正常工作的应用程序。 除此之外,Linux 在很大程度上依赖于共享源代码和从其他 Unix 系统移植代码。 对于这两种类型的代码,您都可能会遇到原始作者在其平台上没有遇到的问题。 因此,与一个好的 C 调试器交朋友是值得的。

幸运的是,自由软件基金会提供了一个名为 gdb 的出色调试器,它适用于 C 和 C++ 代码。 gdb 允许您在程序中停止执行,在执行期间检查和更改变量,并跟踪程序的执行方式。 它还具有类似于 bash(GNU shell)和 Emacs 的命令行编辑和历史记录功能。 事实上,它现在有一个图形界面。 但是,由于我们已经习惯了使用命令行界面(并且它更容易在印刷品中展示),因此在本文中我们将坚持使用命令行界面。

要获得有关所有 gdb 命令的完整文档,请阅读在线 使用 gdb 调试 手册,或从自由软件基金会订购。

gdb 的编译

在您可以使用 gdb 调试程序之前,请使用 -g 选项编译和链接您的代码。 这会导致编译器生成增强的符号表。 例如,命令

$ gcc -g file1.c file2.c file3.o

编译 C 源文件 file1.c 和 file2.c,生成扩展的符号表以供 gdb 使用。 这些文件与 file3.o 链接,file3.o 是一个已经编译的对象文件。

编译器的 -g-O 选项并不冲突; 您可以同时进行优化和编译以进行调试。 此外,与许多其他调试器不同,gdb 甚至会给您一些可以理解的结果。 但是,调试优化的代码很困难,因为本质上,优化会使机器代码与源代码所说的内容有所不同。

启动 gdb

要使用 gdb 调试已编译的程序,请使用命令

$ gdb program [ core-dump ]

其中 program 是您要调试的可执行文件的文件名,core-dump 是先前尝试运行程序时留下的 core dump 文件的名称。 通过使用 gdb 检查 core dump,您可以发现程序失败的位置以及失败的原因。 例如,以下命令告诉 gdb 读取可执行文件 qsort2 和 core dump core.2957

$ gdb qsort2 core.2957
gdb is free software and you are welcome to
distribute copies of it under certain conditions;
type "show copying" to see the conditions.
There is absolutely no warranty for gdb;
type "show warranty" for details.
gdb 4.13 (sparc-sun-sunos4.1.3),
Copyright 1993 Free Software Foundation, Inc...
Core was generated by `qsort2'.
Program terminated with signal 7, Emulator trap.
#0  0x2734 in qsort2 (l=93643, u=93864, strat=1)
at qsort2.c:118
118           do i++; while (i <= u && x[i] < t);
(gdb) quit
$

启动过程相当详细; 它会告诉您您正在使用的 gdb 版本。 然后它会告诉您 core 文件是如何生成的(通过程序 qsort2,它接收到信号 7,一个“模拟器陷阱”),以及程序正在做什么(执行第 118 行)。 提示符“(gdb)”告诉您 gdb 已准备好接收命令。 在这种情况下,我们只需退出。

可执行文件和 core 文件参数都是可选的。 您可以在以后使用 core 命令提供 core 文件。

基本 gdb 命令

只需几个命令,您就可以在 gdb 中完成大部分工作。 您必须做的基本事情是:查看您的源代码、设置断点、运行程序和检查变量。

如果您忘记了要使用哪个命令(或想检查晦涩的功能),请使用内置的帮助工具。 您可以请求特定命令(例如 help print)或关于许多特殊主题的帮助。

列出文件

要查看从中编译可执行程序的源文件的内容,请使用命令 list

$ gdb qsort2
(gdb) list
13     void qsort2();
14     void swap();
15     void gen_and_sort();
16     void init_organ();
17     void init_random();
18     void print_array();
19
20     main()
21     {
22            int power=1;
(gdb)

要打印当前正在调试的文件中的特定行,请使用 list 命令

(gdb) list line1,line2

要列出特定函数的前 10 行,请使用 list 命令

(gdb) list routine-name
执行程序

要运行您正在调试的程序,请使用 run 命令。 后面可以跟您要传递给程序的任何参数,包括标准输入和输出说明符 < 和 >,以及 shell 通配符(*?[])。 您不能使用 C-shell 历史记录(!)或管道(|)。

例如,考虑通过 gdb 运行程序 exp。 以下 gdb 命令使用参数 -b 运行 exp,从 invalues 获取 exp 的标准输入,并将标准输出重定向到文件 outtable

$ gdb exp
(gdb) run -b < invalues > outtable

也就是说,此命令运行 exp -b < invalues > outtable。 如果您没有设置任何断点或使用任何其他 gdb 调试功能,exp 将运行直到终止,无论是正确还是错误地终止。

如果您正在调试的程序异常终止,控制权将返回给 gdb。 然后,您可以使用 gdb 命令来查明程序终止的原因。 backtrace 命令提供堆栈回溯,显示程序崩溃时正在执行的操作

$ gdb badref
(gdb) run
Starting program: /home/los/mikel/cuser/badref
0x22c8 in march_to_infinity () at badref.c:16
16               h |= *p;
(gdb) backtrace
#0  0x22c8 in march_to_infinity () at badref.c:16
#1  0x2324 in setup () at badref.c:25
#2  0x2340 in main () at badref.c:30
(gdb)

backtrace(通常缩写为 back)生成所有活动过程及其调用参数的列表,从最近的开始。 因此,此显示表明程序在名为 march_to_infinity() 的函数中崩溃; 此函数由函数 setup() 调用,而 setup() 又由函数 main() 调用。 剩下的唯一事情是弄清楚 march_to_infinity() 中到底出了什么问题。

打印数据

您可以使用 print 命令检查变量值。 让我们用它来看看前一个程序中到底发生了什么。 首先,我们将列出一些代码,看看我们正在处理什么

(gdb) list
8
9            p=&j;
10           /* march off the end of the world*/
11           for ( i = 0; i < VERYBIG; i++)
12           {
13                h |= *p;
14                p++;
15           }
16      printf("h: %d\en",h);
17

应该已经很清楚发生了什么。 p 是某种指针; 我们可以使用 whatis 命令来测试它,该命令向我们显示其声明

(gdb) whatis p
type = int *
(gdb) print p
$1 = (int *) 0xf8000000
(gdb) print *p
$2 = Cannot access memory at address 0xf8000000.
(gdb) print h
$3 = -1
(gdb)

当我们查看 p 时,我们看到它指向太空中的某个地方。 当然,没有 临时的 方法来知道 p 的这个值是否合法。 但是我们可以看看是否可以读取 p 指向的数据,就像我们的程序所做的那样——当我们给出命令 print *p 时,我们看到它指向无法访问的数据。

print 是 gdb 的真正强大功能之一。 您可以使用它来打印您正在调试的语言中任何有效表达式的值。 除了程序中的变量外,表达式还可以包括

  • 对程序中函数的调用; 这些函数调用可能具有“副作用”(即,它们可以执行诸如修改全局变量之类的操作,这些全局变量在您继续程序执行时将可见)。

    (gdb) print find_entry(1.0)
    $1 = 3
    
  • 数据结构和其他复杂对象。

    (gdb) print *table_start
    $8 = {e_reference = '\e000' <repeats 79 times>,
    location = 0x0, next = 0x0}
    
断点

断点允许您在程序执行时暂时停止程序。 当程序在断点处停止时,您可以检查或修改变量、执行函数或执行任何其他 gdb 命令。 这使您可以检查程序的状态,以确定执行是否正确进行。 然后,您可以从程序停止的位置恢复程序执行。

break 命令(您可以缩写为 b)在您正在调试的程序中设置断点。 此命令具有以下形式

break 行号

在执行给定行之前停止程序。

break 函数名

在进入命名函数之前停止程序。

break 行号或函数名 if 条件

当程序到达给定的行或函数时,如果以下 condition 为真,则停止程序。

命令 break 函数名 在指定函数的入口处设置断点。 当程序正在执行时,gdb 将在给定函数的第一个可执行行处暂时停止程序。 例如,下面的 break 命令在函数 init_random() 的入口处设置断点。 然后 run 命令执行程序,直到它到达此函数的开头。 执行在 init_random() 中的第一个可执行行处停止,这是一个 for 循环,从源文件的第 155 行开始

$ gdb qsort2
(gdb) break init_random
Breakpoint 1 at 0x28bc: file qsort2.c, line 155.
(gdb) run
Starting program: /home/los/mikel/cuser/qsort2
Tests with RANDOM inputs and FIXED pivot
Breakpoint 1, init_random (number=10) at
qsort2.c:155
155             for (i = 0; i < number; i++) {
(gdb)

当您设置断点时,gdb 会分配一个唯一的标识号(在本例中为 1),并打印有关断点的一些基本信息。 每当它到达断点时,gdb 都会打印断点的标识号、描述和当前行号。 如果您在程序中设置了多个断点,则标识号会告诉您哪个断点导致程序停止。 然后 gdb 会显示程序停止的行。

要在程序到达特定源代码行时停止执行,请使用 break 行号 命令。 例如,以下 break 命令在程序的第 155 行设置断点

(gdb) break 155
Note: breakpoint 1 also set at pc 0x28bc.
Breakpoint 2 at 0x28bc: file qsort2.c, line 155.
(gdb)

当在断点处停止时,您可以使用 continue 命令(您可以缩写为 c)继续执行

$ gdb qsort2
(gdb) break init_random
Breakpoint 1 at 0x28bc: file qsort2.c, line 155.
(gdb) run
Starting program: /home/los/mikel/cuser/qsort2
Tests with RANDOM inputs and FIXED pivot
Breakpoint 1, init_random (number=10) at
qsort2.c:155
155             for (i = 0; i < number; i++) {
(gdb) continue
Continuing.
test of 10 elements: user + sys time, ticks: 0
Breakpoint 1, init_random (number=100) at
qsort2.c:155
155             for (i = 0; i < number; i++) {
(gdb)

执行将继续,直到程序结束、您到达另一个断点或发生错误。

gdb 支持另一种断点,称为“观察点”。 观察点有点像我们刚刚讨论的“break-if”断点,只是它们没有附加到特定的行或函数入口。 当表达式为真时,观察点会停止程序:例如,以下命令在变量 testsize 大于 100000 时停止程序。

(gdb) watch testsize > 100000

观察点是一个很棒的想法,但它们很难有效地使用。 如果某些东西随机破坏了一个重要的变量,而您又无法弄清楚是什么,那么它们正是您想要的:程序崩溃了,您发现 mungus 被设置为一些乱七八糟的值,但您知道应该设置 mungus 的代码可以工作; 它显然被其他东西破坏了。 问题是在没有特殊硬件支持的情况下(只有少数工作站上存在),设置观察点会将您的程序速度降低 100 倍左右。 因此,如果您真的绝望了,可以使用常规断点使您的程序尽可能接近故障点; 设置一个观察点; 让程序使用 continue 命令继续执行; 让你的程序整夜运行。

单步执行

gdb 提供两种形式的单步执行。 next 命令在遇到调用时执行整个函数,而 step 命令进入函数并一次执行一个语句。 为了理解这两个命令之间的区别,请查看它们在调试简单程序时的行为。 考虑以下示例

$ gdb qsort2
(gdb) break main
Breakpoint 6 at 0x235c: file qsort2.c, line 40.
(gdb) run
Breakpoint 6, main () at qsort2.c:40
40      int power=1;
(gdb) step
43      printf("Tests with RANDOM inputs
and FIXED pivot\n");
(gdb) step
Tests with RANDOM inputs and FIXED pivot
45      for (testsize = 10; testsize <=
MAXSIZE; testsize *= 10){
(gdb) step
46           gen_and_sort(testsize,RANDOM,FIXED);
(gdb) step
gen_and_sort (numels=10, genstyle=0, strat=1) at
qsort2.c:79
79      s = &start_time;
(gdb)

我们在 main() 函数的入口处设置了一个断点,并开始单步执行。 经过几个步骤后,我们到达对 gen_and_sort() 的调用。 此时,step 命令将我们带入函数 gen_and_sort(); 突然之间,我们在第 79 行而不是第 46 行执行。 它没有完整地执行 gen_and_sort(),而是“步入”了函数。 相比之下,next 将完全执行第 46 行,包括对 gen_and_sort() 的调用。

在调用堆栈中向上和向下移动

许多信息性命令会根据您在程序中的位置而变化; 它们的参数和输出取决于当前帧。 通常,当前帧是您停止的函数。 但是,有时您希望更改此默认设置,以便您可以执行诸如显示来自另一个函数的多个变量之类的操作。

命令 updown 使您在当前调用堆栈中向上和向下移动一级。 命令 up ndown n 使您在堆栈中向上或向下移动 n 级。 向下堆栈意味着离程序的 main() 函数更远; 向上意味着更靠近 main()。 通过使用 updown,您可以调查堆栈中任何函数中的局部变量,包括递归调用。 当然,在您先向上移动之前,您无法向下移动——默认情况下,您位于当前正在执行的函数中,这与您在堆栈中可以到达的最深处相同。

例如,在 qsort2() 中,main() 调用 gen_and_sort()gen_and_sort() 调用 qsort2()qsort2() 调用 swap()。 如果您在 swap() 中的断点处停止,则 where 命令会给您一个如下所示的报告

(gdb) where
#0  swap (i=3, j=7) at qsort2.c:134
#1  0x278c in qsort2 (l=0, u=9, strat=1) at
qsort2.c:121
#2  0x25a8 in gen_and_sort (numels=10, genstyle=0,
strat=1) at qsort2.c:90
#3  0x23a8 in main () at qsort2.c:46
(gdb)

up 命令将 gdb 的注意力指向 qsort2() 的堆栈帧,这意味着您现在可以检查 qsort2 的局部变量; 以前,它们超出了上下文。 另一个 up 将您带到 gen_and_sort() 的堆栈帧; 命令 down 将您移回 swap()。 如果您忘记了您在哪里,命令 frame 会总结当前堆栈帧

(gdb) frame
#1  0x278c in qsort2 (l=0, u=9, strat=1) at
qsort2.c:121
121                    swap(i,j);

在这种情况下,它显示我们正在查看 qsort2() 的堆栈帧,并且当前正在执行对函数 swap() 的调用。 这不足为奇,因为我们已经知道我们在 swap 中的断点处停止。

机器语言工具

gdb 提供了一些用于处理机器语言的特殊命令。 首先,info line 命令用于告诉您特定源代码行的目标代码从哪里开始和结束。 例如

(gdb) info line 121
Line 121 of "qsort2.c" starts at pc 0x277c and
ends at 0x278c.

然后,您可以使用 disassemble 命令来发现此行的机器代码

(gdb) disassemble 0x260c 0x261c
Dump of assembler code from 0x260c to 0x261c:
0x260c <qsort2>:        save  %sp, -120, %sp
0x2610 <qsort2+4>:      st  %i0, [ %fp + 0x44 ]
0x2614 <qsort2+8>:      st  %i1, [ %fp + 0x48 ]
0x2618 <qsort2+12>:     st  %i2, [ %fp + 0x4c ]
End of assembler dump.

命令 stepinexti 等效于 stepnext,但在机器语言指令级别而不是源语句级别工作。 stepi 命令执行下一条机器语言指令。 nexti 命令执行下一条指令,除非该指令调用函数,在这种情况下,nexti 执行整个函数。

内存检查命令 x(表示“examine”)打印内存内容。 它可以通过两种方式使用

(gdb) x/nfu addr
(gdb) x addr

第一种形式提供显式格式信息; 第二种形式接受默认值(通常是用于上一个 xprint 命令的格式——或者十六进制,如果之前没有命令)。 addr 是您要显示其内容的地址。

格式信息由 nfu 给出,它是三个项目的序列

  • n 是一个重复计数,指定要打印多少个数据项;

  • f 指定用于输出的格式;

  • u 指定数据单元的大小(例如,字节、字等)。

例如,让我们调查程序第 79 行中的 sprint 显示它是指向 struct tms 的指针

79          s = &start_time;
(gdb) print s
$1 = (struct tms *) 0xf7fffae8

进一步调查的简单方法是使用命令 print *s,它显示数据结构的各个字段。

(gdb) print *s
$2 = {tms_utime = 9, tms_stime = 14,
tms_cutime = 0, tms_cstime = 0}

为了论证,让我们使用 x 来检查此处的数据。 struct tms(在头文件 time.h 中定义)由四个 int 字段组成; 因此我们需要打印四个十进制字。 我们可以使用命令 x/4dw,从位置 s 开始执行此操作

(gdb) x/4dw s
0xf7fffae8 <_end+-138321592>:  9  14  0  0

从位置 s 开始的四个字是 9、14、0 和 0——这与 print 显示的内容一致。

信号

gdb 通常会捕获发送给它的大多数信号。 通过捕获信号,gdb 可以决定如何处理您正在运行的进程。 例如,按 CTRL-C 会向 gdb 发送中断信号,这通常会终止它。 但是您可能不想中断 gdb; 您真正想要中断的是 gdb 正在运行的程序。 因此,gdb 捕获信号并停止它正在运行的程序; 这使您可以进行一些调试。

命令 handle 控制信号处理。 它接受两个参数:信号名称,以及信号到达时应执行的操作。 例如,假设您要拦截信号 SIGPIPE,防止您正在调试的程序看到它。 但是,每当它到达时,您都希望程序停止,并且您希望收到一些通知。 为了实现这一点,请给出命令

(gdb) handle SIGPIPE stop print

请注意,信号名称始终是大写字母! 您可以使用信号编号代替信号名称。

C++ 程序

如果您使用 C++ 编写代码并使用 g++ 编译,您会发现 gdb 是一个非常棒的环境。 它完全理解该语言的语法以及类如何扩展 C 结构的概念。 让我们看一个简单的程序,看看 gdb 如何处理类和构造函数。 列表 1 包含 gdb 中生成的列表。

为了查看程序的运行情况,我们将在第 24 行的 entry 语句处设置一个断点。 当然,此声明会调用一个函数——entry 构造函数。

(gdb) b 24
Breakpoint 1 at 0x23e4: file ref.C, line 24.
(gdb) run
Starting program: /home/los/mikel/crossref/ref
Breakpoint 1, main (argc=1, argv=0xeffffd8c) at
ref.C:24
24           entry entry_1(text_1, strlen(text_1),
ref_1);

现在我们将进入函数。 我们通过 step 命令来执行此操作,就像在 C 中进入函数时一样。

(gdb) step
entry::entry (this=0xeffffcb8, text=0x2390
"Finding errors in C++ programs",
    length=30, ref=0x23b0 "errc++") at ref.C:14
14                e_text = new char(length+1);

gdb 已移动到 entry 构造函数的第一行,向我们显示调用该函数时使用的参数。 当我们返回到主程序时,我们可以像任何其他数据结构一样打印变量 entry_1

(gdb) print entry_1
$1 = {e_text = 0x6128 "Finding errors in C++
programs",
  e_reference = "errc++",
  '\e000' <repeats 73 times>}

因此,C++ 调试与 C 调试一样简单。

命令编辑

另一个有用的功能是编辑命令以纠正键入错误的能力。 gdb 提供了 Emacs 中可用的编辑命令的子集,使您可以在您键入的行上前后移动。 例如,考虑以下命令

(gdb) stop in gen_and_sort

如果这对您来说看起来不熟悉,那就不应该; 这是一个 dbx 命令。 我们实际上是想键入 break gen_and_sort。 为了修复这个问题,我们可以键入 ESC b 三次,以在 gen_and_sort 中向后移动三个单词(空格、下划线和其他标点符号定义了“单词”的含义)。 然后我们键入 ESC DEL 两次,以删除错误的命令 stop in。 最后,我们键入正确的命令 break,然后按 RETURN 执行它

(gdb) break gen_and_sort
Breakpoint 1 at 0x2544: file qsort2.c, line 79.
(gdb)

Emacs 有一种特殊模式,使其特别容易使用 gdb。 要启动它,请给出命令 ESC x gdb。 Emacs 会在 minibuffer 中提示您输入文件名

Run gdb (like this): gdb

添加可执行文件的名称并按 RETURN; 然后 Emacs 会启动一个特殊窗口来运行 gdb,您可以在其中给出所有常规 gdb 命令。 当您在断点处停止时,gdb 会自动创建一个窗口来显示您的源代码,并标记您停止的位置,如下所示

        struct tms end_time, *e;
        int begin, end;
=>      s = &start_time;
        e = &end_time;

标记 => 显示要执行的下一行。 每当 gdb 停止执行时,位置都会更新——即,在每次单步执行后,在每次 continue 后等等。 您可能永远不需要再次使用内置的 list 命令!

本文改编自 O'Reilly & Associates 出版的书籍“Programming with GNU Software”。

加载 Disqus 评论