GDB Python API 消息与类型改进:提升调试器开发体验

本文详细介绍了对GDB Python API的三项改进:增强类型对象的__repr__方法、支持直接创建GDB原生类型以及动态注册符号功能,这些改进显著提升了调试工具开发效率和逆向工程能力。

GDB Python API 的改进:消息与类型增强

作为Trail of Bits的冬季实习生,我的目标是改进GNU项目调试器(GDB)的两个方面:提升运行速度并改进其Python API,以支持并增强依赖它的工具(如Pwndbg)。主要目标包括并行运行符号解析以更好地利用所有可用的CPU核心。最终,我实现了三项改进来增强GDB的Python API。

除了实际代码,我还学习了在GDB中上游化补丁的过程。这一过程可能需要一段时间,有一定的学习曲线,并涉及与项目维护者的大量来回讨论。我将在本文中讨论这一点,您也可以跟随我的工作进展,因为它仍在GDB补丁邮件列表中进行辩论。

为什么让GDB更快?

GDB有三种方式从程序中加载DWARF符号:

  • 部分符号表加载器:索引加载器负责加载符号名称并将其连接到各自的编译单元(CUs),将解析和构建符号表的工作留给完整加载器。解析仅在需要符号的完整信息时进行。
  • 完整符号表加载器:通过解析CUs并根据需要构建符号表来完成索引加载器留下的工作。此加载器完全解析文件中的DWARF信息并将其存储在内存中。
  • 索引解析器:ELF可以有一个特殊的.gdb_index部分,通过–gdb-index链接器标志或GDB提供的gdb-add-index工具添加。该工具存储内部符号表的索引,允许GDB跳过索引构建过程,显著减少在GDB中加载二进制文件所需的时间。

最初的想法是将Meta的开源调试器drgn中的并行解析方法移植到GDB。并行解析已经为索引加载器实现,只剩下完整加载器和索引解析器作为并行化的潜在候选者。

您可以将GDB的解析例程视为在每个CU基础上拆分为并发任务,因为它们已经按每个CU顺序调用。然而,这种理解有一个主要问题:尽管数据表面上分离,但并未完全分离为读写数据、部分读写数据(带隐式同步)和只读数据。解析子例程完全期望所有这些数据结构至少在一定程度上是读写的。

虽然解决大多数这些问题是一个简单的情况,即将值拆分为单独的读写副本(每个线程拥有一个),但像注册表、缓存,特别是obstack这样的东西更难移动到并发模型。

什么是obstack?

通用分配(如malloc())是耗时的。当用户需要尽可能快地分配许多小对象时,它们可能效率不高,因为它们在每个分配中存储元数据。

进入分配堆栈。每个新对象按顺序在顶部分配并从顶部释放。GNU Obstack是这种分配器的实现,在GDB中广泛使用。每个合理长寿命的容器对象,包括objfile和gdbarch,都有自己的obstack实例,并用于保存其引用的对象,并在对象本身释放时一次性释放它们。

如果您了解对象生命周期跟踪——无论是动态的(如std::shared_ptr)还是静态的(如Rust中的引用)——最后一段听起来会很熟悉。根据obstack分配在GDB中的使用方式,有人可能会假设有一种方法可以保证对象存活时间与拥有它们的容器一样长。

在与IRC和邮件列表中的其他人讨论后,我得出两个结论:调查这将花费相当多的时间,我最好优先处理Python API,以便有机会按时完成改进。最终,我将大部分时间花在了这些可实现的目标上。

GDB对象的__repr__方法

第一个更改相当简单。它为GDB Python API中的一些类型添加了__repr__()实现。这一更改使得我们在Python REPL中检查类型时获得的消息更能说明这些类型代表什么。

以前,我们会得到类似这样的内容,几乎毫无帮助(注意:pi是运行Python REPL的GDB命令):

1
2
3
(gdb) pi
>>> gdb.lookup_type("char")
<gdb.Type object at 0x7ff8e01aef20>

现在,我们可以得到以下内容,它告诉我们这是什么类型的类型,以及它的名称,而不是对象在内存中的位置:

1
2
3
(gdb) pi
>>> gdb.lookup_type("char")
<gdb.Type code=TYPE_CODE_INT name=char>

这也适用于gdb.Architecture、gdb.Block、gdb.Breakpoint、gdb.BreakpointLocation和gdb.Symbol。

这帮助我理解了GDB如何与Python接口以及Python C API通常如何工作。它允许我后来添加自己的函数和类型。

类型来了!

第二个更改添加了从Python API创建类型的能力,而以前只能使用gdb.lookup_type()查询现有类型。现在,您可以直接创建GDB支持的任何原始类型,这在您处理代码但没有符号时,或者编写插件帮助人们处理此类代码时非常方便。来自奇怪额外二进制文件的类型不需要申请!

GDB支持相当多的类型。所有类型都可以直接使用gdb.init_type或专门的gdb.init_*_type函数创建,这些函数允许您指定与正在创建的类型相关的参数。大多数函数工作方式类似,除了gdb.init_float_type,它有自己的新gdb.FloatFormat类型。这允许您指定要创建的浮点类型在内存中的布局方式。

