在 LLVM 的类型系统中,进行两个类型是否等价的判断,对于做代码分析而言是很重要的,本文将介绍遇到的难题以及解决的方法。
内容来源: https://lowlevelbits.org/type-equality-in-llvm/
原文标题:TYPE EQUALITY IN LLVM
作者:AlexDenisov
译者:流左沙
注:翻译较原文会有一定的精简、重排和添删,主要是为了提取重要内容以达到更好的理解,请以原文为准。
1、LLVM 类型系统的简述
关于 LLVM 类型系统的背景,可以看我之前写的一篇译文或者原文,类型系统在本文不再过多展开。
简单再概述一下:
LLVMContext管理 LLVM‘s core infrastructure 的 core “global” data,包括类型和常量的表- 类型的实例会被分配到堆内存上(如
llvm::Type *type = new llvm::Type;) - 类型的比较是通过指针的比较来完成的
- LLVM 类型分成三种:原始类型(primitive types)、派生类型(derived types)、提前声明类型(forward-declared types)
2、问题的产生
如果两个模块(modules)使用了有相同名字的相同结构体,那么他们被加载到同一个上下文(context,内容)时,LLVM 会重命名其中一个,来避免命名冲突。
; Module1
%struct.Point = type { i32, i32 }
; Module 2
%struct.Point = type { i32, i32 }
; same context
%struct.Point = type { i32, i32 }
%struct.Point.1 = type { i32, i32 }
如果想消除重复的数据类型,明显的解决方法是:判断有相似名字的结构体,它们的实现是否相同。
注:直接去掉 LLVM 给名字加上的数字后缀,然后判断名字是否相等是不可行的,因为名字冲突的两个结构体,有可能有不同的实现体。
而 llvm::StructType::isLayoutIdentical 判断类型等价的操作,是基于指针的。导致会出现下面代码中的判断情况。
; these two have identical layout 判断为相同
%Point = type { i32, i32 }
%Pair = type { i32, i32 }
; these two DO NOT have identical layout 判断为不同
%PointWrap = type { %Point }
%PairWrap = type { %Pair }
说明这种方式也是不可行的。
2.1、Primitive Types Equality 原始类型等价判断
在 LLVM 当中,类型都归 LLVMContext 管理。原始类型,如 int32, float, double 是提前分配好,然后再重复使用。在 LLVMContext 的同一个上下文当中,你只能创建一种原始类型的一个实例。有了这个前提条件,很容易就能判断类型是否等价,直接比较它们的指针就够了。
但是如果是来自于不同的上下文,就不能这么比较了。比如,来自一个 LLVMContext 的 int32 类型,与来自另一个 LLVMContext 的 int32 类型。
2.2、Struct Types Equality 结构体类型等价判断
对于结构体类型,判断是否等价就更加复杂了。
; these two have identical layout 判断为相同
%Point = type { i32, i32 }
%Pair = type { i32, i32 }
; these two DO NOT have identical layout 判断为不同
%PointWrap = type { %Point }
%PairWrap = type { %Pair }
llvm::StructType::isLayoutIdentical通过指针来进行类型比较,只有结构体里的类型元素都相等,才返回 true。
2.3、IRLinker/llvm-link
LLVM 通过 IRLinker类来合并两个模块。LLVM 还提供了它的命令行工具 llvm-link。 IRLinker 运作正常,但它会丢掉一些重要信息。
下面是运行 IRLinker 的 IR 的例子:
%struct.Point = type { i32, i32 }
%struct.Tuple = type { i32, i32 }
然后变成
%struct.Point = type { i32, i32 }
另一个结构体被丢掉了(优化掉)了,因为它们有相同的实现体。但我们并不想丢失这个信息。
还有, IRLinker 还可能会引入源码级别没有出现的类型。
3、Solution 解决方法
解决方法的灵感来源是 Tree Automata(树自动机) 和 Ranked Alphabets(字母序列)。
字母序列(ranked alphabet) 包含有符号 F的有限集合, 以及一个算术函数 Arity(f), 而 f 属于 F的集合。
算术函数 Arity 表示符号 f 有多少个参数。符号可以是 constant(常量), unary(一元运算符), binary(二元), ternary(三元), or n-ary.
比如: a, b, f(,), g(), h(,,,,)。
a 和 b 是常量, f(,) 是二元运算, g() 是一元运算, 还有 h(,,,,) 是 n 元。 这些符号的参数数量分别是 0, 0, 2, 1, 5。
3.1、construction 构造
给定字母表 a, b, f(,), g() 我们可以构造以下这些树:
- f(a, b)
- g(b)
- g(f(b, b))
- f(g(a), f(f(a, a), b))
- f(g(a), g(f(a, a)))
- 等等
如果我们知道每个符号的参数数量,那么我们可以生成圆括号和逗号,来将一棵树序列化成一个字符串。这棵树以深度优先算法来构造。比如下面这些序列串:
- fab
- gb
- gfbb
- fgaffaab
- fgagfaa
下面是更容易理解的对应的例子:

