Chinaunix首页 | 论坛 | 博客
  • 博客访问: 141921
  • 博文数量: 24
  • 博客积分: 2035
  • 博客等级: 大尉
  • 技术积分: 370
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-15 07:34
文章分类

全部博文(24)

文章存档

2008年(24)

我的朋友

分类: C/C++

2008-04-21 18:15:23

使用 GDB 调试 Linux 软件

GNU 调试器简介

转自:http://www.ibm.com/developerworks/cn/linux/sdk/gdb/index.html

级别: 初级

David SeagerCICS/390 开发部,IBM Hursley

2001 2 01

    Linux的大部分特色源自于shellGNU调试器,也称作gdbgdb可以让您查看程序的内部结构、打印变量值、设置断点,以及单步调试源代码。它是功能极其强大的工具,适用于修复程序代码中的问题。在本文中,David Seager将尝试说明gdb有多棒,多实用。

    开始调试之前,必须用程序中的调试信息编译要调试的程序。这样,gdb才能够调试所使用的变量、代码行和函数。如果要进行编译,请在gcc(或g++)下使用额外的'-g'选项来编译程序:

                gcc -g eg.c -o eg

    在shell中,可以使用'gdb'命令并指定程序名作为参数来运行gdb,例如'gdb eg';或者在 gdb中,可以使用file命令来装入要调试的程序,例如'file eg'。这两种方式都假设您是在包含程序的目录中执行命令。装入程序之后,可以用gdb命令'run'来启动程序。

    如果一切正常,程序将执行到结束,此时gdb将重新获得控制。但如果有错误将会怎么样?这种情况下,gdb会获得控制并中断程序,从而可以让您检查所有事物的状态,如果运气好的话,可以找出原因。为了引发这种情况,我们将使用一个示例程序:

#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 %d\n", value, div, total);


      return 0;
}


    这个程序将运行10 for 循环,使用"wib()"函数计算出累积值,最后打印出结果。

    在您喜欢的文本编辑器中输入这个程序(要保持相同的行距),保存为'eg1.c',使用'gcc -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 diff',可以相应看到"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会在遇到断点时中断执行。可以使用命令'break main'在进入 main()函数时设置断点,或者可以指定其它任何感兴趣的函数名来设置断点。然而,我们只希望在调用wib()函数之前中断执行。输入'list main'将打印从main()函数开始的源码清单,再次按回车将显示第21行上的 wib()函数调用。要在那一行上设置断点,只需输入'break 21'gdb将发出以下响应:

(gdb) break 21
Breakpoint 1 at 0x8048428: file eg1.c, line 21.


    以显示它已在我们请求的行上设置了1号断点。'run'命令将从头重新运行程序,直到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"相等时就会出现问题,因此输入'continue' 继续执行,直到下一次遇到1号断点。对于这次迭代,'info locals'显示了value=9div=7

    与其再次继续,还不如使用'next'命令单步调试程序,以查看"value""div"是如何改变的。gdb将响应:

(gdb) next
22 total += result;


    再按两次回车将显示加法和减法表达式:

(gdb)
23 div++;
(gdb)
24 value--;


    再按两次回车将显示第21行,wib()调用。'info locals'将显示目前"div""value",这就意味着将发生问题。如果有兴趣,可以使用'step'命令(与'next'形成对比,'next'将跳过函数调用)来继续执行wib()函数,以再次查看除法错误,然后使用'next'来计算 "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'),而且可以使用与断点相同的语法来启用、禁用和删除监视点

    在gdb下运行程序可以使俘获错误变得更容易,但在调试器外运行的程序通常会中止而只留下一个core文件。gdb可以装入core文件,并让您检查程序中止之前的状态。

    在gdb外运行示例程序eg1将会导致核心信息转储:

$ ./eg1
Floating point exception (core dumped)


    要使用core文件启动gdb,在shell中发出命令'gdb eg1 core''gdb eg1 -c core'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'命令将打印出所有程序变量的值,但这要进行很长时间,因为gdb将打印C库和程序代码中的变量。为了更容易地查明在调用wib()的函数中发生了什么情况,可以使用gdb的堆栈命令。

    程序调用堆栈是当前函数之前的所有已调用函数的列表(包括当前函数)。每个函数及其变量都被分配了一个最近调用的函数在0号帧中(“底部”帧)。要打印堆栈,发出命令'bt'('backtrace'[回溯]的缩写)

(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连接的进程标识替换core文件名就可以执行此操作。以下是一个执行循环并睡眠的 示例程序

示例代码

#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:78
#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,使用命令'break 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调试器手册。

参考资料

•    您可以参阅本文在developerWorks全球站点上的 英文原文.
•    GNU 调试器手册
•    调试会话示例的源代码。
•    连接示例的源代码。

关于作者

    David Seager是IBM的软件开发人员,他从事Linux和基于Web的应用工作已有两年时间了。
阅读(1357) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~