LLVM 编译与First Pass

Posted by Qfrost on 2021-10-12
Estimated Reading Time 7 Minutes
Words 1.6k In Total
Viewed Times

要开始正儿八经认真学LLVM了。先吐槽一下,这东西是真的麻烦,编译起来一堆坑,项目还贼大,一编译就是十几分钟至一个小时。还吃电脑各种环境,各种小版本,前前后后至少编译了好几天才整出一个跨平台、可集成多种工具链的成品。下面做个记录。

编译

编译这一步应该是最麻烦的,编译时间长,小版本多,github各种仓库,鱼龙混杂。这里有个很大的坑,就是LLVM的版本。如果就是随便用用,那版本无所谓,但是如果要集成到工具链里去,那就必须要新版的LLVM。比如想要集成到VS2019的v142生成工具上,就要求LLVM的版本大于等于11,不然就用不了C++语法。然后我又是要有用OLLVM需求的,所以就找到了一个LLVM12并集成Obfucation的项目clone下来学习。(想用最新的LLVM13更好,但是翻了翻Github,还没有人集成好LLVM13+Obfucation)

https://github.com/0x3f97/ollvm-12.x

然后是编译命令的问题。用该仓库给出的生成VS项目的命令自然是可以的

1
cmake -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi;lld" -G "Visual Studio 16 2019" -A x64 -Thost=x64 ../ollvm.12x-main/llvm

但是,用vs编译这种超大工程,太吃电脑性能了,基本我的AMD3900X要跑近二十分钟而且期间电脑完全卡死动不了。所以我后面在Windows下编译基本选择cmake生成Mingw工程,用VSCode写代码,这样舒服很多(vs2019加载个工程都够呛,敲代码的时候联想速度也很慢)。

1
cmake -G "MinGW Makefiles" -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi;lld" -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="X86" ../ollvm.12x-main/llvm

上面都是Windows下的编译,一方面是我要把OLLVM集成到VS上去用于编译我的项目,一方面以为(划重点)在Windows下学习LLVM,敲代码和调试会更方面。但是经过和学弟几天的折腾,发现完全不行,学习LLVM必须要在Linux下。 因为学习过程中要自己动手敲一些Pass,而在Windows下,LLVM不支持将单个pass编译成独立Module,如果要调试pass,就要完整编译整个项目并将该Pass集成到流水线上,且不说会不会对代码产生污染,调试测试都是大问题。

Linux下的编译比较简单,我用的是Ubuntu18的WSL,要手动升级一下cmake(项目要求cmake版本大于3.13)

1
cmake -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi;lld" -DLLVM_TARGETS_TO_BUILD="X86" -DCMAKE_BUILD_TYPE=Debug -DLLVM_INCLUDE_TESTS=OFF ../ollvm.12x-main/llvm

然后就是要注意加上 -DLLVM_ENABLE_PROJECTS 指定编译的项目,如果不加上这个参数,只会编译基本的llvm项目内容,也就是不含clang的,那也是不能用的。还有就是这个-DCMAKE_BUILD_TYPE,如果是想编译一次然后用的那就用Release,但是如果是要自己调试学习的,一定要编译成Debug类型,不然没法调试。(PS:Debug模式编译更吃电脑,10代i7 16G内存开9线程编译了四十分钟,到最后单进程内存峰值都要到10G,8G内存的电脑估计吃不消编译;生成的文件也达五十几个G,注意留好硬盘空间

编写第一个Pass

首先先将WSL环境变量配上。

1
2
3
4
vim ~/.bashrc
export LLVM_HOME=/mnt/c/Users/Qfrost/Desktop/code/LLVM/build // 这个是生成的build目录
export PATH=${LLVM_HOME}/bin:$PATH
source ~/.bashrc

然后可以在本机上直接用vscode打开LLVM源码根目录快乐写代码。LLVM Pass的位置是 llvm/lib/Transforms,在这个目录下,可以看到已经有很多Pass了,比如HelloPass和HelloNewPass,这两个Pass的作用仅仅就是对每个函数输出 Hello:%FunctionName%,区别是HelloNewPass会生成独立的模块而HelloPass不会。我们可以仿照他们写一个具有独立模块的Pass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// MyPass
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
struct MyPass : public FunctionPass {
static char ID;
MyPass() : FunctionPass(ID) {}

bool runOnFunction(Function &F) override {
errs() << "MyPass:";
errs().write_escaped(F.getName()) << '\n';
return false;
}
};
}

