LLVM IR转换

强大的现代编译器设计

Posted by 叉叉敌 on May 17, 2022

什么是 LLVM IR

  • ⽆限制的单分配寄存器机器指令集
  • 强类型
  • 三种常⻅表⽰形式: 1, ⼈类可读的 LLVM 程序集(.ll ⽂件) 2, 密集的 bitcode ⼆进制表⽰(.bc ⽂件) 3, C++ 类

SSA

Static Single Assignment, 我理解的是单一静态赋值,寄存器可能只分配一次.

与之对应的就是多赋值,下面就是一个变量a,被赋值2次.

int a = somefunction();
a++;

既然是2次,那下面的%a 用这样表示.

%a = call i32 @somefunction()
%a = add i32 %a, 1

用llc编译的时候,看到会报错的,

error: multiple definition of local value named ’a’
%a = add i32 %a, 1

正确的应该是,2个不同的变量,或者标识,不一定是%a

%a = call i32 @somefunction()
%a2 = add i32 %a, 1

前端必须跟踪哪个寄存器在代码的任何一位置保存的值.

上面的代码是如何跟踪到新的值, 分号是注释, 下面的%0,1,2必须是连续的,中间少了一个会提示不连续.

; int a
%a = alloca i32 , align 4
; a = somefunction
%0 = call i32 @somefunction()
store i32 %0, i32* %a
; a++
%1 = load i32 * %a
%2 = add i32 %1, 1
store i32 %2, i32* %a

寄存器的值是自动分配,上面的表达式是自动llc等翻译的,不过也可以手动写, 可以看出在llvm内存中,不是SSA.

上面看出来有2次store,实际中好像有点冗余,SROA或者寄存器会自动清除这些冗余的步骤,

SROA:scalar replacement of aggregates 如果可能的话,这个转换将聚合类型(结构或数组)的 alloca 指令分解为每个成员的单个 alloca 指令。然后,如果可能的话,它将单个 alloca 指令转换为简洁的纯标量 SSA 形式.

就是下面的样子.


%0 = call i32 @someFunction()
%1 = add i32 %0, 1

控制流

IR的控制流通过jump、branches来实现. 分支为条件和无条件分支,

条件

先来看看条件

int b = 12;
if (a)
    b++;
return b;

控制流请求一个基础的块,每一次循环路径, 当循环路径指定的时候,就执行条件分支.

φ -phi, 念/fi/, φ 节点是SSA构造中使⽤的特殊指令.

IR的入口大概是下面这个样子,可以看到最后一行有2个标识符, br 是branch,lable是跳转到这个命名的块.分别为then和end

entry:
; int b = 12
%b = alloca i32
store i32 12, i32* %b
; if (a)
%0 = load i32 * %a
%cond = icmp ne i32 %0, 0
br i1 %cond , label %then , label %end

then块的内容如下,调用了end块,负责b++

then:
; b++
%1 = load i32 * %b
%2 = add i32 %1, 1
store i32 %2, i32* %b
br label %end

end块的内容如下,return b

end:
; return b
%3 = load i32 * %b
ret i32 %3

加入φ之后,就成了这样了.

简化后的CFG, 就一个select指令.

entry:
%tobool = icmp ne i32 %a, 0
%0 = select i1 %tobool , i32 13, i32 12
ret i32 %0

函数

LLVM函数包含至少一个基础块,

@hello = private constant [13 x i8] c"Hello world!\00"
define i32 @main(i32 %argc , i8 ** %argv) {
entry:
%0 = getelementptr [13 x i8 ]* @hello , i32 0 ,
i32 0
call i32 @puts(i8* %0)
ret i32 0
}

IR如何转化为本地code

Selection DAG

1, 转换(SelectionDAG) 2, 映射到指令(Machine IR) 3, 流到汇编

Instruction Selection

Register allocation

MC Streamer

LLVM重要的几个类

Module

是所有其他 LLVM 中间表示(IR)对象的顶级容器。每个模块直接包含全局变量列表、函数列表、该模块所依赖的库(或其他模块)列表、符号表以及关于目标特征的各种数据。

Function

函数基本上由一个基本块列表、一个参数列表,和一个符号表。

BasicBlock

这表示 LLVM 中的一个基本块。基本块仅仅是一个指令的容器,这些指令按顺序执行。基本块是Value,因为它们被诸如branch和switch表之类的指令引用。BasicBlock 的类型是“ Type::Label_type”,因为基本块表示分支可以跳转到的标签。

GlobalVariable

全局变量是常量指针,指向一个大空间,由 VM 或静态编译器中的链接器分配一个全局variable 可能有一个初始值,这个初始值被复制到 executables. dataarea. 需要全局常量才能有初始化器。

IRBuilder

这提供了一个统一的 API,用于创建指令并将它们插入到基本块中: 或者在 BasicBlock 的末尾,或者在块中的特定迭代器位置。

Type

Type 类的实例是不可变的: 它们一旦被创建,就不会被更改。

ConstantExpr

用表达式使用其他常量值初始化的常量值。

PassManagerBuilder

这个类用于为 c 和 c++ 等语言建立一个标准的优化序列,允许一些 api 以各种方式自定义传递序列

ExecutionEngine

用于执行 LLVM 模块的抽象接口,设计用于支持解释器和实时(JIT)编译器实现。

Read more

https://llvm.org/doxygen/classllvm_1_1Module.html

https://www.cnblogs.com/Five100Miles/p/12824942.html

https://zhuanlan.zhihu.com/p/52724656

4LLVMIRandTransformPipeline.pdf

github博客

微信公众号:cdtfug, 欢迎关注一起吹牛逼,也可以加微信号「xiaorik」朋友圈围观。