绿色箭头表示深度优先的遍历顺序。
所以我们可以将类型问题映射成 ranked alphabet/tree automaton 的概念。
3.2、Type Equality 类型等价判断
我们把每种类型视为一个符号,而它的参数数量则是类型的属性,这正是我们要比较的。然后,我们创建类型的树,把它转换成字符串序列。如果两种类型有相同的字符串序列,那么它们就是等价的。
比如:
i32,i64,i156: 符号是I, 参数数量是 1,因为我们只在乎它的位宽 (32, 64, 156)float: 符号是F, 参数数量是 0, 所有float类型都是一样的[16 x i32]: 符号是A, 参数数量是 2, 我们只在乎数组长度和元素类型i8*: 符号是P, 参数数量是 1, 我们只在乎指针的类型{ i32, [16 x i8], i8* }: 符号是S, 参数数量是 元素数量 + 2。我们要存储结构体的 ID 和元素数量
我们可以把这些类型转为序列化串:
i32->I(32)->I32i177->I(177)->I177[16 x i8*]->A(16, P(I(8)))->A16PI8{ i32, i8*, float }->S(3, S0, I(32), P(I(8)), F)->S3S0I32PI8F
注: S 的值分别是,元素数量 (3), 结构体 ID (S0), 以及所有它所包含的类型(递归地展开)。

3.3、Structural Equality 结构体等价判断
上面有个概念 struct ID。我们需要用它来判断递归类型的结构体是否等价:
%list = type { %list*, i32 }
%node = type { %node*, i32 }
%root = type { %node*, i32 }
上面的结构体都有相同的实现:一个指针 + 一个整型。但我们不会判断它们都是等价的,根据等价的定义,得出下面的结果:
list == node
root != node
root != list
原因很简单,因为 list 和 node 有相同的实现体和相同的递归结构,而 root 是其他的递归结构。

所以需要把递归结构也纳入判断之中,但我们不使用递归结构的名字。在构建树之前,我们给它们赋值一个符号名或者 ID。
所以 list 和 node 会被定义成 S(2, S0, P(S(2, S0, x, x), I(32)) ,而 S0 是结构体 ID。当已经生成过递归结构的类型时,为了使递归能够终止,我们使用了符号 x 来代替。
而 root 被定义成 S(2, S0, P(S(2, S1, P(S(2, S1, x, x), I(32), I(32))), I(32)) ,在这里 S0 和 S1 是结构体 ID。
3.4、Opaque Struct Equality 不明确类型结构体等价判断
注:LLVM 3.0 的 new type system 已经去掉了这个 opaque struct。
对于不明确类型结构体的等价判断,同样采用符号化的名字,如果不明确类型的原始名字是一样的,那么它们就会有相同的符号名。
%struct.A = type opaque
%struct.A.0 = type opaque
%struct.B = type opaque
%foo = type { %struct.A* }
%bar = type { %struct.A.0* }
%buzz = type { %struct.B* }
这里的例子中,不明确类型的原始名字是 A (%struct.A, %struct.A.0) 和 B (%struct.B)。因此我们把 %struct.A 和 %struct.A.0 视为等价的,而 %struct.B 和 A是不等价的。即使这 3 个结构体都能指向相同的类型或者不同的类型。
参考
TYPE EQUALITY IN LLVM:https://lowlevelbits.org/type-equality-in-llvm/
llvm::LLVMContext:https://llvm.org/doxygen/classllvm_1_1LLVMContext.html
本文探讨了在LLVM中如何判断类型等价的问题,尤其是在处理结构体类型时遇到的困难。由于LLVM在合并模块时可能导致类型重命名,简单的指针比较或布局检查无法准确判断类型是否相同。解决方案是通过建立类型树并将其转换为序列化的字符串,结合结构体ID来判断类型等价性,尤其是对于递归结构体和不明确类型结构体的情况。这种方法利用了树自动机和字母序列的概念,确保了类型等价性的正确判断。
1万+

被折叠的 条评论
为什么被折叠?