char MyPass::ID = 0;
static RegisterPass<MyPass> X("myPass", "Hello MyPass", false, false);

然后就需要编译这个Pass,一般有两种编译方法

独立编译成Module

如果要独立编译一个Pass形成so文件,最简单暴力的方法是

1
clang `llvm-config --cxxflags` `llvm-config --ldflags` -I /LLVM_CODE/llvm/include -I /LLVM_HOME/include -shared  -fPIC MyPass.cpp -o MyPass.so

而其中LLVM_CODE是LLVM源码根目录,LLVM_HOME是你的LLVM生成根目录。这里就很离谱,用到的头文件既有源码里的也有生成文件里的。但确实是最简单暴力方便的。

还有就是写一个CMakeLists.txt放在同目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.4)
if(NOT DEFINED ENV{LLVM_HOME})
message(FATAL_ERROR "$LLVM_HOME is not defined")
endif()
if(NOT DEFINED ENV{LLVM_DIR})
set(ENV{LLVM_DIR} $ENV{LLVM_HOME}/lib/cmake/llvm)
endif()
find_package(LLVM REQUIRED CONFIG)
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})

add_library(LLVMMyPass MODULE
# List your source files here.
MyPass.cpp
)

然后cmake && make 同样可以在当前目录下生成LLVMMyPass.so文件

源码目录编译

在Pass目录下写一个CMakeLists.txt

1
2
3
4
add_library(LLVMMyPass MODULE
# List your source files here.
MyPass.cpp
)

然后修改上层CMakeLists.txt,也就是Transforms目录下的CMakeLists.txt,为其添加一行,将自己写的Pass目录添加进去

1
add_subdirectory(MyPass)

然后就重复第一步的对整个LLVM项目进行build。但这样比完全重新编译还是会快不少的,因为编译过的文件不会重复编译。生成的so在LLVM_HOME/lib下

使用模块Pass

先写一个Hello World

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// test.cpp
#include <stdio.h>
#include <iostream>
void hello() {
printf("Hello");
}
void world() {
printf("World");
}
int main(int argc, char* argv[]) {
std::cout << "Main" << std::endl;
hello();
world();
return 0;
}

将其编译成bc文件

  • clang -emit-llvm -c test.cpp

这样就有了一个测试代码的bc文件(test.bc)和我们MyPass的so文件(MyPass.so

然后,用opt加载这个so

  • opt -load ./MyPass.so -mypass test.bc
    或者
  • clang -Xclang -load -Xclang MyPass.so -w test.cpp -o test.bin
1
2
3
4
5
6
7
8
9
10
11
root@Qfrost:/mnt/c/Users/Qfrost/Desktop/code/LLVM/ollvm-12.x-main/llvm/lib/Transforms/MyPass# opt -load ./MyPass.so -mypass test.bc
WARNING: You're attempting to print out a bitcode file.
This is inadvisable as it may cause display problems. If
you REALLY want to taste LLVM bitcode first-hand, you
can force output with the `-f' option.

MyPass:__cxx_global_var_init
MyPass:_Z5hellov
MyPass:_Z5worldv
MyPass:main
MyPass:_GLOBAL__sub_I_test.cpp

参考资料

  1. https://www.leadroyal.cn/p/647/

  2. https://zhuanlan.zhihu.com/p/68113486

  3. https://www.jianshu.com/p/9f450969121b