文章翻译——使用 GNU 的 GDB调试器,内存布局和栈——02

接上一篇:使用 GNU 的 GDB调试器,内存布局和栈 —— 01

原文地址:http://www.dirac.org/linux/gdb/02a-Memory_Layout_And_The_Stack.php

符号表

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

      

       为了让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的相关博文。

posted @ 2012-05-13 21:51  KingsLanding  阅读(1602)  评论(0编辑  收藏  举报