学习一下LLVM的hello world。
LLVM简介 简单编译原理 学过编译原理的人都知道(然而我没学过),编译过程主要可以划分为前端与后端:
前端(Front End)会把高级语言源代码翻译成中间表示(IR)。
后端(Back End)将IR翻译成目标平台的机器码。
对于现在大部分编译器来说,中间表示即汇编语言,并且前端与后端之间强耦合,不会给你接口操作 IR。LLVM 提供了 LLVM IR 这样的中间表示,它介于编译器的前端和后端之间,我们且把它叫做中端(Middle End)。高级语言的翻译流程变成了:.c
经过 LLVM 翻译之后形成 LLVM IR,LLVM IR 经过中断处理变为目标平台的中间表示,最后再由后端翻译成机器码(目前个人浅薄的理解,难免有错)。
强耦合的优点便是提供一条龙服务,.c -> .exe
一气呵成,但是缺点也是很明显的,那就是不同的目标平台和不同的高级语言之间,需要提供不同的编译器。如果由 n 种高级语言,m个目标平台,那么我们需要有 n*m
种不同的编译器,并且这些编译器之间,会有很大的冗余,比如都是 x86 平台的编译器,那么它们将汇编语言翻译成机器语言的部分应当是一样的。
LLVM 将前端和后端进行了分离,出了一个新的中间表示,那么对于高级语言设计者来说,我只需要关心我的语言怎么翻译成 LLVM IR 即可,LLVM 提供了 LLVM IR 翻译成其它所有平台 IR 的方式。这样 n 个前端,m 个后端,可以让它们自由组合形成我想要的编译器,十分灵活。
Clang Clang 是基于 LLVM 的C语言编译器,和 GCC 差不多可以这么理解。因为是基于LLVM的,当然它也能提供从 C 到 LLVM IR 的翻译。为了方便,我们可以把编译结果加入 PATH 变量,这样就可以在任意一个目录下面引用 clang 了。
写一个很简单的程序:
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main () { int a; scanf ("%d" ,&a); if (a==1 ){ puts ("WIN" ); } else { puts ("FAIL" ); } }
LLVM IR有两种格式的表示,一种是适合我们阅读的 .ll
文本格式,另一种就是适合存储的 .bc
,我觉得可以类比为 Linux 下面的 .s 和 .o 文件,它们相互转化也很方便。
.c -> .ll:clang -emit-llvm -S a.c -o a.ll
.c -> .bc: clang -emit-llvm -c a.c -o a.bc
.ll -> .bc: llvm-as a.ll -o a.bc
.bc -> .ll: llvm-dis a.bc -o a.ll
.bc -> .s: llc a.bc -o a.s
这些只要按照之前的方法编译并将二进制文件的目录写进 PATH 之后都是会有这些工具的,类比之后其实也很容易看出来 llvm-as 就是汇编,将文本格式转二进制格式,llvm-dis 就是反汇编,将二进制格式反汇编为文本格式。
LLVMHello 在编译的 600 多个项目当中有一个项目叫 LLVMHello,在Loadable Modules目录下面,以此学习一下 pass 以及加载 pass 的流程。编译完成之后应该会有一个 LLVMHello.dll
在 \Debug\bin\
目录下面,也就是之前和 clang 在同个目录下的。官方给的默认源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include "llvm/ADT/Statistic.h" #include "llvm/IR/Function.h" #include "llvm/Pass.h" #include "llvm/Support/raw_ostream.h" #include <iostream> using namespace llvm;#define DEBUG_TYPE "hello" STATISTIC (HelloCounter, "Counts number of functions greeted" );namespace { struct Hello : public FunctionPass { static char ID; Hello () : FunctionPass (ID) {} bool runOnFunction (Function &F) override { ++HelloCounter; std::cout << "Hello: " ; int fd; errs ().write_escaped (F.getName ()) << '\n' ; return false ; } }; } char Hello::ID = 0 ;static RegisterPass<Hello> X ("hello" , "Hello World Pass" ) ;namespace { struct Hello2 : public FunctionPass { static char ID; Hello2 () : FunctionPass (ID) {} bool runOnFunction (Function &F) override { ++HelloCounter; std::cout << "Hello: " ; __debugbreak; errs ().write_escaped (F.getName ()) << '\n' ; return false ; } void getAnalysisUsage (AnalysisUsage &AU) const override { AU.setPreservesAll (); } }; } char Hello2::ID = 0 ;static RegisterPass<Hello2> Y ("hello2" , "Hello World Pass (with getAnalysisUsage implemented)" ) ;
我是怎么都加载不上的,经了解,可能跟这个原因有关:LLVM17已经开始逐渐淘汰之前的 Legacy Pass 逐渐转用 New pass了,通过 Github 上的 Commit 记录也可以发现该文件最近一次修改在 6 年前。
这里我参考了一篇文章上的源码,写出了 First Pass 并成功加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include "llvm/IR/PassManager.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Support/raw_ostream.h" using namespace llvm;#include <iostream> namespace llvm {struct NewPassHelloWorld : public PassInfoMixin<NewPassHelloWorld> { PreservedAnalyses run (Module &F, ModuleAnalysisManager &AM) { std::cout << "NewPassHelloWorld Loaded" << std::endl; errs () << "MyPass:" ; errs () << F.getName () << "\n" ; return PreservedAnalyses::all (); } bool isRequird () { return true ; } }; } extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK llvmGetPassPluginInfo () { return {LLVM_PLUGIN_API_VERSION, "NewPassHelloWorld" , "v0.1" , [](PassBuilder &PB) { PB.registerPipelineParsingCallback ( [](StringRef PassName, ModulePassManager &FPM, ...) { if (PassName == "NewPassHelloWorld" ) { FPM.addPass (NewPassHelloWorld ()); return true ; } return false ; }); }}; }
这里需要注意,我们需要导出 llvmGetPassPluginInfo
函数,但是不能使用关键字 __declspec(dllexport) ,否则编译不会成功,这里我选择在项目中新建一个export.def。
选择导出指定的函数,再在项目属性中指定该文件作为模块定义文件
这样我们编译出来的 LLVMHello.dll 便具有该导出函数了。
因此发现 pass 加载失败可以先看看 dll 有没有导出这个函数。
opt加载pass 首先先编译出一个 ll 文件。
clang -emit-llvm -S main.c -o main.ll
再使用 opt 加载 pass。
opt --load-pass-plugin=llvmhello.dll --passes=NewPassHelloWorld main.ll -o main.bc
输出信息了说明 pass 加载成功了。
clang加载pass 在LLVM新版本中允许 clang 加载 pass 了(据说以前是不行的),在编译的时候传参 -Xclang -fpass-plugin="/path/to/dll/pass.dll"
可以直接加载 pass。
但是需要在 dll 中调用 registerPipelineStartEPCallback
注册回调才能被 clang 加载,这里我们写一个回调并注册它,就可以被clang加载。
定义回调函数:
1 2 3 4 void myCallback (llvm::ModulePassManager &PM, OptimizationLevel Level) { PM.addPass (NewPassHelloWorld ()); }
注册回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK llvmGetPassPluginInfo () { return {LLVM_PLUGIN_API_VERSION, "NewPassHelloWorld" , LLVM_VERSION_STRING, [](PassBuilder &PB) { PB.registerPipelineStartEPCallback (myCallback); PB.registerPipelineParsingCallback ( [](StringRef PassName, ModulePassManager &FPM, ...) { if (PassName == "NewPassHelloWorld" ) { FPM.addPass (NewPassHelloWorld ()); return true ; } return false ; }); }}; }
编译命令:clang -Xclang -fpass-plugin=./LLVMHello.dll main.c -o test.exe
参考资料
Windows下优雅地使用LLVMPass
LLVM 入门引导