GDB体系结构介绍(二)

4.7 符号方面

GDB的符号端主要负责读取可执行文件,提取它找到的任何符号信息,并将其构建到符号表中。

读取过程从BFD库开始。 BFD是一种用于处理二进制文件和目标文件的通用库;在任何主机上运行,​​它可以读取和写入原始的Unix a.out格式,COFF(用于System V Unix和MS Windows),ELF(现代Unix,GNU / Linux和大多数嵌入式系统),以及其他一些文件格式。在内部,库具有复杂的C宏结构,可扩展为代码,包含许多不同系统的目标文件格式的神秘细节。 GNU汇编器和链接器也使用BFD,它为任何目标生成目标文件的能力是使用GNU工具进行交叉开发的关键。 (移植BFD也是将工具移植到新目标的关键第一步。)

GDB仅使用BFD读取文件,使用它将可执行文件中的数据块拉入GDB的内存。然后GDB有两个级别的读者功能。第一级用于基本符号或“最小符号”,它们只是链接器完成其工作所需的名称。这些是带地址的字符串,而不是其他内容;我们假设文本部分中的地址是函数,数据部分中的地址是数据,等等。

第二级是详细的符号信息,通常具有与基本可执行文件格式不同的格式;例如,DWARF调试格式的信息包含在ELF文件的特殊命名部分中。相比之下,Berkeley Unix的旧stabs调试格式使用了存储在通用符号表中的特殊标记符号。

用于读取符号信息的代码有点单调乏味,因为不同的符号格式编码可能在源程序中的每种类型信息,但每种类型信息都以其自己的特殊方式进行。 GDB阅读器只是遍历格式,构建我们认为对应于符号格式的GDB符号。

部分符号表

对于大尺寸的程序(例如Emacs或Firefox),符号表的构造可能需要相当长的时间,甚至可能需要几分钟。测量结果一致表明,时间不是人们所期望的文件读取时间,而是GDB符号的内存构造。实际上涉及数百万个小型互连物体,时间加起来。

大多数符号信息永远不会在会话中被查看,因为它是用户可能永远不会检查的函数的本地信息。因此,当GDB首先引入程序的符号时,它会粗略地扫描符号信息,只查找全局可见符号并仅在符号表中记录它们。仅当用户在其中停止时,才会填写函数或方法的完整符号信息。

部分符号表允许GDB在几秒钟内启动,即使对于大型程序也是如此。 (共享库符号也是动态加载的,但过程是相当不同的。通常GDB使用特定于系统的技术在加载库时得到通知,然后构建一个符号表,其中的函数位于动态决定的地址上链接。)

语言支持

源语言支持主要包括表达式解析和值打印。表达式解析的细节留给每种语言,但一般来说,解析器基于由手工制作的词法分析器提供的Yacc语法。为了与GDB为交互式用户提供更多灵活性的目标保持一致,解析器预计不会特别严格;例如,如果它可以猜测表达式的合理类型,它将简单地假设该类型,而不是要求用户添加强制转换或类型转换。

由于解析器不需要处理语句或类型声明,因此它比完整语言解析器简单得多。类似地,对于打印,只需要显示少数几种类型的值,并且特定于语言的打印功能通常可以调用通用代码来完成作业。

4.8 目标方

目标方面是关于程序执行和原始数据的操纵。从某种意义上说,目标端是一个完整的低级调试器;如果您满足于逐步执行指令并转储原始内存,则可以使用GDB而无需任何符号。 (无论如何,如果程序碰巧在符号被删除的库中停止,你最终可能会以这种模式运行。)

目标矢量和目标堆栈

最初GDB的目标端由少数特定于平台的文件组成,这些文件处理调用ptrace,启动可执行文件等的细节。这对于长时间运行的调试会话来说不够灵活,在这些会话中,用户可能会从本地调试切换到远程调试,从文件切换到核心转储到实时程序,连接和分离等,所以在1990年John Gilmore重新设计了目标端GDB通过目标向量发送所有特定于目标的操作,目标向量基本上是一类对象,每个对象定义一种目标系统的细节。每个目标向量都实现为几十个函数指针(通常称为“方法”)的结构,其目的包括读取和写入内存和寄存器,恢复程序执行,设置参数以处理共享库。 GDB中有大约40个目标向量,从用于Linux的常用目标向量到模糊向量,例如运行Xilinx MicroBlaze的向量。核心转储支持使用通过读取核心文件来获取数据的目标向量,还有另一个目标向量从可执行文件中读取数据。

