编译
如果要进行编译,请在 gcc(或 g++)下使用额外的 -g
选项来编译程序: gcc -o eg -g eg.c
运行 gdb
可以使用 gdb 命令并指定程序名作为参数来运行 gdb,例如 gdb eg;或
者在 gdb 中,可以使用 file 命令来装入要调试的程序,例如 file eg。这两种方式都
假设您是在包含程序的目录中执行命令。装入程序之后,可以用 gdb 命令 run 来启动程
序。
调试会话示例
如果一切正常,程序将执行到结束,此时 gdb 将重新获得控制。但如果有错误将会怎么
样?这种情况下,gdb 会获得控制并中断程序,从而可以让您检查所有事物的状态,如
果运气好的话,可以找出原因。为了引发这种情况,我们将使用一个示例程序:
代码示例 eg1.c
#include
int wib(int no1, int no2)
{
int result, diff;
diff = no1 - no2;
result = no1 / diff;
return result;
}
int main(int argc, char *argv[])
{
int value, div, result, i, total;
value = 10;
div = 6;
total = 0;
for(i = 0; i < 10; i++)
{
result = wib(value, div);
total += result;
div++;
value--;
}
printf("%d wibed by %d equals %dn", value, div, total);
return 0;
}
这个程序将运行 10 次 for 循环,使用 wib() 函数计算出累积值,最后打印出结果。
在您喜欢的文本编辑器中输入这个程序(要保持相同的行距),保存为 eg1.c,使用 g
cc -g eg1.c -o eg1 进行编译,并用 gdb eg1 启动 gdb。使用 run 运行程序可能会产
生以下消息:
Program received signal SIGFPE, Arithmetic exception.
0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;
(gdb)
gdb 指出在程序第 7 行发生一个算术异常,通常它会打印这一行以及 wib() 函数的自
变量值。要查看第 7 行前后的源代码,请使用 list 命令,它通常会打印 10 行。再次
输入 list(或者按回车重复上一条命令)将列出程序的下 10 行。从 gdb 消息中可以
看出,第 7 行中的除法运算出了错,程序在这一行中将变量 "no1" 除以 "diff"。
要查看变量的值,使用 gdb print 命令并指定变量名。输入 print no1 和 print dif
f,可以相应看到 "no1" 和 "diff" 的值,结果如下:
(gdb) print no1
$5 = 8
(gdb) print diff
$2 = 0
gdb 指出 "no1" 等于 8,"diff" 等于 0。根据这些值和第 7 行中的语句,我们可以推
断出算术异常是由除数为 0 的除法运算造成的。清单显示了第 6 行计算的变量 "diff
",我们可以打印 "diff" 表达式(使用 print no1 - no2 命令),来重新估计这个变
量。gdb 告诉我们 wib 函数的这两个自变量都等于 8,于是我们要检查调用 wib() 函
数的 main() 函数,以查看这是在什么时候发生的。在允许程序自然终止的同时,我们
使用 continue 命令告诉 gdb 继续执行。
(gdb) continue
Continuing.
Program terminated with signal SIGFPE, Arithmetic exception.
The program no longer exists.
使用断点
为了查看在 main() 中发生了什么情况,可以在程序代码中的某一特定行或函数中设置
断点,这样 gdb 会在遇到断点时中断执行。可以使用命令reak main 在进入 main()
函数时设置断点,或者可以指定其它任何感兴趣的函数名来设置断点。然而,我们只希
望在调用 wib() 函数之前中断执行。输入 list main 将打印从 main() 函数开始的源
码清单,再次按回车将显示第 21 行上的 wib() 函数调用。要在那一行上设置断点,只
需输入reak 21。gdb 将发出以下响应:
(gdb) break 21
Breakpoint 1 at 0x8048428: file eg1.c, line 21.
以显示它已在我们请求的行上设置了 1 号断点。 un 命令将从头重新运行程序,直到
gdb 中断为止。发生这种情况时,gdb 会生成一条消息,指出它在哪个断点上中断,以
及程序运行到何处:
Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:21
21 result = wib(value, div);
发出 print value 和 print div 将会显示在第一次调用 wib() 时,变量分别等于 10
和 6,而 print i 将会显示 0。幸好,gdb 将显示所有局部变量的值,并使用 info
locals 命令保存大量输入信息。
从以上的调查中可以看出,当 "value" 和 "div" 相等时就会出现问题,因此输入 con
tinue 继续执行,直到下一次遇到 1 号断点。对于这次迭代,info locals 显示了 va
lue=9 和 div=7。
与其再次继续,还不如使用 next 命令单步调试程序,以查看 "value" 和 "div" 是如何
改变的。gdb 将响应:
(gdb) next
22 total += result;
再按两次回车将显示加法和减法表达式:
(gdb)
23 div++;
(gdb)
24 value--;
再按两次回车将显示第 21 行,wib() 调用。info locals 将显示目前 "div" 等于 "v
alue",这就意味着将发生问题。如果有兴趣,可以使用 step 命令(与 next 形成对比
, next 将跳过函数调用)来继续执行 wib() 函数,以再次查看除法错误,然后使用 ne
xt 来计算 "result"。
现在已完成了调试,可以使用 quit 命令退出 gdb。由于程序仍在运行,这个操作会终
止它,gdb 将提示您确认。
更多断点和观察点
由于我们想要知道在调用 wib() 函数之前 "value" 什么时候等于 "div",因此在上一
示例中我们在第 21 行中设置断点。我们必须继续执行两次程序才会发生这种情况,但
是只要在断点上设置一个条件就可以使 gdb 只在 "value" 与 "div" 真正相等时暂停。
要设置条件,可以在定义断点时指定 "break if "。将 eg1 再次装入 gdb,并输入:
(gdb) break 21 if value==div
Breakpoint 1 at 0x8048428: file eg1.c, line 21.
如果已经在第 21 行中设置了断点,如 1 号断点,则可以使用 condition 命令来代替
在断点上设置条件:
(gdb) condition 1 value==div
使用 run 运行 eg1.c 时,如果 "value" 等于 "div",gdb 将中断,从而避免了在它们
相等之前必须手工执行 continue。调试 C 程序时,断点条件可以是任何有效的 C 表达
式,一定要是程序所使用语言的任意有效表达式。条件中指定的变量必须在设置了断点
的行中,否则表达式就没有什么意义!
使用 condition 命令时,如果指定断点编号但又不指定表达式,可以将断点设置成无条
件断点,例如,condition 1 就将 1 号断点设置成无条件断点。
要查看当前定义了什么断点及其条件,请发出命令 info break:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048428 in main at eg1.c:21
stop only if value == div
breakpoint already hit 1 time
除了所有条件和已经遇到断点多少次之外,断点信息还在 Enb 列中指定了是否启用该断
点。可以使用命令 disable 、enable 或 delete 来禁用、启用和彻底删除断点,例如
disable 1 将阻止在 1 号断点处中断。
如果我们对 "value" 什么时候变得与 "div" 相等更感兴趣,那么可以使用另一种断点
,称作监视。当指定表达式的值改变时,监视点将中断程序执行,但必须在表达式中所
使用的变量在作用域中时设置监视点。要获取作用域中的 "value" 和 "div",可以在
main 函数上设置断点,然后运行程序,当遇到 main() 断点时设置监视点。重新启动
gdb,并装入 eg1,然后输入:
(gdb) break main
Breakpoint 1 at 0x8048402: file eg1.c, line 15.
(gdb) run
...
Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:15
15 value = 10;
要了解 "div" 何时更改,可以使用 watch div,但由于要在 "div" 等于 "value" 时中
断,那么应输入:
(gdb) watch div==value
Hardware watchpoint 2: div == value
如果继续执行,那么当表达式 "div==value" 的值从 0(假)变成 1(真)时,gdb 将
中断:
(gdb) continue
Continuing.
Hardware watchpoint 2: div == value
Old value = 0
New value = 1
main (argc=1, argv=0xbffff954) at eg1.c:19
19 for(i = 0; i < 10; i++)
info locals 命令将验证 "value" 是否确实等于 "div"(再次声明,是 8)。
info watch 命令将列出已定义的监视点和断点(此命令等价于 info break),而且可
以使用与断点相同的语法来启用、禁用和删除监视点。
core 文件
在 gdb 下运行程序可以使俘获错误变得更容易,但在调试器外运行的程序通常会中止而
只留下一个 core 文件。gdb 可以装入 core 文件,并让您检查程序中止之前的状态。
在 gdb 外运行示例程序 eg1 将会导致核心信息转储:
$ ./eg1
Floating point exception (core dumped)
要使用 core 文件启动 gdb,在 shell 中发出命令 gdb eg1 core 或 gdb eg1 -c cor
e。gdb 将装入 core 文件,eg1 的程序清单,显示程序是如何终止的,并显示非常类似
于我们刚才在 gdb 下运行程序时看到的消息:
...
Core was generated by `./eg1.
Program terminated with signal 8, Floating point exception.
...
#0 0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;
此时,可以发出 info locals、print、info args 和 list 命令来查看引起除数为零的
值。info variables 命令将打印出所有程序变量的值,但这要进行很长时间,因为 gd
b 将打印 C 库和程序代码中的变量。为了更容易地查明在调用 wib() 的函数中发生了
什么情况,可以使用 gdb 的堆栈命令。
堆栈跟踪
程序“调用堆栈”是当前函数之前的所有已调用函数的列表(包括当前函数)。每个函
数及其变量都被分配了一个“帧”,最近调用的函数在 0 号帧中(“底部”帧)。要打
印堆栈,发出命令tacktrace [回溯] 的缩写):
(gdb) bt
#0 0x80483ea in wib (no1=8, no2=8) at eg1.c:7
#1 0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21
此结果显示了在 main() 的第 21 行中调用了函数 wib()(只要使用 list 21 就能证实
这一点),而且 wib() 在 0 号帧中,main() 在 1 号帧中。由于 wib() 在 0 号帧中
,那么它就是执行程序时发生算术错误的函数。
实际上,发出 info locals 命令时,gdb 会打印出当前帧中的局部变量,缺省情况下,
这个帧中的函数就是被中断的函数(0 号帧)。可以使用命令 frame 打印当前帧。要查
看 main 函数(在 1 号帧中)中的变量,可以发出 frame 1 切换到 1 号帧,然后发出
info locals 命令:
(gdb) frame 1
#1 0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21
21 result = wib(value, div);
(gdb) info locals
value = 8
div = 8
result = 4
i = 2
total = 6
此信息显示了在第三次执行 "for" 循环时(i 等于 2)发生了错误,此时 "value" 等
于 "div"。
可以通过如上所示在 frame 命令中明确指定号码,或者使用 up 命令在堆栈中上移以及
down 命令在堆栈中下移来切换帧。要获取有关帧的进一步信息,如它的地址和程序语
言,可以使用命令 info frame。
gdb 堆栈命令可以在程序执行期间使用,也可以在 core 文件中使用,因此对于复杂的
程序,可以在程序运行时跟踪它是如何转到函数的。
连接到其它进程
除了调试 core 文件或程序之外,gdb 还可以连接到已经运行的进程(它的程序已经过
编译,并加入了调试信息),并中断该进程。只需用希望 gdb 连接的进程标识替换 co
re 文件名就可以执行此操作。以下是一个执行循环并睡眠的示例程序:
eg2 示例代码
#include
int main(int argc, char *argv[])
{
int i;
for(i = 0; i < 60; i++)
{
sleep(1);
}
return 0;
}
使用 gcc -g eg2.c -o eg2 编译该程序并使用 ./eg2 & 运行该程序。请留意在启动该
程序时在背景上打印的进程标识,在本例中是 1283:
./eg2 &
[3] 1283
启动 gdb 并指定进程标识,在我举的这个例子中是 gdb eg2 1283。gdb 会查找一个叫
作 "1283" 的 core 文件。如果没有找到,那么只要进程 1283 正在运行(在本例中可
能在 sleep() 中),gdb 就会连接并中断该进程:
...
/home/seager/gdb/1283: No such file or directory.
Attaching to program: /home/seager/gdb/eg2, Pid 1283
...
0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
(gdb)
此时,可以发出所有常用 gdb 命令。可以使用backtrace 来查看当前位置与 main()
的相对关系,以及 mian() 的帧号是什么,然后切换到 main() 所在的帧,查看已经在
"for" 循环中运行了多少次:
(gdb) backtrace
#0 0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
#1 0x400a877d in __sleep (seconds=1) at ../sysdeps/unix/sysv/linux/sleep.c:7
8
#2 0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
(gdb) frame 2
#2 0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
7 sleep(1);
(gdb) print i
$1 = 50
如果已经完成了对程序的修改,可以 detach 命令继续执行程序,或者 kill 命令杀死
进程。还可以首先使用 file eg2 装入文件,然后发出 attach 1283 命令连接到进程标
识 1283 下的 eg2。
其它小技巧
gdb 可以让您通过使用 shell 命令在不退出调试环境的情况下运行 shell 命令,调用
形式是 shell [commandline],这有助于在调试时更改源代码。
最后,在程序运行时,可以使用 set 命令修改变量的值。在 gdb 下再次运行 eg1,使
用命令reak 7 if diff==0 在第 7 行(将在此处计算结果)设置条件断点,然后运行
程序。当 gdb 中断执行时,可以将 "diff" 设置成非零值,使程序继续运行直至结束:
Breakpoint 1, wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;
(gdb) print diff
$1 = 0
(gdb) set diff=1
(gdb) continue
Continuing.
0 wibed by 16 equals 10
Program exited normally.
结束语
GNU 调试器是所有程序员工具库中的一个功能非常强大的工具。在本文中,我只介绍了
gdb 的一小部分功能。要了解更多知识,建议您阅读 GNU 调试器手册。
参考资料
GNU 调试器手册
调试会话示例的源代码。
连接示例的源代码。
关于作者
David Seager 是 IBM 的软件开发人员,他从事 Linux 和基于 Web 的应用工作已有两
年时间了。
本节将结合一个简单的例子,向大家演示这些常用指令的具体应用。这是一个冒泡排序算法的程序,这个例子的目的仅仅是演示,并不是实际调试。将下面的源程序存为bubble.c文件,并编译好。
#include <stdio.h>
#define MAX_RECORD_NUMBER 10
int record[MAX_RECORD_NUMBER] =
{12,76,48,62,94,17,37,52,69,32};
swap(int * x , int * y )
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int i,j;
for( i = 0 ; i < MAX_RECORD_NUMBER - 1; i++ )
{
for( j = MAX_RECORD_NUMBER - 1; j > i; j--)
if( record[j] < record[j-1] )
swap(&record[j],&record[j-1]);
}
for( i = 0; i < MAX_RECORD_NUMBER -1; i++)
printf("%d ",record[i]);
printf("
");
return 1;
}
记得在编译时用-g开关。如:gcc -g -o bubble bubble.c。你能在当前子目录下得到一个编译好的文件bubble。我们下面将以这个程序为例子向大家演示上面的指令在实际中的应用。
首先启动GDB,可以在启动的同时载入文件bubble,如:gdb bubble。也可以分两步进行,先启动GDB,执行gdb,进入GDB后,再执行file bubble。
这时可以用list指令列出源程序,list的使用比较简单,但其实在GDB中最不方便的就是看源程序,主要原因是因为GDB仅是一个文本方式的调试器,无法让你用鼠标和光标键来翻阅源程序,在这方面ddd等窗口程序有巨大的优势。
我们先来查看一下当前源程序的信息,如下:
(gdb) info source
Current source file is bubble.c
Compilation directory is /root/sample/
Located in /root/sample/bubble.c
Contains 32 lines.
Source language is c.
Compiled with stabs debugging format.
我们可以知道程序名,目录,文件大小,语言等信息。
下面我们来设置断点,我们想在函数swap出设置一个断点:
(gdb) br swap
Breakpoint 1 at 0x80483d6: file bubble.c, line 11.
br是break的简写。上面的一行是GDB告诉我们这个断点的信息,我们可以知道这个断点的断点号是1,地址是0x80483d6,它在文件bubble.c的11行。
我们再在一个行号上设一个断点,
(gdb) br 23
Breakpoint 2 at 0x804844a: file bubble.c, line 23.
我们已经设了两个断点,许多时候你会想查看一下断点的信息和状态,因此你会用到你最常使用的info指令,info br。
(gdb) info br
Num Type Disp Enb Address What
1 breakpoint keep y 0x080483d6 in swap at bubble.c:11
2 breakpoint keep y 0x0804844a in main at bubble.c:23
我 用这条指令的大多数原因是想查看一下某个断点的断点号,就是第一列的数值。有时也会看一下断点的状态是enable还是disable。以上的两个断点都 是y,也就是都处于enable状态。type列显示breakpoint,是因为info br指令同时也会显示watch的信息,因此用type来识别是断点breakpoint还是检查点watch。
如果你知道断点号,想删除断点很简单,例如想删除断点2,执行del 2就行了。
在程序中,断点2本来设在循环中,那样程序会频烦断下,这也许不是我们希望的。也许我们仅想在某个条件下让它断下,如想当j=5时。
(gdb) br 23 if j==5
Breakpoint 3 at 0x804844a: file bubble.c, line 23.
(gdb) info br
Num Type Disp Enb Address What
1 breakpoint keep y 0x080483d6 in swap at bubble.c:11
3 breakpoint keep y 0x0804844a in main at bubble.c:23
stop only if j == 5
注意现在的断点信息,虽然断点2被删除了,但新设的断点号没有使用2号,而是使用了3号。新设的断点是个条件断点,这从"stop only if j == 5"可以清楚的看出。
现在执行程序,输入run指令。
(gdb) run
Starting program: /root/sample/bubble
Breakpoint 1, swap (x=0x80495a4, y=0x80495a0) at bubble.c:11
11 temp = *x;
程序已经在断点1停了下来。当断点停下时,我们经常需要查看变量值。如查看x值。
(gdb) p x
$1 = (int *) 0x80495a4
GDB告诉我们x是一个指向整数的指针,指针值是0x80495a4。如果想查看指针指向的值。执行:
(gdb) p *x
$2 = 32
(gdb) p *y
$3 = 69
单步执行
(gdb) n
12 *x = *y;
查看变量temp值
(gdb) p temp
$4 = 32
(gdb) n
13 *y = temp;
(gdb) p *x
$5 = 69
现在删除断点1
(gdb) del 1
继续执行
(gdb) cont
Continuing.
Breakpoint 3, main () at bubble.c:23
23 swap(&record[j],&record[j-1]);
程序在断点3停下,记得断点3是个条件断点。要验证很简单,查看一下变量j的值是不是5。
(gdb) p j
$6 = 5
我们可以查看一下全局变量record的值,
(gdb) p record
$7 = {12, 76, 48, 62, 94, 17, 32, 37, 52, 69}
也可以查看一下变量record的地址,
(gdb) p &record
$8 = (int (*)[10]) 0x8049580
知道地址时,也可以x指令查看内存值。
(gdb) x/4uw 0x8049580
0x8049580 : 12 76 48 62
上面的指令查看4个4字节数,以整数方式显示。可以看到这与reocrd值是相附的。
(gdb) x/4bb record
0x8049580 : 12 0 0 0
显示4个单字节数,以字节当时显示。上面的4个字节值正好是record数组第一个整数值,因为整数是4字节,而且intel机器的数值是低字节在前。
改变变量值也很简单,如果想将reocrd数组第一个值该为1,
(gdb) set record[0]=1
看一下值是否改变了。
(gdb) p record
$10 = {1, 76, 48, 62, 94, 17, 32, 37, 52, 69}
第一个值以改成了1。
以上简单地介绍了一些常用的GDB指令,由于篇幅所限,我们无法涉及GDB所有指令及GDB其它许多功能,读者应当自己在实践中不断地学习。Linux系统中会有详细的GDB的资料,你可以用info gdb来查阅这些资料。