LLVM设计架构
LLVM IR与GCC IR对比
特性 | LLVM IR | GCC IR (GIMPLE) |
---|---|---|
独立性和库化架构 | 高度模块化,前端和后端分离,易于添加新语言和目标平台 | 传统GCC架构,前端和后端耦合较紧密 |
表达形式 | 人类可读的汇编形式、C++对象形式、序列化后的bitcode形式 | GIMPLE表示形式,三地址代码,SSA形式 |
设计和应用 | 更独立,可在编译器之外的工具中重用,有正式定义和良好的C++ API,更接近硬件行为 | 降低控制流复杂度,优化相对容易 |
适用场景 | 适合学术界的应用,因为已经做了较大简化,可以更快地得出结果 | 适合工业应用,可以自己生成统一的AST进行数据流分析,或生成类似GIMPLE的三地址码进行分析 |
LLVM IR的优点包括:
- 更独立:LLVM IR设计为可以在编译器之外的任意工具中重用,使得可以轻松集成其他类型的工具,例如静态分析器和插桩器。
- 更正式的定义和更好的C++ API:这使得处理、转换和分析变得更加容易。
- 更接近硬件行为:LLVM IR提供了类似RISCV的模拟指令集和强类型系统,实现了其“通用表示”的目的。足够底层的指令和细粒度的类型使得上层语言和IR的隔离变得容易,同时IR的行为更加贴近硬件,使得在LLVM IR上的进一步分析成为可能。
GIMPLE的优点:
- 降低控制流复杂度:GIMPLE通过降低控制流复杂度、采用三地址表示和限制语法,使得优化变得相对容易。
LLVM架构设计
首先我们以C,C++,ObjC高级语言作为输入,输入到Clang前端,Clang前端会对这些高级语言做一些词法分析、语法分析、语义分析等操作,生成抽象语法树(AST)。然后Clang会将AST转换为LLVM的中间表示(IR)。
LLVM的中间表示(IR)连接前端和后端。然后LLVM的优化器会对IR进行优化,优化器会将IR转换为更高效的IR。最后,LLVM的后端会将IR转换为目标平台相关的汇编代码,并生成可执行文件。
LLVM架构图:
在详细的架构图中,我们可以看到LLVM的前端、优化器、后端等各个组件的交互。在前端,Clang会将高级语言代码转换为为LLVM的中间表示(IR)。
在中间的编译优化层中,LLVM会有非常多的pass,不同的pass会对IR进行不同的优化处理,将IR转换为更高效的IR。对IR的优化完成之后,编译器还是会以LLVM的中间表示(IR)作为输出,然后LLVM的后端会将IR转换为目标平台相关的机器码。
在后端中LLVM会进行指令选择,选择适合特定机器架构的指令。接下来,会进行寄存器分配,为指令分配合适的寄存器。然后,进行指令调度,优化指令执行顺序以提高性能。之后经过指令调度,会对代码进行布局,调整代码的排列顺序以适应目标硬件的执行特性。最后,会进行代码组装,生成目标硬件对应的汇编指令。
LLVM核心流程分析
编译器前端工作流程(词法、语法、语义分析) 中间优化层大数据中的Pass优化 编译器后端工作流程(机器指令选择、寄存器分配、指令调度)
在使用LLVM(Low-Level Virtual Machine)时,编译过程从原始的C代码开始,经过多个步骤最终生成可执行文件。以下是这个过程的具体步骤:
- 预处理
- 原始C代码经过预处理,移除所有注释、执行宏替换等。
- 生成中间表示(IR)
- 预处理后的代码被编译成LLVM的中间表示(IR),有两种格式:
- .ll 文件:LLVM汇编语言格式,更易于阅读和理解。
- .bc 文件:LLVM字节码格式,一种二进制表示。
- 预处理后的代码被编译成LLVM的中间表示(IR),有两种格式:
- 编译IR为汇编代码
- 使用LLVM后端编译器工具 llc 将IR转换为目标特定机器的汇编代码。
- 汇编代码转换为机器码
- 汇编代码通过汇编器转换为机器码,生成目标平台的 object 文件。
- 链接
- 将多个 object 文件和库链接在一起,生成最终的可执行二进制文件。
实践:Clang编译流程
生成.i文件
clang -E -c .\hello.c -o .\hello.i
将预处理过后的.i文件转化为.bc文件
clang -emit-llvm .\hello.i -c -o .\hello.bc
clang -emit-llvm .\hello.c -S -o .\hello.ll
llc
lld链接器子项目旨在为LLVM开发一个内置的,平台独立的链接器[23],去除对所有第三方链接器的依赖。在2017年5月,lld已经支持ELF、PE/COFF、 和Mach-O。在lld支持不完全的情况下,用户可以使用其他项目,如GNU ld链接器。 lld支持链接时优化。当LLVM链接时优化被启用时,LLVM可以输出bitcode而不是本机代码,而本机代码生成由链接器优化处理。
llc .\hello.ll -o .\hello.s
llc .\hello.bc -o .\hello2.s
此时hello.s与hello2.s是相同的汇编代码
转变为可执行的二进制文件
clang .\hello.s -o hello
查看编译过程
clang -ccc-print-phases .\hello.c
+- 0: input, ".\hello.c", c
+- 1: preprocessor, {0}, cpp-output
+- 2: compiler, {1}, ir
+- 3: backend, {2}, assembler
+- 4: assembler, {3}, object
+-5: linker, {4}, image
6: bind-arch,"x86_64", {5}, image
其中0是输入,1是预处理,2是编译,3是后端优化,4是产生汇编指令,5是库链接,6是生成可执行的x86_64二进制文件。
总结
对于LLVM来说把编译器移植给新的语言只需要实现一个新的编译前端,已有的优化和后端都能实现复用;
LLVM组件之间交互发生在高层次抽象,不同组件隔离为单独程序库,易于在整个编译流水线中集成转换和优化Pass。现在被作为实现各种静态和运行时编译语言的通用基础结构;
在接下来的小节中,我们将对LLVM的IR、前端、优化、后端、编译过程等进行详细的介绍。