在 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 会重命名其中一个,来避免命名冲突。

1
2
3
4
5
6
7
8
9
; 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 判断类型等价的操作,是基于指针的。导致会出现下面代码中的判断情况。

1
2
3
4
5
6
7
; 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 的同一个上下文当中,你只能创建一种原始类型的一个实例。有了这个前提条件,很容易就能判断类型是否等价,直接比较它们的指针就够了。

但是如果是来自于不同的上下文,就不能这么比较了。比如,来自一个 LLVMContextint32 类型,与来自另一个 LLVMContextint32 类型。

2.2、Struct Types Equality 结构体类型等价判断

对于结构体类型,判断是否等价就更加复杂了。

1
2
3
4
5
6
7
; 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。

LLVM 通过 IRLinker类来合并两个模块。LLVM 还提供了它的命令行工具 llvm-linkIRLinker 运作正常,但它会丢掉一些重要信息。

下面是运行 IRLinker 的 IR 的例子:

1
2
%struct.Point = type { i32, i32 }
%struct.Tuple = type { i32, i32 }

然后变成

1
%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(,,,,)

ab 是常量, 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。我们需要用它来判断递归类型的结构体是否等价:

1
2
3
%list = type { %list*, i32 }
%node = type { %node*, i32 }
%root = type { %node*, i32 }

上面的结构体都有相同的实现:一个指针 + 一个整型。但我们不会判断它们都是等价的,根据等价的定义,得出下面的结果:

1
2
3
list == node
root != node
root != list

原因很简单,因为 listnode 有相同的实现体和相同的递归结构,而 root 是其他的递归结构。

所以需要把递归结构也纳入判断之中,但我们不使用递归结构的名字。在构建树之前,我们给它们赋值一个符号名或者 ID。

所以 listnode 会被定义成 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)) ,在这里 S0S1 是结构体 ID。

3.4、Opaque Struct Equality 不明确类型结构体等价判断

注:LLVM 3.0 的 new type system 已经去掉了这个 opaque struct。

对于不明确类型结构体的等价判断,同样采用符号化的名字,如果不明确类型的原始名字是一样的,那么它们就会有相同的符号名。

1
2
3
4
5
6
7
%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.BA是不等价的。即使这 3 个结构体都能指向相同的类型或者不同的类型。

参考

TYPE EQUALITY IN LLVM:https://lowlevelbits.org/type-equality-in-llvm/

llvm::LLVMContext:https://llvm.org/doxygen/classllvm_1_1LLVMContext.html