天天看点

使用 GNU 的 GDB调试器,内存布局和栈

我们会学些什么?

       为了更高效的学习使用GDB,你必须了解帧,通常也成为栈帧,因为帧构成了栈。为了学习栈,我们需要了解可执行文件的内存布局。这里的讨论主要都是理论上的,但是为了使学习比较有趣,在本章结束之前我们将通过GDB来展现栈和栈帧的例子。

       本章学习的东西似乎相当的理论化,但是对于达到以下目的来说却是非常有用的:

1. 理解栈对于使用GDB调试器是绝对有必要的

2. 了解一个进程的内存布局有助于我们理解什么是段错误和为什么会发生段错误(或者为什么有时候该发生段错误的时候却没有发生)。简略的说,段错误是一个程序崩溃最普通和最直接的原因。

3. 拥有程序内存空间的只是通常使得我们能够定位出隐藏得很好的程序错误,而不用使用print之类的打印语句,有时候甚至是编译器或者GDB的错误也能定位。下一部分是由我们的一个朋友Mark Kim写的,我们将能看到一些Sherlock Holmes式的侦探式的分析。Mark在一个很长的代码当中遇到了一个很不容易发现的问题。他运用所了解的关于程序内存空间的知识,只用了5到10分钟来看程序,就解决的问题。真是非常厉害!

多的就不说了,我们先来看看程序的内存布局。

虚拟内存:

        当创建了一个进程后,内核会为它分配一块物理内存,这块物理内存可以在任何位置。然而,虚拟内存很神奇,使得一个进程认为自己独占了整个计算机内存空间。你也许听说过虚拟内存,那是在在内存被耗尽后,使用硬盘来提供内存空间的时候。这也叫做虚拟内存,但是这与我们这里谈到的虚拟内存无关。我们主要关注虚拟内存的以下几点:

  1. 每个进程分配的物理内存我们称为进程的虚拟内存空间。
  2. 进程不知道物理内存的细节(比如:物理内存的位置)。所有进程只知道内存块的大小和内存块的起始地址是地址0。
  3. 每个进程都不用知道其他进程的虚拟内存。
  4. 即使一个进程知道其他虚拟内存块,自己也会去访问那块内存块。

当进程想要读写内存的时候,该进程的操作请求必须从虚拟内存地址转换为物理内存地址。同样,当内核需要访问一个进程的虚拟内存的时候,内核必须把物理地址转换为虚拟地址。这个过程主要涉及两个问题:

  1. 计算机经常访问内存,使得地址访问工作非常普遍;因此必须很快完成。
  2. 操作系统如何能够保证一个进程不会侵犯到其他进程的虚拟内存。

这两个问题的答案都在于操作系统自己并不会亲自管理虚拟内存;操作系统从CPU那里获得帮助。很多CPU都拥有一个被称为内存管理单元(MMU)的设备。MMU和操作系统一起负责管理——虚拟内存,虚拟内存和物理内存之间的转换,授权每个进程可以访问的内存区域,授权在虚拟内存的段上的读写操作。

Linux只能安装在拥有MMU的计算机上(因此不能安装在x286上)。然而,在1998年,Linux被安装在了MC68000上,MC68000并没有MMU。这使得Linux能够走向嵌入式领域,比如Linux可以安装到Palm Pilot上。

练习

  1. 阅读维基百科上关于MMU的简介
  2. 选作:如果想了解更多有关VM的知识,请看这里link。这里有很多内容,不用一定要知道的。

内存布局

       这就是虚拟内存工作的方式。 在大多数情况下,每个进程的虚拟内存空间的布局都是类似并且是可预知的:

使用 GNU 的 GDB调试器,内存布局和栈

代码段:代码段包含了实际可执行代码。代码段通常是可共用的,因此多个程序可以共享代码段,这样可以降低内存消耗。代码段通常是只读的,因此一个程序不能修改其中的指令。

初始化了的数据段:这个段包含了由程序员初始化的全局变量

未初始化的数据段:也称为“bss” 段,这是以前编译器使用过的一个操作符。这个段包含了未初始化的全局变量。这个段中的所有的变量在程序执行之前都被初始化为0或者NULL 指针。

栈:栈是一系列栈帧的集合,将会在下一节中讲到。当需要增加一个帧(一个函数调用的结果),栈就会向下增长。

