llvm 学习(二):编译、加载单独的Pass

llvm系列文章,本文讲一下Pass的编译和加载。网上ollvm等作品都是整个llvm都丢到 git 里了,非常不优雅,所以本文讲一下如何编译、加载单独的 Pass。

一、准备环境

本文环境为:

  • MacOS
  • 已按照第一篇安装好clang+llvm,使用7.0
  • 已安装CMake
  • 设置了一个环境变量,叫$LLVM_HOME,指向安装好的目录,文件结构如下:
1
2
3
4
5
6
7
8
9
➜  /tmp lsa $LLVM_HOME
total 0
drwxr-xr-x 7 leadroyal staff 224B 10 9 16:32 .
drwxr-xr-x 6 leadroyal staff 192B 10 10 13:28 ..
drwxr-xr-x 77 leadroyal staff 2.4K 10 9 16:33 bin
drwxr-xr-x 6 leadroyal staff 192B 10 9 16:32 include
drwxr-xr-x 173 leadroyal staff 5.4K 10 9 16:33 lib
drwxr-xr-x 4 leadroyal staff 128B 10 9 16:32 libexec
drwxr-xr-x 7 leadroyal staff 224B 10 9 16:33 share

将来编写代码时,要用到里面的一些头文件、库文件、可执行文件等。

本文的代码在 https://github.com/LeadroyaL/llvm-pass-tutorial 的 master 分支里。

二、编写CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
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_subdirectory(skeleton) # Use your pass name here.

前面几句的逻辑就是,首先得设置LLVM_HOME这个环境变量,当然你用别的也行,这个我用起来舒服。

注意set(ENV{LLVM_DIR} $ENV{LLVM_HOME}/lib/cmake/llvm) ,在低版本可能不是这个目录,指向的那个目录里有LLVM-Config.cmake 还是LLVMConfig.cmake 来着,反正找不到的话cmake会报错的,可以搜一下这两个文件在哪。

我对CMake也不熟,前几句就是使用llvm提供的一些环境,最后那句add_subdirectory 是使用子目录的CMake 工程。接下来贴子目录的 CMake 内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
add_library(SkeletonPass MODULE
# List your source files here.
Skeleton.cpp
)

# Use C++11 to compile your pass (i.e., supply -std=c++11).
target_compile_features(SkeletonPass PRIVATE cxx_range_for cxx_auto_type)

# LLVM is (typically) built with no C++ RTTI. We need to match that;
# otherwise, we'll get linker errors about missing RTTI data.
set_target_properties(SkeletonPass PROPERTIES
COMPILE_FLAGS "-fno-rtti"
)

# Get proper shared-library behavior (where symbols are not necessarily
# resolved when the shared library is linked) on OS X.
if(APPLE)
set_target_properties(SkeletonPass PROPERTIES
LINK_FLAGS "-undefined dynamic_lookup"
)
endif(APPLE)

这个非常简单,就很简单的编译一个叫 libSkeletonPass.so 的共享库出来,源文件是 Skeleton.cpp 。

三、编写代码

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
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
using namespace llvm;

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

virtual bool runOnFunction(Function &F) {
errs() << "I saw a function called " << F.getName() << "!n";
return false;
}
};
}

char SkeletonPass::ID = 0;

// Automatically enable the pass.
// http://adriansampson.net/blog/clangpass.html
static void registerSkeletonPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new SkeletonPass());
}
static RegisterStandardPasses
RegisterMyPass(PassManagerBuilder::EP_EarlyAsPossible,
registerSkeletonPass);

稍微解释一下,定义了一个叫 SkeletonPass ,继承了 FunctionPass 。里面有个被重写的方法,runOnFunction ,里面就是真正的代码了,打印一下函数名就 return 掉。

后面有个标准的入口,本文暂时不讲,就当时一个提供给 llvm 的入口标记即可。

四、加载 so 文件

2022年05月07日更新:在 llvm13、llvm14 上,该方案已是即将被官方废弃的方案,临时解决方案是加参数-flegacy-pass-manager

编译的方法有很多,我一般用 CLion 上面那个编译按钮,(不是运行按钮),之后在附近的目录里翻一下,就可以翻出来。 在 clang 时候添加一些额外的参数,大概长下面这个样子

1
$LLVM_HOME/bin/clang -Xclang -load -XClang build/skeleton/libSkeletonPass.so /tmp/test.c

核心的是

1
-Xclang -load -XClang build/skeleton/libSkeletonPass.so

执行一下,有下面的结果

1
2
3
4
➜  /tmp $LLVM_HOME/bin/clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include -Xclang -load -Xclang /Users/leadroyal/CLion_code/llvm-pass-tutorial/cmake-build-debug/skeleton/libSkeletonPass.so -w test.c -o test.bin
I saw a function called func1!
I saw a function called func2!
I saw a function called main!

嗯,说明这个Pass被正确加载了。 这里千万不能使用 /usr/bin/clang ,因为 apple-clang 和 开源clang 完全是两个东西,无法加载我们编译出来的 Pass,会出现符号找不到。

1
2
3
4
5
6
➜  /tmp /usr/bin/clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include -Xclang -load -Xclang /Users/leadroyal/CLion_code/llvm-pass-tutorial/cmake-build-debug/skeleton/libSkeletonPass.so -w test.c -o test.bin
error: unable to load plugin '/Users/leadroyal/CLion_code/llvm-pass-tutorial/cmake-build-debug/skeleton/libSkeletonPass.so':
'dlopen(/Users/leadroyal/CLion_code/llvm-pass-tutorial/cmake-build-debug/skeleton/libSkeletonPass.so, 9): Symbol not found: __ZN4llvm23EnableABIBreakingChecksE
Referenced from: /Users/leadroyal/CLion_code/llvm-pass-tutorial/cmake-build-debug/skeleton/libSkeletonPass.so
Expected in: flat namespace
in /Users/leadroyal/CLion_code/llvm-pass-tutorial/cmake-build-debug/skeleton/libSkeletonPass.so'

2019年09月08日更新,最近有读者问到这个符号报错如何解决:

一定要用对应版本的Clang去加载对应版本的 Pass,否则可能符号出错,比如下面的。

关于 __ZN4llvm23EnableABIBreakingChecksE__ZN4llvm24DisableABIBreakingChecksE 符号,原因是 Debug 版本和 Release 版本编译出来的符号不一致,与这几个llvm的编译配置有关:LLVM_ENABLE_ABI_BREAKING_CHECKS LLVM_ENABLE_ASSERTIONS因此在使用不配套的 llvm 套件时,会出现该符号找不到的情况

对此的解决方案是:一定要使用成套的 llvm组件,不要混用自己编译出来的组件和 Mac 自带的组件,很容易出问题,在环境变量 PATH 上把编译出来的 bin 目录提到前面,就可以解决这个问题。

2019年11月15日更新,符号报错原因和解决方案均可在本系列第十二篇找到,如果是新的 case 请在第十二篇留言或者直接邮箱交流。

五、结论,参考链接

嗯,这样基本的Pass 开发环境就做好了,编译、加载一条龙,非常方便。

主要参考链接是

https://github.com/abenkhadra/llvm-pass-tutorial

http://llvm.org/docs/CMake.html#cmake-out-of-source-pass

本文的源码在

https://github.com/LeadroyaL/llvm-pass-tutorial