PS: Clang为LLVM提供的C语言编译器,默认参数可以生成本机可执行的二进制程序。-S和-c参数与GCC一样,可分别生成.s汇编文件与.o目标文件。
将C文件编译为LLVM bitcode文件
clang -O3 -emit-llvm hello.c -c -o hello.bc
-emit-llvm选项可与-S或-c选项一起使用,以分别为代码生成LLVM .ll
或.bc
文件。两者都是LLVM Bitcode,区别在于前者是可读的文本,后者是不可读的二进制格式。
这两个文件都可以使用标准LLVM的工具来操作它。
bc
文件可以使用lli
工具执行,lli
可以通过解释器或使用高级选项中的即时 (JIT) 编译器执行此工作。请参阅 参考资料,获取关于 lli
的更多信息的链接。
lli hello.bc
llvm-dis
工具可用来反汇编LLVM bitcode文件,它会将bc文件的内容以可读文本的形式进行展示。
llvm-dis < hello.bc
将bc
文件转换成ll
文件
llvm-dis hello.bc
llc
将 LLVM 字节代码转换成特定于平台的汇编代码
写第一个pass
理论基础
pass机制是llvm系统的一项非常重要的组成部分,也是整个llvm编译器体系中最为有趣和复杂的部分,通过编写pass来为llvm进行代码的优化、转化,或者进行分析以供为优化和转化提供依据。
所有的pass都是llvm的Pass类的子类,通过重写继承的虚函数来实现特定的功能。根据pass不同的功能分类,继承的类也不同,比如:ModulePass , CallGraphSCCPass, FunctionPass , LoopPass, RegionPass, BasicBlockPass,llvm系统会根据实例的类别来判断pass的功能,然后将其整合到现有的优化体系中去。
接下来我们先从简单的开始,比如写一个最简单的hello world!的pass,全面介绍编写pass的代码结构、编译方法、加载方式和执行结果,由浅入深。
hello world!这个pass仅仅打印代码里的函数,并不会对函数或者代码做任何修改,只是Analyze分析一下。这个hello world!的pass的源码和环境其实已经在lib/Transforms/Hello文件夹里了,因为它是官方的“Hello World!”
:~/llvm-8.0.0.src/lib/Transforms$ tree -NCfhl |grep Hello
├── [4.0K] ./Hello
│ ├── [ 431] ./Hello/CMakeLists.txt
│ ├── [1.9K] ./Hello/Hello.cpp
│ └── [ 0] ./Hello/Hello.exports
准备编译环境
首先在lib/Transforms/目录下创建Hello文件夹,并且在文件夹内创建一个编译文件CMakeLists.txt,内容如下:
# If we don't need RTTI or EH, there's no reason to export anything
# from the hello plugin.
if( NOT LLVM_REQUIRES_RTTI )
if( NOT LLVM_REQUIRES_EH )
set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Hello.exports)
endif()
endif()
if(WIN32 OR CYGWIN)
set(LLVM_LINK_COMPONENTS Core Support)
endif()
add_llvm_library( LLVMHello MODULE BUILDTREE_ONLY
Hello.cpp
DEPENDS
intrinsics_gen
PLUGIN_TOOL
opt
)
该编译文件将会被make命令所加载和使用,将文件夹内的Hello.cpp编译并且链接成为一个库文件$(LEVEL)/lib/LLVMHello.so,该库文件可以被opt命令使用-load参数来动态加载。
接下来要修改一下上层目录中的lib/Transforms/CMakeLists.txt文件,加上一行:
add_subdirectory(Hello)
可以看到文件内其实已经“注册”好了大量的pass,这些都会最终被编译系统所编译。
$ cat CMakeLists.txt
add_subdirectory(Utils)
add_subdirectory(Instrumentation)
add_subdirectory(AggressiveInstCombine)
add_subdirectory(InstCombine)
add_subdirectory(Scalar)
add_subdirectory(IPO)
add_subdirectory(Vectorize)
add_subdirectory(Hello)
add_subdirectory(ObjCARC)
add_subdirectory(Coroutines)
编写代码并编译
接下来就是编写Hello.cpp代码,虽然其实在源码中已经编写好了,我们也稍微注释和解释一下。
开头是包含一些头文件,因为我们写的是Pass,我们操作的是Function,还要做一点输出的工作。
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
在llvm命名空间内活动
using namespace llvm;
创建一个匿名的命名空间,C++中的匿名空间就像C代码中的静态块一样,限定了空间内的变量的访问范围仅限于匿名空间内部。这也体现了pass之间是相互独立的,并不需要另外的界面或者接口(当然这些也是可以有的,只是不需要而已)
namespace {
接下来定义我们的pass类——"Hello",继承于FunctionPass类。FunctionPass类一次只操作一个函数。
struct Hello : public FunctionPass {
声明一个pass的ID,llvm将会使用ID来定位这些pass,这样就避免了llvm使用复杂的C++运行时机制。
static char ID;
Hello() : FunctionPass(ID) {}
定义runOnFunction
方法来覆写虚函数,在方法内实现我们想要的操作和功能,当然,目前只是打印出函数名就好。
bool runOnFunction(Function &F) override {
errs() << "Hello: ";
errs().write_escaped(F.getName()) << 'n';
return false;
}
}; // end of struct Hello
} // end of anonymous namespace
将ID初始化一下。llvm使用ID的地址来定位pass,所以初始化的值是多少都没关系。
char Hello::ID = 0;
最后将类注册一下,给一个命令行选项为hello,给一个名字“Hello World Pass”。最后两个参数定义了pass的行为,如果只是遍历CFG并不修改的话,则第三个参数为true,反之为false。如果该pass是一个analyze的pass的话,第四个参数为true,反之为false。
static RegisterPass<Hello> X("hello", "Hello World Pass",
false /* Only looks at CFG */,
false /* Analysis Pass */);
把代码全部组合起来,即官方的示例代码:
//===- Hello.cpp - Example code from "Writing an LLVM Pass" ---------------===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
//
// This file implements two versions of the LLVM "Hello World" pass described
// in docs/WritingAnLLVMPass.html
//
//===----------------------------------------------------------------------===//
#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
#define DEBUG_TYPE "hello"
STATISTIC(HelloCounter, "Counts number of functions greeted");
namespace {
// Hello - The first implementation, without getAnalysisUsage.
struct Hello : public FunctionPass {
static char ID; // Pass identification, replacement for typeid
Hello() : FunctionPass(ID) {}
bool runOnFunction(Function &F) override {
++HelloCounter;
errs() << "Hello: ";
errs().write_escaped(F.getName()) << '\n';
return false;
}
};
}
char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass");
namespace {
// Hello2 - The second implementation with getAnalysisUsage implemented.
struct Hello2 : public FunctionPass {
static char ID; // Pass identification, replacement for typeid
Hello2() : FunctionPass(ID) {}
bool runOnFunction(Function &F) override {
++HelloCounter;
errs() << "Hello: ";
errs().write_escaped(F.getName()) << '\n';
return false;
}
// We don't modify the program, so we preserve all analyses.
void getAnalysisUsage(AnalysisUsage &AU) const override {
AU.setPreservesAll();
}
};
}
char Hello2::ID = 0;
static RegisterPass<Hello2>
Y("hello2", "Hello World Pass (with getAnalysisUsage implemented)");
在build目录下执行make,生成LLVMHello.so
编译完成之后,可以在lib/LLVMHello.so路径找到我们的共享库文件。
使用opt命令加载pass
既然我们已经编译成功了,接下来就是加载了。使用opt命令的-load选项来加载该pass,并且可是使用该pass的参数-hello,当然前提是已经加载成功了。
clang -S -O3 -emit-llvm sample.c
这时候在目录下就会出现sample.ll的IR中间码。可以用刚刚写的pass来观察下函数名:
./opt -load ../lib/LLVMHello.dylib -hello < hello.ll > /dev/null
Hello: main
-load
参数指定共享库的路径并将其加载,加载之后-hello参数才是可以使用的,当然也是我们注册了这个参数的结果。因为-hello并没有修改任何函数,所以输出肯定跟输入是一样的,直接扔到/dev/null里去就行了。
我们上文还注册了一串字符串呢,大家还记得么?也是可以看到的。
./opt -load ../lib/LLVMHello.dylib -help |grep -i hello
-hello - Hello World Pass
既然我们的pass已经加载起来跑起来了,可以使用PassManager的-time-passes命令行工具来测一下pass的执行时间,该工具会统计生效中的所有pass的执行时间。
./opt -load ../lib/LLVMHello.dylib -hello -time-passes < sample.ll > /dev/null
可以看到占用时间最多的是Bitcode Writer,其次才是Hello World Pass,说明该pass并没有占用多长时间。
Comments | NOTHING