堆:大部分动态分配内存,无论是通过C语言的malloc分配还是通过C++的new分配,都是从堆上分配的。C语言库也是从堆上获得动态内存的。因为有更多的内存在运行时需要被分配,堆是向上增长的。

一个.o文件或者可执行文件,你能够确定其中每个段的大小(我们并不是在谈论内存布局,我们在讲的是一个磁盘文件最终会进入到内存中)。 看下面的hello_world-1.c 和Makefile:

// hello_world-1.c

#include <stdio.h>

int main(void)

{

   printf("hello world\n");

   return 0;

}

通过下面命令进行编译和链接:

$ gcc –W –Wall –c hello_world-1.c

$ gcc –o hello_world-1 helloworld-1.o

可以通过size命令来查看每个段的大小:

$ size hello_world-1 hello_world-1.o

text   data   bss    dec   hex   filename

 916    256     4   1176   498   hello_world-1

  48      0     0     48    30   hello_world-1.o

data由初始化和未初始化的段构成。dec 和 hex 分别以10进制和16进制表示文件大小

也可以通过 “objdum -h”和 “objdum -x”来查看.o文件各段的大小:

   $ objdump -h hello_world-1.o

   hello_world-1.o:     file format elf32-i386

   Sections:

   Idx Name          Size      VMA       LMA       File off  Algn

     0 .text         00000023  00000000  00000000  00000034  2**2

                     CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

     1 .data         00000000  00000000  00000000  00000058  2**2

                     CONTENTS, ALLOC, LOAD, DATA

     2 .bss          00000000  00000000  00000000  00000058  2**2

                     ALLOC

     3 .rodata       0000000d  00000000  00000000  00000058  2**0

                     CONTENTS, ALLOC, LOAD, READONLY, DATA

     4 .note.GNU-stack 00000000  00000000  00000000  00000065  2**0

                     CONTENTS, READONLY

     5 .comment      0000001b  00000000  00000000  00000065  2**0

                     CONTENTS, READONLY

练习:

  1. size命令查看hello_word或者 hello_wordl.o并没有列出栈或者堆所在的段,你怎么看?
  2. 在hello_world-1.c中没有全局变量,请解释为什么执行size命令的结果显示data和bss段对于.o文件来说长度为0,对于可执行文件来说长度不为0。
  3. 使用size和objdump 查看代码段的大小不同。你能猜猜其中的差别来自哪里吗?提示:这个差异有多大?看看源代码中任何这个长度的东西。
  4. 选作:阅读.o文件的格式link。

栈帧和栈

        刚刚了解了一个进程的内存布局。内存布局的这样一个段叫做栈,这是一系列栈帧的集合。每个栈帧代表了一个函数调用。当一个函数被调用后,栈帧的数量就会增加,栈的大小增长。同样,当函数调用返回,栈帧的数量会减少,栈的大小会收缩。在这一部分,我们将学习什么是栈帧。更加详细的解释可以参见这里here,但我们还是有针对性的讨论。

       一个程序是由相互调用的一个或多个函数构成。每次函数调用都会分配一定区域的内存块,该内存被叫做函数调用的栈帧。这块内存区域保存了一些重要的信息,比如:

  1. 新近调用的函数的所有本地变量的存储空间
  2. 函数调用结束后的返回地址
  3. 被调用函数的变量和参数

     每个函数调用都会获得自己的栈帧。总的来说,所有栈帧构成了栈调用。下一个例子我们将使用hello_world-2.c。

1   #include <stdio.h>

2   void first_function(void);

3   void second_function(int);

4  

5   int main(void)

6   {

7      printf("hello world\n");

8      first_function();

9      printf("goodbye goodbye\n");

10 

11     return 0;

12  }

13 

14 

15  void first_function(void)

16  {

17     int imidate = 3;

18     char broiled = 'c';

19     void *where_prohibited = NULL;

20 

21     second_function(imidate);

22     imidate = 10;

23  }

24 

25 

26  void second_function(int a)

27  {

28     int b = a;

29  }

当程序开始运行时,只有一个属于main()的栈帧。由于main()函数没有局部变量,

没有参数,不会返回到其他函数,我们将不关心它的栈帧。下面是main()函数在调用first_function的之前的栈:

使用 GNU 的 GDB调试器,内存布局和栈

当调用第一个函数first_function(),未使用的栈内存被用来为first_function创建一个栈帧。栈帧保存了:一个 int ,一个 char,一个 void *,和返回语句。下面是在调用second_function之前的栈:

