1.概述
llvm是一个由若干工具(汇编器、编译器和调试器等)所组成的工具集合,并被设计为与Unix系统上现有的工具兼容。尽管llvm有很多独特的功能,并且有一些伟大的工具(例如,Clang编译器,在很多方面优于gcc)。但是llvm最与众不同的还是他的内部结构。从2000年12月它的诞生之日起,llvm的设计目标就是具有良好定义接口的可重用的库的集合。而很多开源软件,例如gcc,被实现为目的单一的一个巨大的可执行程序。例如,很难重用gcc的语法分析器来做静态分析或者反射(refactoring)。
除了编译器的结构之外,围绕语言的社区也呈两极化。要么提供传统的静态编译器如gcc、FreePascal和FreeBASIC等,要么以解释器或者JIT的形式提供一个运行时的编译器。同时支持两者的非常少见。过去的十年里,llvm改变了这种状况。llvm被用作编译广泛的静态或者动态语言(例如,gcc、java、.NET、Python、Ruby、Scheme、Haskell等支持的以及其他一些很少见的语言)的通用架构。
2.现存的编译框架
为了方便移植与重用,编译器的理想架构如下:
这样模块化的结构下,N个前端和M个后端,就可以达到非模块化结构的N*M种编译器。
这种模块化的结构有三个成功案例。第一个是Java,.NET等虚拟机。他们提供了JIT编译器,运行时(runtime)和良好定义的字节码格式。任何程序只要能转换为字节码格式,就能狗利用虚拟机提供的各种功能,包括JIT和运行时等。其缺点就是,其运行时提供的灵活性不够,强制实施JIT编译和垃圾收集,并且使用了一种特殊的模型,当处理类似C这样的不太适合与该模型的语言时,就有性能上的折扣。
第二个成功的案例也许是不幸,但也是被广泛使用的重用编译器的方法:把输入的源程序转化为C代码,然后输入到现存的C编译器中。这种方法能够重用优化和代码生成机制,有比较好的灵活性,并且能够控制运行时,易于为前端开发者所理解、实施和维护。但是不幸的是,这种做法不能有效实施异常处理,不利于调试,并且减缓了编译速度,对于那些具有C不支持的特性的语言来说,还可能出现问题。
最后一个成功的案例是GCC4。GCC支持很多前端和后端,并且有一个庞大而活跃的开发团队。GCC在很长时间里是充当C语言的编译器,支持若干不同的目标机器。随着时间推移,GCC社区的设计更加清晰,例如在GCC4.4中,就有一个供优化使用的中间表示(GIMPLE),跟以前相比,其与前端的联系更加松散。Fortran和Ada使用的是简单的AST。
尽管非常成功,这三种方法都有局限性。他们都是被设计成一个单一的应用程序。例如很难把GCC嵌入到其他的应用中,或者重用并提取片段的GCC代码。而LLVM则具有清晰良好的设计,使得其很容易被重用。
3.LLVM的中间表示与简单使用
LLVM的中间表示为bitcode,通常后缀为bc。以简单的hello world程序为例。
#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}
可以直接把它编译为本地的可执行文件,命令为
clang hello.c -o hello
clang是llvm编译器前端的名字,也是其整个编译器的驱动,类似与gcc。
也可以把它编译成为一个bc格式的文件,使用的编译选项为-emit-llvm -c,如下
clang -emit-llvm -c hello.c -o hello.bc
bc格式的也可以执行,执行命令如下:
lli hello.bc
如果要想生成bc的文本格式(后缀为ll),使用的命令是-emit-llvm -S
clang -emit-llvm -S hello.c -o hello.ll
bc文件也可以被转换为本地的汇编码,命令为:
llc hello.bc -o hello.s
生成汇编码之后,可以使用gcc完成链接并执行:gcc hello.s -o hello.native
./hello.native
另外,llvm-as可以把.ll格式的文件转化为.bc,llvm-dis可以把.bc文件转化为ll。
llvm的中间(IR)表示是其最重要的部分,IR可以支持编译器优化部分的分析和转换,包括轻量级的运行时优化、过程间优化、全程序(whole program analysis)优化以及激进的重构优化。最为重要的是,它具有一个良好定义的语义。如下所示。
define i32 @add1(i32 %a, i32 %b) {
entry:
%tmp1 = add i32 %a, %b
ret i32 %tmp1
}
define i32 @add2(i32 %a, i32 %b) {
entry:
%tmp1 = icmp eq i32 %a, 0
br i1 %tmp1, label %done, label %recurse
recurse:
%tmp2 = sub i32 %a, 1
%tmp3 = add i32 %b, 1
%tmp4 = call i32 @add2(i32 %tmp2, i32 %tmp3)
ret i32 %tmp4
done:
ret i32 %b
}
===========================================================
unsigned add1(unsigned a, unsigned b) {
return a+b;
}
// Perhaps not the most efficient way to add two numbers.
unsigned add2(unsigned a, unsigned b) {
if (a == 0) return b;
return add2(a-1, b+1);
}
上面的是中间表示,下面的是对应的C代码。从中可以看出,LLVM IR是类是RISC的虚拟指令集,指令为三地址格式,很像是汇编程序。但是与机器对应的汇编程序不同的是:1) 其IR是有类型的,i32表示32位的整型值;2) call和ret的细节被抽象掉了,只是跟了参数,接近于高级语言的形式;3) 有无穷多个寄存器,使用“%”表示。