LLVM CookBook 阅读笔记 —— LLVM设计与使用
更新中。。。
一、概述
本章内容:
- LLVM的设计理念
- 如何把C语言代码编译为LLVM IR(Intermediate Representation——中间码)
- 链接bitcode文件并运行
- C语言前端-Clang
二、预备知识
-
LLVM代码的3种表示形式:内存编译器中的IR、存于磁盘的bitcode,以及用户可读的汇编码
- LLVM IR是基于静态单赋值(Static Single Assignment——SSA,简单理解就是一个变量只能被赋值一次)的,并且提供了类型安全性、底层操作性、灵活性,能够表达绝大多数高级语言。
-
Pass(优化)
- LLVM优化器为用户提供了不同的优化Pass,对每个Pass的源码编译,得到一个Object文件,之后这些不同的文件再链接得到一个库。Pass之间耦合很小,而Pass之间的依赖信息由LLVM Pass管理器(PassManager)来统一管理,在Pass运行的时候会进行解析。
-
交叉编译
- 所谓交叉编译,指的是我们能够在一个平台(例如x86)编译并构建二进制文件,而在另一个平台(例如ARM)运行。编译二进制文件的机器称为主机(host),而运行生成的二进制文件的平台我们称为目标平台(target)。为相同平台(主机与目标机器相同)编译代码我们称为本机编译(native assembler),而当主机与目标机器为不同平台时编译代码则称为交叉编译(cross-compiler)。
三、LLVM的设计理念 ——模块化设计
LLVM的设计目标是成为一系列的库。例如,与其他编译器不同的是,LLVM的优化器(optimizer)就是一个库的概念,它支持自主选择Pass的执行与否、控制Pass的执行顺序。
四、将C源码转换为LLVM IR
将C语言代码编译为LLVM IR的过程从词法分析开始——将C语言源码分解成token流,每个token可表示标识符、字面量、运算符等;token流会传递给语法分析器,语法分析器会在语言的CFG(Context Free Grammar,上下文无关文法)的指导下将token流组织成AST(抽象语法树);接下来会进行语义分析,检查语义正确性,然后生成IR。
- 示例:
$ cat multiply.c
int mult() {
int a =5;
int b = 3;
int c = a * b;
return c;
}
$ clang -emit-llvm -S multiply.c -o multiply.ll
; ModuleID = 'multiply.c'
source_filename = "multiply.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @mult() #0 {
entry:
%a = alloca i32, align 4
%b = alloca i32, align 4
%c = alloca i32, align 4
store i32 5, i32* %a, align 4
store i32 3, i32* %b, align 4
%0 = load i32, i32* %a, align 4
%1 = load i32, i32* %b, align 4
%mul = mul nsw i32 %0, %1
store i32 %mul, i32* %c, align 4
%2 = load i32, i32* %c, align 4
ret i32 %2
}
attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 10.0.0 (Linx SERVER LLVM V000R000C00 8b84d86556fc)"}
可以使用 llvm-as[1] 将LLVM IR可以进一步转换成bitcode[2],命令如下:
llvm-as test.ll –o test.bc
还可以使用LLVM静态编译器llc[3]将bitcode转换为目标平台汇编码,命令如下:
llc test.bc –o test.s
或者通过Clang从bitcode文件格式生成汇编码,命令如下:
clang -S test.bc -o test.s –fomit-frame-pointer
在以上命令中加入-march=architechture参数,可以生成特定目标架构的汇编码。使用-mcpu=cpu参数则可以指定其CPU,而-regalloc =basic/greedy/fast/pbqp则可以指定寄存器分配类型。
我们同样可以使用反汇编工具llvm-dis[4],将bitcode转回为LLVM汇编码,命令如下:
llvm-dis test.bc –o test.ll
我们还可以使用opt工具[5] (LLVM的优化器,需要安装),对LLVM IR进行优化,命令格式如下(可以通过opt --help来查看opt的选项):
opt –passname input.ll –o output.ll
例如对示例中的IR进行转换,优化内存访问(将局部变量从内存提升到寄存器):
opt -mem2reg -S multiply.ll -o multiply1.ll
常见的opt参数包括:
adce:入侵式无用代码消除。
bb-vectorize:基本块向量化。
constprop:简单常量传播。
dce:无用代码消除。
deadargelim:无用参数消除。
globaldce:无用全局变量消除。
globalopt:全局变量优化。
gvn:全局变量编号。
inline:函数内联。
instcombine:冗余指令合并。
licm:循环常量代码外提。
loop-unswitch:循环外提。
loweratomic:原子内建函数lowering。
lowerinvoke:invode指令lowering,以支持不稳定的代码生成器。
lowerswitch:switch指令lowering。
mem2reg:内存访问优化。
memcpyopt:MemCpy优化。
simplifycfg:简化CFG。
sink:代码提升。
tailcallelim:尾调用消除。
五、链接LLVM bitcode
使用工具链中的llvm-link[6]可以实现.bc文件的链接。
为了展示llvm-link的功能,首先在不同文件中编写两段代码,其中一个引用另一个:
test1.c:
int func(int a) {
a = a*2;
return a;
}
test2.c:
#include<stdio.h>
extern int func(int a);
int main() {
int num = 5;
num = func(num);
printf("number is %d\n", num);
return num;
}
$ clang -emit-llvm -S test1.c -o test1.ll
$ clang -emit-llvm -S test2.c -o test2.ll
$ llvm-as test1.ll -o test1.bc
$ llvm-as test2.ll -o test2.bc
$ llvm-link test1.bc test2.bc –o output.bc
对于得到的output.bc,可以使用lli[7]工具进行执行:
$ lli output.bc