使用 GNU 的 GDB调试器,内存布局和栈

当调用second_function之后,形成了second_function的栈帧。该栈帧包含了:一个 int 型的数据,一个second_function 内的当前执行地址。当second_function返回后的栈:

使用 GNU 的 GDB调试器,内存布局和栈

当second_function返回后,它的栈帧决定了返回的位置(first_function的第22行),释放帧返回到栈。下面是second_function返回之后的栈:

使用 GNU 的 GDB调试器,内存布局和栈

当first_function返回后,其帧决定了返回的位置(main()的第九行),释放帧返回到栈。下面是first_function返回后的栈:

使用 GNU 的 GDB调试器,内存布局和栈

练习:

  1. 一个拥有5个函数调用的程序,栈上会有多少帧?
  2. 我们注意到栈线性向下增长,当一个函数返回后,栈上的最后一帧被释放并返回到没有使用过的内存。对于栈中间的某一个帧来说有没有可能返回到未使用的内存?如果可以,对于正在执行的程序来说意味着什么?
  3. 一个goto语句能造成栈中间的帧被释放吗?答案是不会,为什么呢?
  4. 一个longjmp()语句会造成栈中的帧被释放吗?

符号表

       一个符号就是一个变量或者一个函数。符号表如你所想:就是在可执行文件中的一个包含变量和函数的表。 正常情况下符号表只包含符号的地址,因为计算机不使用我们给变量或者函数起的名字。

       为了让GDB对我们来说有用,有必要能够通过变量或者函数名来引用变量,而不是使用它们的地址。人类使用的是诸如“main()”或者“i”。 计算机使用如0x804b64d 或0xbffff784 之类的地址。因为这种不同,我们在编译代码的时候加入“调试信息”,告诉GDB两件事:

  1. 如何将一个符号地址和源码中的名字关联起来。
  2. 如何将机器码的地址和源代码中的某一行关联起来。

拥有这些额外调试信息的符号表被叫做加强了的符号表。由于gcc和GDB运行在很多不同的平台上,有很多不同的调试信息格式:

·stabs:DBX在大部分BSD系统上使用的格式

·coff:SDB在大部分在System V发行版4 之前的System V系统上使用的格式

·xcoff:DBX在 IBM RS/6000 系统上使用的格式

·dwarf: SDB在多数System V 发行版4 系统上使用的格式

·dwarf2:DBX在IRIX 6 上使用的格式

·vms:DEBUG在VMS 系统上使用的格式

       对于调试格式,GDB能够解析这些格式加强的变量,使得GDB能够使用GNU的这些扩展。使用GNU加强后的调试格式,调试一个可执行文件;加上一些非GDB的东西,可以调试任何正确或者使调试器崩溃的程序。

       不要被这些格式所吓到了,在下一节中,我将会展示GDB会自动选择最适合你的格式。对于0.0001的需要使用其他格式的你,你也有足够的知识来做出选择。

为调试准备一个可执行文件

       如果你打算调试一个可执行文件,一个可执行文件的核心部分,或者是一个运行中的进程,你必须在编译这个可执行程序的时候使用增强的符号表。为了产生一个增强型的符号表,我们需要使用gcc 的‘-g’选项:

       gcc –g –o filename filename.c

       如前面讨论过的,有很多不同的格式。‘-g’选项实际的意义是在你的系统中产生本地格式。

       作为‘-g’选项的一个可替换选项,我们可以使用gcc的‘-ggdb’选项:

       gcc –ggdb –o filename  filename.c

这将会以可用的最简明的格式产生调试信息,包括之前讨论过的GNU加强类型的变量。我觉得下面的选项是大多数时候你会使用的:

       你也可以给‘-g’、‘-ggdb’和其他所有的调试选项增加一个数值参数。1表示最少信息,3 表示最多信息。 不添加该数值参数的话,默认的调试级别为2。通过使用‘-g3’选项,甚至可以查看预编译宏,这相当的有用。我建议你使用‘-ggdb3’选项来生成加强型的符号表。

       被编译进可执行文件的调试信息不会被读入到内存中,除非GDB将其加载到内存。这意味着拥有调试信息的可执行文件并不会比没有调试信息的可行程序运行得慢(这是一个普遍的误解)。同样,可执行文件的加载时间基本上也是相同的,除非你使用GDB来运行这些可调式的可执行程序。

       最后一个观点。对于拥有一个加强型的符号表的可执行程序来说,进行编译优化是完全可以的,例如:gcc –g -09 try1.c 。事实上,GDB是少数几个对调试优化后的可执行文件表现不错的符号调试器。然而,在调试一个可执行文件的时候你最好关闭优化选项,因为有时候你会让GDB犯糊涂。变量会因为优化而消失,函数可能会变成 ‘inline’类型的,更多的情况还可能发生,这些情况都可能(但也不一定)会使GDB犯晕。为了安全起见,调试程序的时候最后关闭优化选项。

