使用rellic-headergen解析C数据结构
你是否曾想过编译器是如何看待你的数据结构的?Compiler Explorer可以帮助你理解源代码和机器码之间的关系,但在数据布局方面提供的支持有限。你可能听说过填充、对齐和"普通旧数据类型"的概念。也许你甚至尝试过通过在C语言中嵌入一个结构体来模拟继承。但你能在不查看平台ABI参考或标准库源代码的情况下,猜出所有这些类型的确切内存布局吗?
|
|
具备必要的ABI知识后,推理C结构体相对简单。然而,更复杂的C++类型则完全是另一回事,特别是当模板和继承参与其中时。理想情况下,我们能够将所有复杂类型转换为简单的C结构体,以便更容易地推理它们的内存布局。这正是rellic-headergen的确切目的,这是我在Trail of Bits实习期间开发的一个工具。
rellic-headergen的工作原理
rellic-headergen的目的是生成与LLVM位码文件中包含的类型定义等效的C类型定义,这些类型不一定是从C源代码生成的。这有助于分析包含复杂数据布局的程序。
该工具基于一个假设:被分析的程序可以编译为包含完整调试信息的LLVM位码。工具首先构建所有可用调试信息的类型列表,从函数定义开始。
工具需要决定类型定义的顺序,但考虑到C语言的要求,这并非易事:当引用尚未定义的类型时,C语言需要显式的前向声明,而且结构体不能包含仅被前向声明类型的字段。
解决方案之一是预防性地前向声明所有现有类型。但这还不够。例如,结构体不能包含类型未完全定义的字段,但可以包含指向前向声明类型的指针字段。
因此,工具从类型定义形成一个有向无环图(DAG),并找到拓扑排序。按照这个顺序检查类型时,可以确信任何字段的类型都已完全定义。
结构体的处理技巧
DWARF元数据提供了几条信息,可用于恢复其描述类型的C结构定义:
- 类型的大小
- 每个字段的类型
- 每个字段的偏移量
- 类型最初是结构体还是联合体
rellic-headergen的重建算法首先按偏移量递增顺序排序字段,然后定义一个新结构体来添加每个字段。元数据没有提供原始定义是否声明为打包的信息,因此rellic-headergen首先尝试直接按照元数据的指定生成布局。如果结果布局与输入不匹配,工具会重新开始并生成打包布局,根据需要手动插入填充。
结构体与继承
在处理更复杂的用例时,我遇到了一些ABI工作方式不明显的情况。例如,处理C++继承需要谨慎,因为简单的方法并不总是正确的。
将:
|
|
转换为:
|
|
似乎是个好主意,在实践中也有效,但这种方法扩展性不好。
此外,空结构体在C语言中技术上无效。它们可以通过GCC和Clang扩展使用,在C++中有效,但会带来问题:空结构体的sizeof永远不会是0,通常是1。
rellic-headergen的未来发展
rellic-headergen有进一步改进的机会。工具的目标之一是能够从经过优化的代码中恢复字段访问模式。
另一个值得探索的未来功能是改变输出语言的可能性:有时候你确实希望保留继承信息作为C++!
结语
虽然rellic-headergen目前适用范围很窄,但在处理C和C++代码库时已经非常强大,它能够为rellic本身提取类型信息,包括LLVM和Clang。在导航带有调试信息构建的二进制文件时,它已经提供了有用的见解,但扩展其功能集以能够从更多样化的代码库中提取信息,将使它在处理更大项目时更加有用。