这一更改的一个额外考虑是这些新类型的内存究竟来自哪里。由于这些函数基于GDB内部已有的函数,并且这些函数使用给定objfile的obstack,因此obstack是这些分配的内存来源。这有一个很大的优势:引用这些类型并属于同一objfile的对象保证不会比它们存活更久。

您可能已经意识到这种方法的一个显著缺点:当Python运行时释放类型时,分配在obstack上的任何类型很有可能不在堆栈顶部。因此,无论它们的实际生命周期要求如何,类型只能与拥有它们的objfile一起释放。主要含义是,不可达的类型将在objfile的生命周期内泄漏其内存。

手动跟踪类型的初始化需要对现有类型对象基础设施进行更深入的更改。这对于第一个补丁来说太雄心勃勃了。

以下是此方法的一些示例:

1
2
3
4
5
6
(gdb) pi
>>> objfile = gdb.lookup_objfile("servo")
>>>
>>> # Time to standardize integer extensions. :^)
>>> gdb.init_type(objfile, gdb.TYPE_CODE_INT, 24, "long short int")
<gdb.Type code=TYPE_CODE_INT name=long short int>

这将创建一个名为“long short int”的新24位整数类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(gdb) pi
>>> objfile = gdb.lookup_objfile("servo")
>>>
>>> ff = gdb.FloatFormat()
>>> ff.totalsize = 32
>>> ff.sign_start = 0
>>> ff.exp_start = 1
>>> ff.exp_len = 8
>>> ff.man_start = 9
>>> ff.man_len = 23
>>> ff.intbit = False
>>>
>>> gdb.init_float_type(objfile, ff, "floatier")
<gdb.Type code=TYPE_CODE_FLOAT name=floatier>

这将创建一个新的浮点类型,让人想起标准x86机器中可用的类型。

符号呢?

第三个更改添加了注册三种符号的能力:类型、goto标签和静态变量。这使得添加新符号变得容易得多,这在您进行逆向工程且没有任何原始符号时特别有用。没有此补丁,添加新符号的主要方法涉及将它们添加到单独的文件中,将文件编译到目标架构,并在使用add-symbol-file命令加载基础程序后将其加载到GDB中。

GDB的内部符号基础设施大多不适用于动态添加。让我们看看GDB如何创建、存储和查找符号。

GDB中的符号通过称为compunit_symtab的结构深处的指针找到。这些结构通过构建器设置,允许在构建过程中将符号添加到表中。此构建器后来负责将新结构注册到(在此补丁的情况下)拥有它的objfile。在objfile的情况下,这些表存储在一个列表中,在查找期间——不考虑符号查找缓存——会遍历该列表,直到在其中一个表中找到符合给定要求的符号。

目前,表没有设置以便在构建后随意向表中添加符号。因此,如果我们不想在第一个补丁之前对GDB进行深度更改,我们必须找到一种方法绕过此限制。我最终的方法是构建一个新的符号表,并将其连接到列表的末尾,以便每个新符号。尽管这是一种相当低效的方法,但足以使功能工作。

随着此补丁继续上游化,我旨在改进实现此功能的机制。

最后,我想展示一个创建新类型并将其注册为符号以供将来查找的示例:

1
2
3
4
5
6
(gdb) pi
>>> objfile = gdb.lookup_objfile("servo")
>>> type = gdb.init_type(objfile, gdb.TYPE_CODE_INT, 24, "long short int")
>>> objfile.add_type_symbol("long short int", type)
>>> gdb.lookup_type("long short int")
<gdb.Type code=TYPE_CODE_INT name=long short int>

全部合并

总体而言,这个冬季在Trail of Bits产生了更具信息性的消息,以及在GDB的Python API中创建支持类型的能力,这在您没有代码符号时非常有用。

GDB在处理贡献方面是老派的。其维护者使用电子邮件提交、测试和评论补丁,然后才上游化。这通常意味着在提交补丁时有非常严格的礼仪要遵循。

作为一个从未处理过基于电子邮件的项目的人,我第一次尝试提交补丁很糟糕。我拼凑了一个包含git diff输出的文本文件,然后手动编写了整个消息,然后通过一个处理非UNIX行结尾不佳的客户端发送。这造成了一个混乱,可以理解的是,列表中没有维护者愿意修补和测试。尽管如此,他们足够友好地告诉我应该使用Git的内置电子邮件功能:git send-email直接发送。

在那次事件之后,我花时间将我的更改拆分为适当的分支,并重新基它们,以便它们全部压缩为每个主要更改的单个提交。这创建了一个更合理和描述性的消息,涵盖了整个更改,并且更适合与git send-email一起使用。自那以后,事情进展相当顺利,尽管为了将所有更改纳入,进行了大量的来回讨论。

虽然这三个更改已经提交,但实现__repr__()的更改在管道中更靠后,而其他两个仍在等待审查。请密切关注它们!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计