卷2:第4章 GDB

2018-02-24 15:55 更新

原文链接:http://www.aosabook.org/en/gdb.html

作者:Stan Shebs

GDB, 即GNU调试器(GNU Debugger)。它诞生自开源软件基金会(Free Software Foundation)成立之初的第一批程序,并一直是免费和开源软件系统中的主要成员。最初GDB只是Unix系统上一个简单的源码层次的调试器,代码量不过数千行C代码,后来逐步发展壮大,拓展到包括嵌入式系统在内多个平台,代码量也达到了上百万行。

GDB在发展,不断地满足着新的用户需求并增加新的功能。这一章将我们将介绍GDB的整体内部结构,探讨一下GDB是如何做到这一点的。

4.1 目标

GDB的设计目标是一个针对使用命令式(imperative)语言(例如C,C++,Ada,Fortran等)编写的程序的符号调试器。使用GDB原始命令行界面的一个示例如下:

% gdb myprog
[...]
(gdb) break buggy_function
Breakpoint 1 at 0x12345678: file myprog.c, line 232.
(gdb) run 45 92
Starting program: myprog
Breakpoint 1, buggy_function (arg1=45, arg2=92) at myprog.c:232
232     result = positive_variable * arg1 + arg2;
(gdb) print positive_variable
$$1 = -34
(gdb)

GDB能显示程序中的错误,开发者据此判断错误的类型并找到解决的方案。

设计GDB最需要考虑的是调试工具的交互性,因为用户在调试时提交的请求是不可预测的。此外,GDB还需要深入到系统最底层,因为编译器会充分利用硬件的各种选项来优化程序的性能。

GDB还要求能够调试不同编译器编译的程序(不仅仅是GNU C编译器),能够调试过时编译器编译的程序,能够调试符号信息丢失、过时或错误的程序。所以,另外一个设计要求是,即使程序中的数据丢失、损坏或干脆无法理解,GDB也能够继续工作并发挥作用。

接下来的几章假定读者熟悉GDB基本的命令行使用方法。如果你还是新手,建议先用一用GDB并细读一下手册[SPS+00]

4.2 GDB的起源

GDB程序历史悠久,早在1985年就已经存在。它的作者是Richard Stallman,这个人还编写了GCC,GNU Emacs和其它一些早期的GNU软件。(由于当时并没有软件仓库,GDB开发过程的细节已不为人所知。)

GDB的最早的稳定版本在1988年发布,但在今天的GDB源码中已经找不到多少相似的地方了,GDB被完全重写过至少一次。 令人惊讶的是,早期的GDB并没有太大的野心,后来的平台移植和功能扩展并没有包括在GDB最初的计划之中。

4.3 GDB结构框图

set print elements 80"使用了3个表格,第一个是包含了所有命令的表格,第二个是包含了set选项的表格,第三个是print选项的表格,其中elements选项用于控制打印一个集合体(如字符串或数组)中输出对象的个数。最后瀑布型表格将控制权交给一个实际的命令处理函数,命令的参数将传递给这个函数来解析。一些命令, 比如run, 处理参数的方式和传统C语言的argc/argv标准类似, 而其它一些命令, 比如print, 则假定参数是一个程序表达式, 并将其完整传递给源码解析器。

机器界面

一种GUI调试器方案是将GDB作为图形用户界面程序的后端,将鼠标点击翻译成GDB命令,然后将打印的结果显示在窗口中。这种方案已经在一些软件中实现,比如KDbg和DDD(Data Display Debugger)。但这个方法仍然不理想,因为有时候显示结果时为了可读性会省略掉一些细节,前端提供上下文的能力也会影响到结果的显示。

为解决这个问题,GDB提供了一个被称为机器界面(Machine Interface,MI)的接口。本质上MI仍然是一个命令行界面,但是命令和结果都增加了额外的语法,使得其意义更为显然:每个参数都使用了引号,复杂输出则使用定界符来分组,使用参数名来分块。此外, MI的命令还可以加上顺序标识符作为前缀, 并在结果中返回,保证了结果和命令的匹配。

为了比较两种界面, 分别给出它们对于同一命令的使用情况。下面是正常的step命令及GDB的响应

(gdb) step

buggy_function (arg1=45, arg2=92) at ex.c:232
232  result = positive_variable * arg1 + arg2;
With the MI, the input and output are more verbose, but easier for other software to parse accurately:

下面是MI的输入和输出,虽然显得有些冗余,但更加精确,便于第三方软件进行解析。

4321-exec-step

4321^done,reason="end-stepping-range",
      frame={addr="0x00000000004004be",
             func="buggy_function",
             args=[{name="arg1",value="45"},
                   {name="arg2",value="92"}],
             file="ex.c",
             fullname="/home/sshebs/ex.c",
             line="232"}

Eclipse[ecl12]开发环境是最著名的使用MI的调试环境。

其它用户界面

其它GDB前端软件包括基于tcl/tk的GDBtk或Insight,基于文字界面的TUI(最初由Hewlett-Packard开发)。GDBtk是一个传统的多面板图形用户界面,使用tk软件库开发,而TUI是一个在终端中使用的分屏文字界面。

4.10. 开发过程

维护者

