Linux应用程序设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.10 实训任务5 使用GDB调试程序

2.10.1 基本使用流程

来看一个短小的程序,由此一起熟悉GDB的使用流程。建议读者能够实际动手操作。

首先,打开Linux下的编辑器Source Insight(或者VI),编辑test.c如下代码。

#include <stdio.h>
int sum(int m);
int main()
{
    int i, n = 0;
    int ret;
    ret = sum(50);
    if (0 != ret)
    {
        printf("sum funciton error!\n");
        return -1;
    }
    for(i = 1; i <= 50; i++)
    {
        n += i;
    }
    printf("The sum of 1-50 is %d \n", n );
    return 0;
}
int sum(int m)
{
    int i, n = 0;
    for(i = 1; i <= m; i++)
    {
        n += i;
    }
    printf("The sum of 1-m is %d\n", n);
    return 0;
}

在保存退出后首先使用GCC对test.c进行编译,注意一定要加上选项“-g”,这样编译出的可执行程序中才包含调试信息,否则之后GDB无法载入该可执行文件对应的代码信息。

# gcc -g test.c -o test

虽然这段程序没有编译错误,但调试编译完全正确的程序可以更加了解GDB的使用流程。接下来就是启动GDB进行调试。注意,GDB进行调试的是可执行文件,而不是如“.c”的源代码,因此,需要先通过GCC编译生成可执行文件才能用GDB进行调试。

# gdb test
GNU Gdb Red Hat Linux (6.3.0.0-1.21rh)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or 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.
This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db
library "/lib/libthread_db.so.1".
(gdb)

可以看出,在GDB的启动画面中指出了GDB的版本号、使用的库文件等信息,接下来就进入了由“(gdb)”开头的命令行界面了。

1.查看文件

在GDB中输入“l”(list)就可以查看所载入的文件,如下所示:

(Gdb) l
1 #include <stdio.h>
2 int sum(int m);
3 int main()
4 {
5 int i,n=0;
6 sum(50);
7 for(i=1; i<=50; i++)
8 {
9 n += i;
10}
(Gdb) l
11 printf("The sum of 1~50 is %d \n", n );
12
13}
14 int sum(int m)
15 {
16 int i,n=0;
17 for(i=1; i<=m;i++)
18 n += i;
19 printf("The sum of 1~m is = %d\n", n);
20}

可以看出,GDB列出的源代码中明确地给出了对应的行号,这样就可以大大地方便代码的定位。

注意:在GDB的命令中都可使用缩略形式的命令,如“l”代表“list”,“b”代表“breakpoint”,“p”代表“print”等,读者也可使用“help”命令来查看帮助信息。

2.设置断点

设置断点是调试程序中是一个非常重要的手段,它可以使程序到一定位置暂停它的运行。因此,程序员在该位置可以方便地查看变量的值、堆栈等情况,从而找出代码的问题所在。在GDB中设置断点非常简单,只需在“b”后加入对应的行号即可(这是最常用的方式,另外还有其他方式设置断点),如下所示:

(Gdb) b 6
Breakpoint 1 at 0x804846d: file test.c, line 6.

要注意的是,在GDB中利用行号设置断点是指代码运行到对应行之前将其停止,如上例中,代码运行到第6行之前就暂停(并没有运行第6行)。

3.查看断点情况

在设置完断点之后,用户可以输入“info b”来查看设置断点的情况,在GDB中可以设置多个断点。

(Gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804846d in main at test.c:6

4.运行代码

接下来就可运行代码,GDB默认从首行开始运行代码,可输入“r”(run)即可(若想从程序中指定行开始运行,可在r后面加上行号)。

(Gdb) r
Starting program: /root/workplace/Gdb/test
Reading symbols from shared object read from target memory done.
Loaded system supplied DSO at 0x5fb000
Breakpoint 1, main () at test.c:6
6 sum(50);

可以看到,程序运行到断点处就停止了。

5.查看变量值

在程序停止运行之后,程序员所要做的工作是查看断点处的相关变量值。在GDB中只需输入“p+变量名”即可,如下所示:

(Gdb) p n
$1 = 0
(Gdb) p i
$2 = 134518440

在此处,为什么变量“i”的值为如此奇怪的一个数字呢?原因就在于程序是在断点设置的对应行之前停止的,那么在此时,并没把“i”的数值赋为零,而只是一个随机的数字。但变量“n”是在第4行赋值的,故在此时已经为零。

小技巧:GDB在显示变量值时都会在对应值之前加上“$N”标记,它是当前变量值的引用标记,所以以后若想再次引用此变量就可以直接写作“$N”,而无须写冗长的变量名。

6.单步运行

单步运行可以使用命令“n”(next)或“s”(step),它们之间的区别是:若有函数调用的时候,“s”会进入该函数而“n”不会进入该函数。因此,“s”就类似于VC等工具中的“step in”,“n”类似于VC等工具中的“step over”。它们的使用如下所示:

(Gdb) n
The sum of 1-m is 1275
7 for(i=1; i<=50; i++)
(Gdb) s
sum (m=50) at test.c:16
16 int i,n=0;

可见,使用“n”后,程序显示函数sum的运行结果并向下执行,而使用“s”后则进入到sum函数之中单步运行。

7.恢复程序运行

在查看完所需变量及堆栈情况后,就可以使用命令“c”(continue)恢复程序的正常运行了。这时,它会把剩余还未执行的程序执行完,并显示剩余程序中的执行结果。以下是之前使用“n”命令恢复执行后的结果:

(Gdb) c
Continuing.
The sum of 1-50 is :1275
Program exited with code 031.

可以看出,程序在运行完后退出,之后程序处于“停止状态”。

小知识:在GDB中,程序的运行状态有“运行”、“暂停”和“停止”3种,其中“暂停”状态为程序遇到断点或观察点之类的,程序暂时停止运行,而此时函数的地址、函数参数、函数内的局部变量都会被压入“栈”(Stack)中。故在这种状态下可以查看函数的变量值等各种属性。但在函数处于“停止”状态之后,“栈”就会自动撤销,也就无法查看各种信息了。

2.10.2 调试错误程序实验

1.实验目的

通过调试一个有问题的程序,使读者进一步熟练使用VI操作,而且熟练掌握GCC编译命令及GDB的调试命令,通过对有问题程序的跟踪调试,进一步提高发现问题和解决问题的能力。这是一个很小的程序,只有35行。

2.实验内容

(1)使用VI编辑器,将以下代码输入到名为greet.c的文件中。此代码的原意为输出倒序main函数中定义的字符串,但其结果显示没有输出。代码如下所示:

#include <stdio.h>
int display1(char *string);
int display2(char *string);
int main ()
{
   char string[] = "Embedded Linux";
   display1 (string);
   display2 (string);
}
int display1 (char *string)
{
   printf ("The original string is %s \n", string);
}
int display2 (char *string1)
{
   char *string2;
   int size,i;
   size = strlen (string1);
   string2 = (char *) malloc (size + 1);
   for (i = 0; i < size; i++)
   {
   string2[size - i] = string1[i];
   }
   string2[size+1] = ' ';
   printf("The string afterward is %s\n",string2);
}

(2)使用GCC编译这段代码,注意要加上“-g”选项方便之后的调试。

(3)运行生成的可执行文件,观察其运行结果。

(4)使用GDB调试程序,通过设置断点、单步跟踪,一步步找出错误所在。

(5)纠正错误,更改源程序并得到正确的结果。

3.实验步骤

(1)在工作目录上新建文件greet.c,并用VI启动:vi greet.c。

(2)在VI中输入以上代码。

(3)在VI中保存并退出:wq。

(4)用GCC进行编译:gcc -g greet.c -o greet。

(5)运行greet:./greet,输出为:

The original string is Embedded Linux
The string afterward is

可见,该程序没有能够倒序输出。

(6)启动GDB调试:gdb greet。

(7)查看源代码,使用命令“l”。

(8)在30行(for循环)处设置断点,使用命令“b 30”。

(9)在33行(printf函数)处设置断点,使用命令“b 33”。

(10)查看断点处设置情况,使用命令“info b”。

(11)运行代码,使用命令“r”。

(12)单步运行代码,使用命令“n”。

(13)查看暂停点变量值,使用命令“p string2[size - i]”。

(14)继续单步运行代码数次,并使用命令查看,发现string2[size-1]的值正确。

(15)继续程序的运行,使用命令“c”。

(16)程序在printf前停止运行,此时依次查看string2[0]、string2[1]……,可以发现string[0]并没有被正确赋值,而后面的复制都是正确的,这时,定位程序第31行,发现程序运行结果错误的原因在于“size-1”。由于i只能增到“size-1”,这样string2[0]就不能被赋值而保持NULL,故不能输出任何结果。

(17)退出GDB,使用命令q。

(18)重新编辑greet.c,把其中的“string2[size - i] = string1[i]”改为“string2[size – i - 1] =string1[i];”即可。

(19)使用GCC重新编译:gcc -g greet.c -o greet。

(20)查看运行结果:./greet

The original string is Embedded Linux
The string afterward is xuniL deddedbmE

这时,正确输出结果。

4.实验结果

将原来有错的程序经过GDB调试,找出问题所在,并修改源代码,输出正确的倒序显示字符串的结果。