练习:

  1. 运行“strip – only-keep-debug try1”。查看try1的文件大小。运行“strip –strip-debug try1”,查看文件的大小。 运行“strip –strip-all try1”,查看文件的大小。你能想象结果吗?如果不能,对你的惩罚是阅读 “man strip”,做一些刺激的阅读。
  2. 在前面的练习中,你从try1去除了所有不必要的符号。重新运行重新确保能够运行。运行“strip  --remove-section=.text try1”,查看文件长度。尝试运行try1。你觉得会发生什么?
  3. 关于符号表的阅读link
  4. 选作:阅读关于COFF 目标文件的格式link

使用GDB深入学习栈

       我们回过头来再看栈,这次使用GDB。由于还不知道断点,你可能对此一无所知,但这是相当直观的。编译并运行try1.c

1  #include<stdio.h>

2  static void display(int i, int *ptr);

4  int main(void) {

5     int x = 5;

6     int *xptr = &x;

7     printf("In main():\n");

8     printf("   x is %d and is stored at %p.\n", x, &x);

9     printf("   xptr points to %p which holds %d.\n", xptr, *xptr);

10    display(x, xptr);

11    return 0;

12 }

13

14  void display(int z, int *zptr) {

15        printf("In display():\n");

16     printf("   z is %d and is stored at %p.\n", z, &z);

17     printf("   zptr points to %p which holds %d.\n", zptr, *zptr);

18 }

    在接着往下学习之前,先确保你能理解当前的输出。我的运行结果为:

$ ./try1

In main():

   x is 5 and is stored at 0xbffff948.

   xptr points to 0xbffff948 which holds 5.

In display():

   z is 5 and is stored at 0xbffff924.

   zptr points to 0xbffff948 which holds 5.

你可以使用GDB加可执行文件名的方式来开始对一个可执行文件的调试。使用“try1”开始调试,你会看到一个冗长的提示:

$ gdb try1

GNU gdb 6.1-debian

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.

(gdb)

“(gdb)”表示GDB已经启动,等待我们输入命令。此时程序并未开始运行,输入“run”开始运行程序。这种方式在GDB内部运行程序:

   (gdb) run

   Starting program: try1

   In main():

      x is 5 and is stored at 0xbffffb34.

      xptr points to 0xbffffb34 which holds 5.

   In display():

      z is 5 and is stored at 0xbffffb10.

      zptr points to 0xbffffb34 which holds 5.

   Program exited normally.

   (gdb)

现在,程序已经运行起来了。是一个不错的开始,但坦率的讲,却有些过于普通。我们也可以自己运行该程序(不用使用GDB)。但有点我们不能做的就是在程序运行的过程中不能使程序暂停运行,然后看看栈的当前情况。接下来我们就将这样做。

通过使用断点GDB可以暂停执行程序。后面我们会讲到断点,此时,你需要知道当你使用“break 5”的时候,你的程序会暂停在第五行。你也许会问:程序执行过第五行了吗(是不是停在了第五行和第六行之间)?或者程序没有执行第五行(停在第四行和第五行之间)?答案是第五行并未执行。记住以下原则:

  1. “break 5”意味着停在第五行
  2. 这意味GDB运行的程序暂停在第四行和第五行之间。第四行执行了,第五行没有执行。

在第十行设置一个断点,重新运行程序:

   (gdb) break 10

   Breakpoint 1 at 0x8048445: file try1.c, line 10.

   (gdb) run

   Starting program: try1

   In main():

      x is 5 and is stored at 0xbffffb34.

      xptr holds 0xbffffb34 and points to 5.

   Breakpoint 1, main () at try1.c:10

   10         display(x, xptr);

我们在try1.c的第十行设置了断点。GDB告诉我们第十行相关的内存地址为“0x8048445”。我们重新运行程序得到前两行输出。我们此时在main() 函数中,在第十行之前。我们可以使用“backtrace”命令来查看栈:

   (gdb) backtrace

   #0  main () at try1.c:10

   (gdb)

