LLVM 编译与First Pass
要开始正儿八经认真学LLVM了。先吐槽一下,这东西是真的麻烦,编译起来一堆坑,项目还贼大,一编译就是十几分钟至一个小时。还吃电脑各种环境,各种小版本,前前后后至少编译了好几天才整出一个跨平台、可集成多种工具链的成品。下面做个记录。
编译
编译这一步应该是最麻烦的,编译时间长,小版本多,github各种仓库,鱼龙混杂。这里有个很大的坑,就是LLVM的版本。如果就是随便用用,那版本无所谓,但是如果要集成到工具链里去,那就必须要新版的LLVM。比如想要集成到VS2019的v142生成工具上,就要求LLVM的版本大于等于11,不然就用不了C++语法。
这边是之前找的别人移植好的高版本OLLVM仓库:
https://github.com/0x3f97/ollvm-12.x
https://gitee.com/qingyu_yyy/ollvm-project/tree/14.x
然后就是编译,这里我觉得必须要分情况讨论,Windows下的编译实在是比Linux下编译复杂太多了
Linux
Linux下的编译比较简单,我用的是Ubuntu18的WSL,要手动升级一下cmake(项目要求cmake版本大于3.13)
1
cmake -G Ninja -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TARGETS_TO_BUILD="X86;Mips;ARM;BPF" -DCMAKE_BUILD_TYPE=Debug -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_RTTI=ON ../llvm_project/llvm
然后说几个坑点:
要加上 -DLLVM_ENABLE_PROJECTS 指定编译的项目,如果不加上这个参数,只会编译基本的llvm项目内容,也就是不含clang的,那是不能用的
对于 -DCMAKE_BUILD_TYPE,如果是想编译一次然后用的那就用Release,但是如果是要自己调试学习的,一定要编译成Debug类型,不然没法调试。(PS:Debug模式编译更吃电脑,10代i7 16G内存开9线程编译了四十分钟,到最后单进程内存峰值都要到10G,8G内存的电脑估计吃不消编译;生成的文件也达五十几个G,注意留好硬盘空间
一些情况下可能会报一个错 config.guess: 71: Syntax error: word unexpected (expecting “in”) 查了一圈资料发现是因为行尾的问题。我是在Windows下拉取仓库进WSL编译Linux的Bin时遇到的,在Linux编译时对行尾的识别错误导致失败。做法是要么在Linux(WSL)下拉取仓库,或者用dos2unix转化一下行尾
Windows
(不建议在Windows下折腾LLVM 本节完(×
Windows下编译我遇到过诸多问题,这里就不细说踩坑过程了,记录一下一些坑点吧:
直接用官方的命令生成.sln项目并直接用VS图形化编译工具编译,电脑会直接卡爆,反正我16G内存到link阶段直接BSOD
使用 -G “MinGW Makefiles” 参数并用MinGW工具链编译可能会报错 Using std::regex with exceptions disabled is not fully supported,这是由于默认的MinGW的GCC版本是8.1.0(截止至我写此博客时),这个版本太低了。
使用MinGW配置文件并下载release的Clang作为编译器,-G “MinGW Makefiles” -D CMAKE_C_COMPILER=clang -D CMAKE_CXX_COMPILER=clang++,可能会报错 Unknown architecture host,这是由于Windows系统的SDK通常是MSVC的,而clang不支持MSVC中的部分语法,比如这个错误就是因为MSVC中使用了typeid而clang不支持引起的
最终我的方法还是使用MSVC工具链来编译。但是需要先设置一下注册表的Windows最大路径长度,因为LLVM会输出一些神奇临时文件最大长度会大于Windows的最大路径长度(260字节)。这个可以在注册表的 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem 项中的LongPathEnabled键上,将0设置为1即可。我选择使用MSVC的命令行进行编译并通过-m控制并发数避免卡死。
1
2
cmake -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_ENABLE_PLUGINS=On -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS_DEBUG="/Ox" -DCMAKE_CXX_FLAGS_DEBUG="/Ox" -DLLVM_INCLUDE_TESTS=On -DLLVM_ENABLE_RTTI=ON -DCMAKE_INSTALL_PREFIX="../Windows" ../
# MinSizeRel模式编译不会生成.ink文件(大概有五十几个G)
这里对几个我踩过坑的选项做一些解释:
插件加载:在Windows下,为了加载插件(即导出插件的符号),在编译时需要开一个选项,LLVM12即之前的版本应该是LLVM_EXPORT_SYMOBLS_FOR_PLUGINS,后续版本中应为LLVM_ENABLE_PLUGINS
libc编译:在新版LLVM中已经没有Project:libcxx和libcxxabi了,如果要编译运行库需要指定 -DLLVM_ENABLE_RUNTIMES=“libcxx;libcxxabi;libunwind” 参数。
架构指定:有的版本的LLVM需要手动指定架构,即,要加上 -DCMAKE_CXX_FLAGS="-DENDIAN_LITTLE"
BUILD_TYPE:这个就很迷了,如果要完整符号并且可以挂调试器调试这种,是要编译Debug的,同时Debug版本下,当clang或者加载的插件发生任何崩溃时会告诉你崩溃的位置和栈回溯。然后我发现,其他的好像都差不多,发生崩溃的时候就一句话 clang-cl : error : clang frontend command failed due to signal (use -v to see invocation),包括RelWithDebInfo发生崩溃后也是不会有任何报错提示的。 总结一下,要调试、或者说有做开发需求的,就用Debug编译,否则根本不知道错在哪里,如果单纯使用,就用MinSizeRel
Install:很多情况下,在make完之后会使用make install安装LLVM(将生成的bin和lib复制到安装目录,并配置一些环境变量),故可以使用CMAKE_INSTALL_PREFIX选项指定安装位置
然后其实Windows下编译LLVM的话可以不用编译lld,只编译clang,lld使用MSVC的,这样相当于编译器使用clang连接器使用MSVC,就可以使用clang编译Windows驱动了(Windows sys文件的编译链接器必须要MSVC,或者说不用MSVC的链接器会究极麻烦)
生成好配置文件后,使用VS x64 Native Tools 或 x86_64 Cross Tools工具,用MSBuild命令行编译LLVM
1
2
3
4
5
MSBuild LLVM.sln -p:configuration=Debug -p:Platform="x64" -m:1
MSBuild LLVM.sln -p:configuration=MinSizeRel -p:Platform="x64" -m:1
MSBuild install.vcxproj -p:configuration=Debug -p:Platform="x64" -m
MSBuild install.vcxproj -p:configuration=MinSizeRel -p:Platform="x64" -m
接下来,电脑完全卡死,本章结束(×
执行Install后大概2G不到空间,然后就可以八十几个G的生成项目删掉了,手动配置Install的目录到LLVM_DIR环境变量里,以后就可以在CMAKEList里用find_package找到LLVM了(这对编译插件很重要)
编写第一个Pass
Linux编译Pass会比较简单,并且官方支持动态加载Pass模块,所以后续若不做特殊说明都是在Linux环境下调试LLVM Pass so模块文件。
首先先将WSL环境变量配上。
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
然后就需要编译这个Pass,一般有两种编译方法
独立编译成Module
如果要独立编译一个Pass形成so文件,最简单暴力的方法是
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放在同目录下
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
add_library(LLVMMyPass MODULE
# List your source files here.
MyPass.cpp
)
然后修改上层CMakeLists.txt,也就是Transforms目录下的CMakeLists.txt,为其添加一行,将自己写的Pass目录添加进去
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
#include
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
参考资料
https://www.leadroyal.cn/p/647/
https://zhuanlan.zhihu.com/p/68113486
https://www.jianshu.com/p/9f450969121b
https://zhuanlan.zhihu.com/p/122522485
https://llvm.org/docs/WritingAnLLVMPass.html
https://blog.csdn.net/qq_41923691/article/details/123258565