【译】LLVM 类型相等判断
文章目录
在 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 会重命名其中一个,来避免命名冲突。
|
|
如果想消除重复的数据类型,明显的解决方法是:判断有相似名字的结构体,它们的实现是否相同。
注:直接去掉 LLVM 给名字加上的数字后缀,然后判断名字是否相等是不可行的,因为名字冲突的两个结构体,有可能有不同的实现体。
而 llvm::StructType::isLayoutIdentical
判断类型等价的操作,是基于指针的。导致会出现下面代码中的判断情况。
|
|
说明这种方式也是不可行的。
2.1、Primitive Types Equality 原始类型等价判断
在 LLVM 当中,类型都归 LLVMContext
管理。原始类型,如 int32
, float
, double
是提前分配好,然后再重复使用。在 LLVMContext
的同一个上下文当中,你只能创建一种原始类型的一个实例。有了这个前提条件,很容易就能判断类型是否等价,直接比较它们的指针就够了。
但是如果是来自于不同的上下文,就不能这么比较了。比如,来自一个 LLVMContext
的 int32
类型,与来自另一个 LLVMContext
的 int32
类型。
2.2、Struct Types Equality 结构体类型等价判断
对于结构体类型,判断是否等价就更加复杂了。
|
|
llvm::StructType::isLayoutIdentical
通过指针来进行类型比较,只有结构体里的类型元素都相等,才返回 true。
2.3、IRLinker/llvm-link
LLVM 通过 IRLinker
类来合并两个模块。LLVM 还提供了它的命令行工具 llvm-link
。 IRLinker
运作正常,但它会丢掉一些重要信息。
下面是运行 IRLinker
的 IR 的例子:
|
|
然后变成
|
|
另一个结构体被丢掉了(优化掉)了,因为它们有相同的实现体。但我们并不想丢失这个信息。
还有, 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)
->I32
-
i177
->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
和 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。
对于不明确类型结构体的等价判断,同样采用符号化的名字,如果不明确类型的原始名字是一样的,那么它们就会有相同的符号名。
|
|
这里的例子中,不明确类型的原始名字是 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
文章作者 calssion
上次更新 2021-07-24