栈上有一帧,编号0,属于main() 函数。 如果执行下一行,程序将进入display() 函数。 根据前面的章节,你应该知道栈上会发生什么:另外一帧将会加入到栈上。我们实际来看一下。你可以通过使用“step”命令来执行下一行:

      (gdb) step

   display (z=5, zptr=0xbffffb34) at try1.c:15

   15              printf("In display():\n");

   (gdb)

再次来看此时的栈,确信你理解所看到的一切:

   (gdb) backtrace

   #0  display (z=5, zptr=0xbffffb34) at try1.c:15

   #1  0x08048455 in main () at try1.c:10

需要注意的地方:

·此时我们有两个帧,帧1属于main() 函数,帧 0 属于display()函数

·每一帧列出了函数的参数。我们可以看到main() 函数没有参数,但display()函数有参数。

·每一帧列出了在该帧内当前被执行的行号。回过头来看源代码确保你理解在执行“backtrace” 命令时程序执行到的行。

·个人认为,帧号的确定方式有点模糊。我更希望main()函数仍然是第0帧,对于其它的帧,分配更大的帧号。但这却是与栈是向下增长的观点是保持一致的。记住拥有最小帧号的帧属于最近调用的函数。

执行下两行代码:

      (gdb) step

   In display():

   16         printf("   z is %d and is stored at %p.\n", z, &z);

   (gdb) step

      z is 5 and is stored at 0xbffffb10.

   17         printf("   zptr holds %p and points to %d.\n", zptr, *zptr);

该帧存储了函数的局部变量。GDB总是在与当前正在执行的函数相关的帧上下文中运行,除非你另行通知。此时在display() 函数中执行, GDB的上下文是帧0。 我们可以询问GDB此时的帧上下文,使用“frame”命令:

   (gdb) frame

   #0  display (z=5, zptr=0xbffffb34) at try1.c:17

   17         printf("   zptr holds %p and points to %d.\n", zptr, *zptr);

我之前并没有解释“上下文”是什么意思,现在来解释。因为此时的帧上下文是 第0帧,我们能够访问帧0 中的所有局部变量。换句话说,我们不能访问其他帧中的局部变量。我们来看看这一点。使用“print”命令,能够打印当前帧中的变量值。变量“z”和“zptr”在display()函数中,GDB当前运行在display()的帧中,我们应该能够打印它的局部变量:

      (gdb) print z

   $1 = 5

   (gdb) print zptr

   $2 = (int *) 0xbffffb34

此时我们访问不了其他帧中的局部变量。试试查看main()函数中的局部变量,也就是帧1的局部变量:

      (gdb) print x

   No symbol "x" in current context.

   (gdb) print xptr

   No symbol "xptr" in current context.

神奇的是,我们可以使用“frame”命令加上帧号告诉GDB从帧0跳转到帧1。使得我们能够访问帧1的变量。如你所想,当进行了帧转换之后,我们就不能访问帧0 的变量了。接着:

      (gdb) frame 1                           <--- switch to frame 1

   #1  0x08048455 in main () at try1.c:10

   10         display(x, xptr);

   (gdb) print x

   $5 = 5                                  <--- we have access to variables in frame 1

   (gdb) print xptr

   $6 = (int *) 0xbffffb34                 <--- we have access to variables in frame 1

   (gdb) print z

   No symbol "z" in current context.       <--- we don't have access to variables in frame 0

   (gdb) print zptr

   No symbol "zptr" in current context.    <--- we don't have access to variables in frame 0

顺便说一句,查看GDB的程序输出是最困难的一件事情:

   x is 5 and is stored at 0xbffffb34.

   xptr holds 0xbffffb34 and points to 5.

与GDB的输出混在一起:

   Starting program: try1

   In main():

   ...

      Breakpoint 1, main () at try1.c:10

   10         display(x, xptr);

与你输入到GDB的命令混在一起:

      (gdb) run

与你输入到程序的数据混在一起()。这会产生误解,但你使用GDB越多,你会越熟悉使用它。当程序执行结束操作的时候变得比较棘手(例如: ncurses 或 svga库),但还是有解决办法。