混合来自几个目标载体的方法通常是有用的。考虑在Unix上打印初始化的全局变量;在程序开始运行之前,打印变量应该可以工作,但是此时没有要读取的进程,并且字节需要来自可执行文件的.data部分。因此,GDB使用目标向量来执行可执行文件并从二进制文件中读取。但是在程序运行时,字节应该来自进程的地址空间。因此,GDB有一个“目标堆栈”,当进程开始运行时,实时进程的目标向量被推送到可执行文件的目标向量之上,并在它退出时弹出。

实际上,目标堆栈并不像人们想象的那样像堆栈一样。目标矢量并不真正彼此正交;如果你在会话中同时拥有可执行文件和实时进程,虽然让实时进程的方法覆盖可执行文件的方法是有意义的,但反过来几乎没有意义。因此,GDB最终得出了一个层次的概念,其中“类似过程”的目标向量都在一个层,而“类文件”目标向量被分配到较低层,并且目标向量可以插入以及推了推。

(虽然GDB维护者不太喜欢目标堆栈,但没有人提出或原型化 - 任何更好的替代方案。)

Gdbarch

作为直接与CPU指令一起工作的程序,GDB需要深入了解芯片的细节。它需要知道所有寄存器,不同类型数据的大小,地址空间的大小和形状,调用约定的工作方式,导致陷阱异常的指令等等。 GDB的所有这些代码通常在1,000到10,000多行C之间,具体取决于架构的复杂性。

最初这是使用特定于目标的预处理器宏来处理的,但随着调试器变得越来越复杂,这些宏越来越大,并且随着时间的推移,宏定义被定义为从宏调用的常规C函数。虽然这有所帮助,但它对架构变体(ARM与Thumb,32位与64位版本的MIPS或x86等)没有太大帮助,更糟糕的是,多架构设计即将出现,宏根本不起作用。 1995年,我提议用基于对象的设计解决这个问题,从1998年开始,Cygnus Solutions资助Andrew Cagney开始转换。 (Cygnus Solutions是一家成立于1989年的公司,为Red Hat在2000年收购的自由软件提供商业支持。)花了几年的时间和数十名黑客的贡献来完成这项工作,总共影响了80,000行代码。

引入的构造称为gdbarch对象,此时可能包含多达130个定义目标体系结构的方法和变量,尽管一个简单的目标可能只需要十几个这样的方法。

要了解新旧方法的比较,请参阅gdb / config / i386 / tm-i386.h(大约2002年)x86 long double的大小为96位的声明:

#define TARGET_LONG_DOUBLE_BIT 96

从2012年的gdb / i386-tdep.c:

i386_gdbarch_init( [...] )
{
  [...]

  set_gdbarch_long_double_bit (gdbarch, 96);

  [...]
}

执行控制

GDB的核心是它的执行控制循环。我们在描述单行踩线时更早地提到了它;该算法需要循环多个指令,直到找到与不同源线相关联的指令。循环称为wait_for_inferior,简称为“wfi”。

从概念上讲,它位于主命令循环中,并且仅为导致程序恢复执行的命令输入。当用户键入continue或step然后等待而没有发生任何事情时,GDB实际上可能非常繁忙。除了上面提到的单步循环之外,程序可能正在命中陷阱指令并将异常报告给GDB。如果异常是由于陷阱是由GDB插入的断点,则它会测试断点的条件,如果为false,则删除陷阱,单步执行原始指令,重新插入陷阱,然后让程序恢复。类似地,如果发出信号,GDB可以选择忽略它,或者以预先指定的几种方式之一处理它。

