为什么人人都该懂点LLVM

只要你和程序打交道,了解编译器架构就会令你受益无穷——无论是分析程序效率,还是模拟新的处理器和操作系统。通过本文介绍,即使你对编译器原本一知半解,也能开始用LLVM,来完成有意思的工作。

LLVM是什么?

LLVM是一个好用、好玩,而且超前的系统语言(比如C和C++语言)编译器。

当然,因为LLVM实在太强大,你会听到许多其他特性(它可以是个JIT;支持了一大批非类C语言;还是App Store上的一种新的发布方式等等)。这些都是真的,,不过就这篇文章而言,还是上面的定义更重要。

下面是一些让LLVM与众不同的原因:

为什么人人需要懂点儿LLVM?

是,LLVM是一款酷炫的编译器,但是如果不做编译器研究,还有什么理由要管它?

答:只要你和程序打交道,了解编译器架构就会令你受益,而且从我个人经验来看,非常有用。利用它,可以分析程序要多久一次来完成某项工作;改造程序,使其更适用于你的系统,或者模拟一个新的处理器架构或操作系统——只需稍加改动,而不需要自己烧个芯片,或者写个内核。对于计算机科学研究者来说,编译器远比他们想象中重要。建议你先试试LLVM,而不用hack下面这些工具(除非你真有重要的理由):

就算一个编译器不能完美地适合你的任务,相比于从源码到源码的翻译工作,它可以节省你九成精力。

下面是一些巧妙利用了LLVM,而又不是在做编译器的研究项目:

重要的话说三遍:LLVM不是只用来实现编译优化的!LLVM不是只用来实现编译优化的!LLVM不是只用来实现编译优化的!

组成部分

LLVM架构的主要组成部分如下(事实上也是所有现代编译器架构):

前端,流程(Pass),后端

下面分别来解释:

虽然当今大多数编译器都使用了这种架构,但是LLVM有一点值得注意而与众不同:整个过程中,程序都使用了同一种中间表示。在其他编译器中,可能每一个流程产出的代码都有一种独特的格式。LLVM在这一点上对hackers大为有利。我们不需要担心我们的改动该插在哪个位置,只要放在前后端之间某个地方就足够了。

开始

让我们开干吧。

获取LLVM

首先需要安装LLVM。Linux的诸发行版中一般已经装好了LLVM和Clang的包,你直接用便是。但你还是需要确认一下机子里的版本,是不是有所有你要用到的头文件。在OS X系统中,和XCode一起安装的LLVM就不是那么完整。还好,用CMake从源码构建LLVM也没有多难。通常你只需要构建LLVM本身,因为你的系统提供的Clang已经够用(只要版本是匹配的,如果不是,你也可以自己构建Clang)。

具体在OS X上,Brandon Holt有一个不错的指导文章。用Homebrew也可以安装LLVM。

去读手册

你需要对文档有所了解。我找到了一些值得一看的链接:

写一个流程

使用LLVM来完成高产研究通常意味着你要写一些自定义流程。这一节会指导你构建和运行一个简单的流程来变换你的程序。

框架

我已经准备好了模板仓库,里面有些没用的LLVM流程。我推荐先用这个模板。因为如果完全从头开始,配好构建的配置文件可是相当痛苦的事。

首先从GitHub上下载llvm-pass-skeleton仓库:

主要的工作都是在skeleton/Skeleton.cpp中完成的。把它打开。这里是我们的业务逻辑:

LLVM流程有很多种,我们现在用的这一种叫(这是一个不错的入手点)。正如你所期望的,LLVM会在编译每个函数的时候先唤起这个方法。现在它所做的只是打印了一下函数名。

细节:

errs()是一个LLVM提供的C++输出流,我们可以用它来输出到控制台。函数返回false说明它没有改动函数F。之后,如果我们真的变换了程序,我们需要返回一个true。构建

通过CMake来构建这个流程:

如果LLVM没有全局安装,你需要告诉CMake LLVM的位置.你可以把环境变量LLVM_DIR的值修改为通往share/llvm/cmake/的路径。比如这是一个使用Homebrew安LLVM的例子:

构建流程之后会产生一个库文件,你可以在build/skeleton/libSkeletonPass.so或者类似的地方找到它,具体取决于你的平台。下一步我们载入这个库来在真实的代码中运行这个流程。

运行

想要运行你的新流程,用clang编译你的C代码,同时加上一些奇怪的flag来指明你刚刚编译好的库文件:

-Xclang -load -Xclang path/to/lib.so这是你在Clang中载入并激活你的流程所用的所有代码。所以当你处理较大的项目的时候,你可以直接把这些参数加到Makefile的CFLAGS里或者你构建系统的对应的地方。

(通过单独调用clang,你也可以每次只跑一个流程。这样需要用LLVM的opt命令。这是,但在这里我就不赘述了。)

恭喜你,你成功hack了一个编译器!接下来,我们要扩展这个hello world水平的流程,来做一些好玩的事情。

理解LLVM的中间表示

想要使用LLVM里的程序,你需要知道一点中间表示的组织方法。

模块(Module),函数(Function),代码块(BasicBlock),指令(Instruction)模块包含了函数,函数又包含了代码块,后者又是由指令组成。除了模块以外,所有结构都是从值产生而来的。

容器

首先了解一下LLVM程序中最重要的组件:

大部分LLVM中的内容——包括函数,代码块,指令——都是继承了一个名为值的基类的C++类。值是可以用于计算的任何类型的数据,比如数或者内存地址。全局变量和常数(或者说字面值,立即数,比如5)都是值。

指令

这是一个写成人类可读文本的LLVM中间表示的指令的例子。

这个指令将两个32位整数相加(可以通过类型i32推断出来)。它将4号寄存器(写作%4)中的数和字面值2(写作2)求和,然后放到5号寄存器中。这就是为什么我说LLVM IR读起来像是RISC机器码:我们甚至连术语都是一样的,比如寄存器,不过我们在LLVM里有无限多个寄存器。

在编译器内,这条指令被表示为指令C++类的一个实例。这个对象有一个操作码表示这是一次加法,一个类型,以及一个操作数的列表,其中每个元素都指向另外一个值(Value)对象。在我们的例子中,它指向了一个代表整数2的常量对象和一个代表5号寄存器的指令对象。(因为LLVM IR使用了静态单次分配格式,寄存器和指令事实上是一个而且是相同的,寄存器号是人为的字面表示。)

另外,如果你想看你自己程序的LLVM IR,你可以直接使用Clang:

查看流程中的IR在乎的应该是沿途的风景以及看风景的心情。

为什么人人都该懂点LLVM

相关文章:

你感兴趣的文章:

标签云: