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

本文详细介绍对GDB Python API的三项改进:增强类型信息显示、支持动态创建类型及注册符号功能,帮助开发者在无符号环境下更高效地进行逆向工程和调试工作。

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 顺序调用一次。然而,这种理解有一个主要问题:尽管数据表面上分离,但它并未完全分离为完全读写、部分读写(具有隐式同步)和只读数据。解析子例程完全期望所有这些数据结构至少在一定程度上是可读写的。

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

什么是 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 运行时释放时很可能不在栈顶。因此,无论它们的实际生命周期要求如何,类型只能与拥有它们的 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 directly。

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

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

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