所有这些活动都由wait_for_inferior管理。最初这是一个简单的循环,等待目标停止,然后决定如何处理它,但由于各种系统的端口需要特殊处理,它增长到一千行,goto语句纵横交错,原因很难理解。例如,随着Unix变种的激增,没有一个人能理解他们所有的优点,也没有人能够访问所有他们的回归测试,所以有一种强烈的动机来修改代码保留现有端口的行为 - 并且跳过部分循环是一个非常简单的策略。

对于任何类型的线程程序的异步处理或调试,单个大循环也是一个问题,其中用户想要启动和停止单个线程,同时允许程序的其余部分继续运行。

转换为面向事件的模型花了几年时间。我在1999年分解了wait_for_inferior,引入了一个执行控制状态结构来替换一堆局部变量和全局变量,并将跳转的纠结转换为更小的独立函数。与此同时,Elena Zannoni和其他人引入了事件队列,其中包括来自用户的输入和来自下级的通知。

远程协议

尽管GDB的目标向量架构允许多种方式来控制在不同计算机上运行的程序,但我们只有一个首选协议。它没有区别名称,通常只称为“远程协议”,“GDB远程协议”,“远程串行协议”(缩写为“RSP”),“remote.c协议”(源文件之后)实现它),或者有时是“存根协议”,指的是目标的协议实现。

基本协议很简单,反映了将其用于20世纪80年代的小型嵌入式系统的愿望,其存储器以千字节为单位进行测量。例如,协议包$ g请求所有寄存器,并期望包含所有寄存器的所有字节的应答,所有寄存器一起运行 - 假设它们的数量,大小和顺序将与GDB知道的相匹配。

该协议期望对发送的每个数据包进行单一回复,并假设连接是可靠的,仅向发送的数据包添加校验和(因此$ g实际上通过网络发送为$ g#67)。

尽管只有少数必需的数据包类型(对应于最重要的六种目标向量方法),但多年来已添加了大量额外的可选数据包,以支持从硬件断点,跟踪点到共享的所有内容。库。

在目标本身上,远程协议的实现可以采用多种形式。该协议在GDB手册中有完整的记录,这意味着可以编写一个不受GNU许可证限制的实现,事实上,许多设备制造商已经在实验室和实验室中集成了代表GDB远程协议的代码。场。运行大部分网络设备的思科IOS就是一个众所周知的例子。

目标的协议实现通常被称为“调试存根”,或者只是“存根”,意味着它不会自己做很多工作。 GDB源包括一些示例存根,通常约为1,000行低级别C。在没有操作系统的完全裸板上,存根必须安装自己的处理程序以用于硬件异常,最重要的是捕获陷阱指令。如果硬件链接是串行线,它还需要串行驱动程序代码。实际的协议处理很简单,因为所有必需的数据包都是可以用switch语句解码的单个字符。

远程协议的另一种方法是构建一个“精灵”,它在GDB和专用调试硬件之间进行接口,包括JTAG设备,“摇摆器”等。这些设备通常都有一个必须在物理上连接到目标的计算机上运行的库。通常,库API在架构上与GDB的内部结构不兼容。因此,虽然GDB的配置直接调用了硬件控制库,但事实证明,将sprite作为一个独立的程序运行更简单,该程序了解远程协议并将数据包转换为设备库调用。

GDBSERVER

GDB源确实包括远程协议目标端的一个完整且有效的实现:GDBserver。 GDBserver是一个本机程序,在目标操作系统下运行,并使用其本机调试支持控制目标操作系统上的其他程序,以响应通过远程协议接收的数据包。换句话说,它充当本机调试的一种代理。

GDBserver不执行本机GDB无法执行的任何操作;如果您的目标系统可以运行GDBserver,那么从理论上讲它可以运行GDB。但是,GDBserver小10倍,不需要管理符号表,因此对于嵌入式GNU / Linux用法等非常方便。

                                           图4.2:GDBserver

GDB和GDBserver共享一些代码,但是虽然封装特定于操作系统的进程控制是一个明显的想法,但在本机GDB中分离出默认依赖关系存在实际困难,并且转换进展缓慢。

原文链接

猜你喜欢

转载自blog.csdn.net/u013702678/article/details/83815550
今日推荐