练习:

  1. 继续刚才的例子,转回display()函数的帧中,确认你访问的是display()函数中的局部变量而不是main()函数帧中。
  2. 找出推出GDB的方法。Control –d 可以退出,但是我希望你自己猜猜退出命令。
  3. GDB也有帮助特性。如果你输入“help foo”,GDB捡回打印出对命令“foo”的描述。输入GDB(不加任何参数),然后阅读所有本章讲到过的命令。
  4. 再次调试try1,在display()函数的各个位置设置断点,然后运行程序。找出如何

后记:

  对于GDB调试程序之前就有接触,但是比较皮毛,通过学习“程序员的自我修养”对栈帧的知识有了更深入的了解,在内网上看见有人介绍这篇文章,于是感觉很对味,就学习了。这是我翻译的第一篇文章,自己看的时候半小时之内就看完了,昨天晚上打球回来开始翻译,还以为会很快搞定,想不到直到今天才翻译完,word文档都有12页,因此博文分成了两部分。其实这篇文章对GDB调试讲述的主要是原理,对于实用方面来说并不是很完整,接下来还会有学习GDB的相关博文。

GDB基本调试:

一:列文件清单

1. List

(gdb) list line1,line2

二:执行程序

要想运行准备调试的程序,可使用run命令,在它后面可以跟随发给该程序的任何参数,包括标准输入和标准输出说明符(<和>)和外壳通配符(*、?、[、])在内。

如果你使用不带参数的run命令,gdb就再次使用你给予前一条run命令的参数,这是很有用的。

利用set args 命令就可以修改发送给程序的参数,而使用show args 命令就可以查看其缺省参数的列表。

(gdb)set args –b –x

(gdb) show args

backtrace命令为堆栈提供向后跟踪功能。

Backtrace 命令产生一张列表,包含着从最近的过程开始的所以有效过程和调用这些过程的参数。

三:显示数据

利用print 命令可以检查各个变量的值。

(gdb) print p (p为变量名)

whatis 命令可以显示某个变量的类型

(gdb) whatis p

type = int *

print 是gdb的一个功能很强的命令,利用它可以显示被调试的语言中任何有效的表达式。表达式除了包含你程序中的变量外,还可以包含以下内容:

对程序中函数的调用

(gdb) print find_entry(1,0)

 数据结构和其他复杂对象

(gdb) print *table_start

$8={e=reference=’\000’,location=0x0,next=0x0}

值的历史成分

(gdb)print $1 ($1为历史记录变量,在以后可以直接引用 $1 的值)

人为数组

人为数组提供了一种去显示存储器块(数组节或动态分配的存储区)内容的方法。早期的调试程序没有很好的方法将任意的指针换成一个数组。就像对待参数一样,让我们查看内存中在变量h后面的10个整数,一个动态数组的语法如下所示:

[email protected]

因此,要想显示在h后面的10个元素,可以使用[email protected]:

(gdb)print [email protected]

$13=(-1,345,23,-234,0,0,0,98,345,10)

四:断点(breakpoint)

break命令(可以简写为b)可以用来在调试的程序中设置断点,该命令有如下四种形式:

 break line-number 使程序恰好在执行给定行之前停止。

 break function-name 使程序恰好在进入指定的函数之前停止。

break line-or-function if condition 如果condition(条件)是真,程序到达指定行或函数时停止。

break routine-name 在指定例程的入口处设置断点

如果该程序是由很多原文件构成的,你可以在各个原文件中设置断点,而不是在当前的原文件中设置断点,其方法如下:

(gdb) break filename:line-number

(gdb) break filename:function-name

要想设置一个条件断点,可以利用break if命令,如下所示:

(gdb) break line-or-function if expr

例:

(gdb) break 46 if testsize==100

从断点继续运行:countinue 命令

五.断点的管理

1. 显示当前gdb的断点信息:

(gdb) info break

他会以如下的形式显示所有的断点信息:

Num Type Disp Enb Address What

1 breakpoint keep y 0x000028bc in init_random at qsort2.c:155

2 breakpoint keep y 0x0000291c in init_organ at qsort2.c:168

(gdb)

2.删除指定的某个断点:

(gdb) delete breakpoint 1

该命令将会删除编号为1的断点,如果不带编号参数,将删除所有的断点

(gdb) delete breakpoint

也可以使用:clear+断点的行号

3.禁止使用某个断点

(gdb) disable breakpoint 1

该命令将禁止断点 1,同时断点信息的 (Enb)域将变为 n

4.允许使用某个断点

(gdb) enable breakpoint 1

该命令将允许断点 1,同时断点信息的 (Enb)域将变为 y