六、C语言前端——Clang
Clang可以用作高层编译器驱动。
示例:
备注
如果想深入了解上文提到的pass以及所有工具具体的工作流程,可以借助gdb工具进行调试和源码阅读。
llvm-as即是LLVM的汇编器。它会将LLVM IR转为bitcode(就像把普通的汇编码转成可执行文件)。 ↩︎
LLVM bitcode(也称为字节码——bytecode)由两部分组成:位流(bitstream,可类比字节流),以及将LLVM IR编码成位流的编码格式。可以使用hexdump文件来查看bitcode文件(eg: hexdump -C case.bc) ↩︎
llc命令把LLVM 输入编译为特定架构的汇编语言,如果我们在之前的命令中没有为其指定任何架构,那么默认生成本机的汇编码,即调用llc命令的主机。 ↩︎
llvm-dis命令即是LLVM反汇编器,它使用LLVM bitcode文件作为输入,输出LLVM IR。如果省略文件名,llvm-dis工具会从标准输入读取输入。 ↩︎
opt是LLVM的优化和分析工具,采用input.ll文件作为输入,并且按照passname执行Pass。在执行Pass之后的输出存于output.ll文件,其中包含转换后的IR代码。opt工具可以使用多个Pass。如果给opt工具传入-analyze参数,它会在输入源码上执行不同的分析,并且在标准输出流或错误流打印分析结果。 ↩︎
llvm-link工具的功能和传统的链接器一致:如果一个函数或者变量在一个文件中被引用,却在另一个文件中定义,那么链接器就会解析这个文件中引用的符号。但和传统的链接器不同,llvm-link不会链接Object文件生成一个二进制文件,它只链接bitcode文件。 ↩︎
lli工具命令执行LLVM bitcode格式程序,它使用LLVM bitcode格式作为输入并且使用即时编译器(JIT)执行。当然,如果当前的架构不存在JIT编译器,会用解释器执行。如果lli能够采用JIT编译器,那么它能高效地使用所有代码生成器参数,如llc。 ↩︎

浙公网安备 33010602011771号