作为一个GNU程序,GDB的开发遵循"大教堂(cathedral)"开发模型。GDB最初由Stallman编写,随后维护者几易其人,每个人都是身兼设计师,补丁审查员,发布管理员数职,他们有权访问仅向少数Cygnus雇员开放的源码仓库。

1999年,GDB被迁移到一个公共源码仓库,维护团队也扩展到了几十人,并且还有一些拥有签入(commit)权限的个人从旁协助。这个模式显著加速了GDB的开发,从原来的每周10个签入增加到了100个以上。

测试,测试

由于GDB高度依赖于特定平台,几乎涵盖全系列的计算设备,而且包含了数以百计的命令,选项以及使用风格,即使是一个经验丰富的GDB黑客也难以完全预料一个修改所产生的后果。

于是,测试套件变得举足轻重。GDB的测试套件包含了众多测试程序以及expect脚本,使用一个基于tcl被称为DejaGNU的测试框架。其基本模式是, 每个脚本驱动GDB去调试一个测试程序, 然后向其发送命令, 并使用模式匹配来判断结果正确与否。

这个测试套件还能进行交叉调试,既支持真实硬件也支持模拟器,它还能对于特定平台或配置进行测试。

到2011年底,GDB测试套件包含了大约18000个测试用例,包括了基本功能测试,语言特性测试,体系特性测试,和MI测试。所有这些测试都是通用的,适用于所有配置。GDB需要志愿者来测试打补丁后的源码,新的功能也需要新的测试。但是,因为没有人能在所有平台上测试同一修改,要实现测试的完全通过是不现实的。对于本地调试来说, 主干GDB测试时失败10-20次左右是可以授受的, 嵌入式系统则更容易出错。

4.11. 经验教训

开放是王道

GDB是"大教堂"开发模型的典范,在该模式下,维护者严密控制源码,而外部用户则跟踪其进度。补丁提交数目较少,封闭的开发过程实际上并不鼓励补丁。自从采用开放模式之后,补丁数量显著增多,而软件质量则一如既往,甚至更好。

制订计划, 但计划赶不上变化

开源软件开发过程实际上会比较混乱,因为开发者之间是松散的,流动性很大。

但是,制订开发计划并发布仍然很有意义。这有助于指导开发者完成相关任务,而且能够吸引潜在的赞助者,另外志愿者在尝试做出贡献时也能有一定的依据。

但是不要尝试设置截止时间,即使是每个人都热情地朝着一个方向努力,也不要指望大家都能全身心地投入并按时完成任务。

鉴于此,不要坚持一个已经过时的计划。长期以来,GDB都有重构为软件库libgdb的计划,这样, 别的程序就可以通过使用libgdb来实现一个拥有GUI的调试器。开发人员甚至尝试过将构建libgdb.a作为整个构建过程的一个中间步骤。虽然这个想法一直存在,但随着Eclipse和MI的成功,libgdb被搁置了起来,到2012年1月这个想法最终寿终正寝。

无比聪明该多好

看到曾经提交的修改,我们也许会想:为什么一开始不这么做呢? 唉,只因为我们不够聪明。

我们本可以预料到GDB会如此流行,并且会移植到数以百计的平台上,还支持本地和交叉调试。如果事先知道这些,说不定一开始就会使用gdbarch对象,而不会数年来都在用陈旧的宏和全局变量,目标向量也早该出现。

我们本可以预料到GDB将会被用到GUI中, 毕竟1986年Mac和X窗口系统已经出现了2年。与其设计一个传统的命令行界面,我们更应该让其支持异步事件处理。

然而,真正的教训不在于GDB开发者们有多蠢,而是我们不可能如此聪明地未卜先知。1986年, 窗口-鼠标风格的界面的未来还并不清晰, 我们预料不到它会像今天这样流行,如果第一个版本的GDB就设计为在GUI下使用,我们就可以称得上天才了,但这种好运不是人人都能有的。相反,在一个有限的范围内让GDB有所作为,我们已经为今后的扩展和重构打下了用户基础。

学会接受缺陷

尽力完成过渡,但是时间总是太快,你只能接受缺陷。

在2003年的GCC峰会上, Zack Weinberg哀叹GCC的"不完整过渡",新的底层结构已经引入,但是旧的却尾大不掉。GDB有着同样的问题,但是我们应该看到积极的一面,因为毕竟一些过渡已经完成,比如目标向量,gdbarch等等。虽然过渡需要多年来完成,调试却要一直继续。

谨防着迷于代码

当你遇到一个对你非常重要的项目,你会花费大量时间在单个代码上, 你会很容易沉迷其中,甚至为了迎合代码而改变自己的想法。但是,很有可能你已经误入歧途,退一步说不定海阔天空。

这样的事情要杜绝发生。

所有代码都源自于一系列清醒的判断:有些来自灵感,有些则不是。1991年节省空间的小伎俩对于2011年的数个G的内存来说是毫无意义的。

GDB曾经支持Gould超级计算机。当他们在2000年关闭最后一台机器时,保留对这种机器的支持已是毫无意义。那些代码只是GDB过往历史中的一些小小篇章,然而现在大部分的发行版中仍然有些"怀旧"。

事实上,很多激进的修改已经摆上日程或已经开展,包括对Python脚本的支持,对并行多核平台的支持,重编码为C++等。这些修改可能要花费数年,但其动机却来自于今天(等到它们完成时说不定已经过时)。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号