5.清除原文件中某一代码行上的所有断点

(gdb)clean number

注:number 为原文件的某个代码行的行号

六.变量的检查和赋值

 whatis:识别数组或变量的类型

 ptype:比whatis的功能更强,他可以提供一个结构的定义

 set variable:将值赋予变量

 print 除了显示一个变量的值外,还可以用来赋值

七.单步执行

 next

不进入的单步执行

 step

进入的单步执行

如果已经进入了某函数,而想退出该函数返回到它的调用函数中,可使用命令finish

八.函数的调用

 call name 调用和执行一个函数

(gdb) call gen_and_sork( 1234,1,0 )

(gdb) call printf(“abcd”)

$1=4

l finish 结束执行当前函数,显示其返回值(如果有的话)

九.机器语言工具

有一组专用的gdb变量可以用来检查和修改计算机的通用寄存器,gdb提供了目前每一台计算机中实际使用的4个寄存器的标准名字:

 $pc : 程序计数器

 $fp : 帧指针(当前堆栈帧)

 $sp : 栈指针

 $ps : 处理器状态

十.信号

gdb通常可以捕捉到发送给它的大多数信号,通过捕捉信号,它就可决定对于正在运行的进程要做些什么工作。例如,按CTRL-C将中断信号发送给gdb,通常就会终止gdb。但是你或许不想中断gdb,真正的目的是要中断gdb正在运行的程序,因此,gdb要抓住该信号并停止它正在运行的程序,这样就可以执行某些调试操作。

Handle命令可控制信号的处理,他有两个参数,一个是信号名,另一个是接受到信号时该作什么。几种可能的参数是:

 nostop 接收到信号时,不要将它发送给程序,也不要停止程序。

 stop 接受到信号时停止程序的执行,从而允许程序调试;显示一条表示已接受到信号的消息(禁止使用消息除外)

 print 接受到信号时显示一条消息

 noprint 接受到信号时不要显示消息(而且隐含着不停止程序运行)

 pass 将信号发送给程序,从而允许你的程序去处理它、停止运行或采取别的动作。

 nopass 停止程序运行,但不要将信号发送给程序。

例如,假定你截获SIGPIPE信号,以防止正在调试的程序接受到该信号,而且只要该信号一到达,就要求该程序停止,并通知你。要完成这一任务,可利用如下命令:

(gdb) handle SIGPIPE stop print

请注意,UNIX的信号名总是采用大写字母!你可以用信号编号替代信号名

如果你的程序要执行任何信号处理操作,就需要能够测试其信号处理程序,为此,就需要一种能将信号发送给程序的简便方法,这就是signal命令的任务。该命令的参数是一个数字或者一个名字,如SIGINT。假定你的程序已将一个专用的SIGINT(键盘输入,或CTRL-C;信号2)信号处理程序设置成采取某个清理动作,要想测试该信号处理程序,你可以设置一个断点并使用如下命令:

(gdb) signal 2

continuing with signal SIGINT(2)

该程序继续执行,但是立即传输该信号,而且处理程序开始运行.

十一. 原文件的搜索

search text:该命令可显示在当前文件中包含text串的下一行。

Reverse-search text:该命令可以显示包含text 的前一行。

十二.UNIX接口

shell 命令可启动UNIX外壳,CTRL-D退出外壳,返回到 gdb.

十三.命令的历史

为了允许使用历史命令,可使用 set history expansion on 命令

(gdb) set history expansion on

小结:常用的gdb命令

backtrace 显示程序中的当前位置和表示如何到达当前位置的栈跟踪(同义词:where)

breakpoint 在程序中设置一个断点

cd 改变当前工作目录

clear 删除刚才停止处的断点

commands 命中断点时,列出将要执行的命令

continue 从断点开始继续执行

delete 删除一个断点或监测点;也可与其他命令一起使用

display 程序停止时显示变量和表达时

down 下移栈帧,使得另一个函数成为当前函数

frame 选择下一条continue命令的帧

info 显示与该程序有关的各种信息

jump 在源程序中的另一点开始运行

kill 异常终止在gdb 控制下运行的程序

list 列出相应于正在执行的程序的原文件内容

next 执行下一个源程序行,从而执行其整体中的一个函数

print 显示变量或表达式的值

pwd 显示当前工作目录

pype 显示一个数据结构(如一个结构或C++类)的内容

quit 退出gdb

