只要你和程序打交道,了解编译器架构就会令你受益无穷——无论是分析程序效率,还是模拟新的处理器和操作系统。通过本文介绍,即使你对编译器原本一知半解,也能开始用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仓库:
主要的工作都是在 virtual bool runOnFunction(Function &F) { errs() << "I saw a function called " << F.getName() << "!\n"; return false; } LLVM流程有很多种,我们现在用的这一种叫函数流程(function pass)(这是一个不错的入手点)。正如你所期望的,LLVM会在编译每个函数的时候先唤起这个方法。现在它所做的只是打印了一下函数名。 细节:
构建 通过CMake来构建这个流程: $ cd llvm-pass-skeleton $ mkdir build $ cd build $ cmake .. # Generate the Makefile. $ make # Actually build the pass.
如果LLVM没有全局安装,你需要告诉CMake LLVM的位置.你可以把环境变量 $ LLVM_DIR=/usr/local/opt/llvm/share/llvm/cmake cmake ..
构建流程之后会产生一个库文件,你可以在 运行
想要运行你的新流程,用 $ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.* something.c I saw a function called main!
(通过单独调用 恭喜你,你成功hack了一个编译器!接下来,我们要扩展这个hello world水平的流程,来做一些好玩的事情。 理解LLVM的中间表示想要使用LLVM里的程序,你需要知道一点中间表示的组织方法。
容器
首先了解一下LLVM程序中最重要的组件: 大部分LLVM中的内容——包括函数,代码块,指令——都是继承了一个名为值的基类的C++类。值是可以用于计算的任何类型的数据,比如数或者内存地址。全局变量和常数(或者说字面值,立即数,比如5)都是值。 指令 这是一个写成人类可读文本的LLVM中间表示的指令的例子。 %5 = add i32 %4, 2
这个指令将两个32位整数相加(可以通过类型 在编译器内,这条指令被表示为指令C++类的一个实例。这个对象有一个操作码表示这是一次加法,一个类型,以及一个操作数的列表,其中每个元素都指向另外一个值(Value)对象。在我们的例子中,它指向了一个代表整数2的常量对象和一个代表5号寄存器的指令对象。(因为LLVM IR使用了静态单次分配格式,寄存器和指令事实上是一个而且是相同的,寄存器号是人为的字面表示。) 另外,如果你想看你自己程序的LLVM IR,你可以直接使用Clang:
查看流程中的IR
让我们回到我们正在做的LLVM流程。我们可以查看所有重要的IR对象,只需要用一个普适而方便的方法:
下面是代码。你可以通过在 errs() << "Function body:\n"; F.dump(); for (auto& B : F) { errs() << "Basic block:\n"; B.dump(); for (auto& I : B) { errs() << "Instruction: "; I.dump(); } }
使用C++ 11里的 如果你重新构建流程并通过它再跑程序,你可以看到很多IR被切分开输出,正如我们遍历它那样。 做些更有趣的事当你在找寻程序中的一些模式,并有选择地修改它们时,LLVM的魔力真正展现了出来。这里是一个简单的例子:把函数里第一个二元操作符(比如+,-)改成乘号。听上去很有用对吧?
下面是代码。这个版本的代码,和一个可以试着跑的示例程序一起,放在了 for (auto& B : F) { for (auto& I : B) { if (auto* op = dyn_cast<BinaryOperator>(&I)) { // Insert at the point where the instruction `op` appears. IRBuilder<> builder(op); // Make a multiply with the same operands as `op`. Value* lhs = op->getOperand(0); Value* rhs = op->getOperand(1); Value* mul = builder.CreateMul(lhs, rhs); // Everywhere the old instruction was used as an operand, use our // new multiply instruction instead. for (auto& U : op->uses()) { User* user = U.getUser(); // A User is anything with operands. user->setOperand(U.getOperandNo(), mul); } // We modified the code. return true; } } } 细节如下:
现在我们编译一个这样的程序(代码库中的example.c): #include <stdio.h>int main(int argc, const char** argv) { int num; scanf("%i", &num); printf("%i\n", num + 2); return 0; } 如果用普通的编译器,这个程序的行为和代码并没有什么差别;但我们的插件会让它将输入翻倍而不是加2。 $ cc example.c $ ./a.out 10 12 $ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.so example.c $ ./a.out 10 20 很神奇吧! 链接动态库如果你想调整代码做一些大动作,用IRBuilder来生成LLVM指令可能就比较痛苦了。你可能需要写一个C语言的运行时行为,然后把它链接到你正在编译的程序上。这一节将会给你展示如何写一个运行时库,它可以将所有二元操作的结果记录下来,而不仅仅是闷声修改值。
这里是LLVM流程的代码,也可以在 // 从运行时库获取调用函数 LLVMContext& Ctx = F.getContext(); Constant* logFunc = F.getParent()->getOrInsertFunction( "logop", Type::getVoidTy(Ctx), Type::getInt32Ty(Ctx), NULL ); for (auto& B : F) { for (auto& I : B) { if (auto* op = dyn_cast<BinaryOperator>(&I)) { // Insert *after* `op`. IRBuilder<> builder(op); builder.SetInsertPoint(&B, ++builder.GetInsertPoint()); // Insert a call to our function. Value* args[] = {op}; builder.CreateCall(logFunc, args); return true; } } }
你需要的工具包括Module::getOrInsertFunction和IRBuilder::CreateCall。前者给你的运行时函数 #include <stdio.h>void logop(int i) { printf("computed: %i\n", i); } 要运行这个程序,你需要链接你的运行时库: $ cc -c rtlib.c $ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.so -c example.c $ cc example.o rtlib.o $ ./a.out 12 computed: 14 14 如果你希望的话,你也可以在编译成机器码之前就缝合程序和运行时库。llvm-link工具——你可以把它简单看做IR层面的ld的等价工具,可以帮助你完成这项工作。 注记(Annotation)大部分工程最终是要和开发者进行交互的。你会希望有一套注记(annotations),来帮助你从程序里传递信息给LLVM流程。这里有一些构造注记系统的方法:
我希望能在以后的文章里展开讨论这些技术。 其他LLVM非常庞大。下面是一些我没讲到的话题:
转载请保留固定链接: https://linuxeye.com/Linux/2797.html |