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

本文详细介绍了对GDB Python API的三项改进:增强类型对象的__repr__方法、支持通过Python API直接创建类型以及添加符号注册功能。这些改进提升了调试效率,特别是在缺乏符号信息的情况下进行逆向工程时尤为有用。

A Winter’s Tale: Improving messages and types in GDB’s Python API

Matheus Branco Borella, University of São Paulo
April 18, 2023
internship-projects

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

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

为什么让GDB更快?

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

  • 部分符号表加载器:索引加载器负责加载符号名称并将其连接到各自的编译单元(CU),将解析和构建其符号表的工作留给完整加载器。解析仅在需要符号的完整信息时稍后进行。
  • 完整符号表加载器:通过解析CU并根据需要构建其符号表来完成索引加载器留下的工作。此加载器完全解析文件中的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运行时释放它时,分配在其上的任何类型很有可能不在栈的顶部。因此,无论它们的实际生命周期要求如何,类型只能与拥有它们的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__()的那个在管道中更靠后,而其他两个仍在等待审查。请密切关注它们!

如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

页面内容 为什么让GDB更快? 什么是obstack? GDB对象的__repr__方法 类型来了! 符号呢? 全部合并 最近的帖子 Trail of Bits的Buttercup在AIxCC挑战赛中获得第二名 Buttercup现已开源! AIxCC决赛:记录表 攻击者的提示注入工程:利用GitHub Copilot 在新员工中发现NVIDIA Triton中的内存损坏 © 2025 Trail of Bits。 使用Hugo和Mainroad主题生成。

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