reverse-search 在源文件中反向搜索正规表达式

run 执行该程序

search 在源文件中搜索正规表达式

set variable 给变量赋值

signal 将一个信号发送到正在运行的进程

step 执行下一个源程序行,必要时进入下一个函数

undisplay display命令的反命令,不要显示表达式

until 结束当前循环

up 上移栈帧,使另一函数成为当前函数

watch 在程序中设置一个监测点(即数据断点)

whatis 显示变量或函数类型

****************************************************

 GNU的调试器称为gdb,该程序是一个交互式工具,工作在字符模式。在 X Window 系统中,有一个gdb的前端图形工具,称为xxgdb。gdb 是功能强大的调试程序,可完成如下的调试任务:

  * 设置断点;

  * 监视程序变量的值;

  * 程序的单步执行;

  * 修改变量的值。

  在可以使用 gdb 调试程序之前,必须使用 -g 选项编译源文件。可在 makefile 中如下定义 CFLAGS 变量:

   CFLAGS = -g

   运行 gdb 调试程序时通常使用如下的命令:

   gdb progname

  在 gdb 提示符处键入help,将列出命令的分类,主要的分类有:

  * aliases:命令别名

  * breakpoints:断点定义;

  * data:数据查看;

  * files:指定并查看文件;

  * internals:维护命令;

  * running:程序执行;

  * stack:调用栈查看;

  * statu:状态查看;

  * tracepoints:跟踪程序执行。

  键入 help 后跟命令的分类名,可获得该类命令的详细清单。

gdb 的常用命令

命令 解释

  break NUM 在指定的行上设置断点。

  bt 显示所有的调用栈帧。该命令可用来显示函数的调用顺序。

  clear 删除设置在特定源文件、特定行上的断点。其用法为clear FILENAME:NUM

  continue 继续执行正在调试的程序。该命令用在程序由于处理信号或断点而 导致停止运行时。

  display EXPR 每次程序停止后显示表达式的值。表达式由程序定义的变量组成。

  file FILE 装载指定的可执行文件进行调试。

  help NAME 显示指定命令的帮助信息。

  info break 显示当前断点清单,包括到达断点处的次数等。

  info files 显示被调试文件的详细信息。

  info func 显示所有的函数名称。

  info local 显示当函数中的局部变量信息。

  info prog 显示被调试程序的执行状态。

  info var 显示所有的全局和静态变量名称。

  kill 终止正被调试的程序。

  list 显示源代码段。

  make 在不退出 gdb 的情况下运行 make 工具。

  next 在不单步执行进入其他函数的情况下,向前执行一行源代码。

  print EXPR 显示表达式 EXPR 的值。

******gdb 使用范例************************

-----------------

清单 一个有错误的 C 源程序 bugging.c

代码:

-----------------

1 #include<stdio.h>

2    #include<string.h>

3 static char buff [256];

4 static char* string;

5 int main ()

6 {

7   printf ("Please input a string: ");

8   gets (string);  

9   printf ("\nYour string is: %s\n", string);

10 }

-----------------

 上面这个程序非常简单,其目的是接受用户的输入,然后将用户的输入打印出来。该程序使用了一个未经过初始化的字符串地址 string,因此,编译并运行之后,将出现 Segment Fault 错误:

$ gcc -o bugging -g bugging.c

$ ./bugging

Please input a string: asfd

Segmentation fault (core dumped)

为了查找该程序中出现的问题,我们利用 gdb,并按如下的步骤进行:

1.运行 gdb bugging 命令,装入 bugging 可执行文件;

2.执行装入的 bugging 命令 run;

3.使用 where 命令查看程序出错的地方;

4.利用 list 命令查看调用 gets 函数附近的代码;

5.唯一能够导致 gets 函数出错的因素就是变量 string。用print命令查看 string 的值;

6.在 gdb 中,我们可以直接修改变量的值,只要将 string 取一个合法的指针值就可以了,为此,我们在第8行处设置断点 break 8;

7.程序重新运行到第 8行处停止,这时,我们可以用 set variable 命令修改 string 的取值;

8.然后继续运行,将看到正确的程序运行结果。

使用GDB调试一个工程:(每个文件编译的时候都要加 -g 选项)

1。对每个文件都加-g

2。在运行时对主函数中调用其他文件定义的行设置断点,然后用s命令(step)就可以进入当前调用函数所在的文件了。

可以用list查看,并加新的断点,继续调试。

继续阅读