LLVM:从零开始实现 Function Pass

本文是我在学习 LLVM 的过程中,从零开始实现一个 Function Pass 的过程。具有如下特点:

  1. 基于 apt 安装 LLVM。不需要构建源码

  2. 使用 C++14 标准。使用 LLVM 14 版本。

  3. 使用新的 PassInfoMixin 机制。不使用旧的 PassManager 机制。

  4. 使用最简化的 Makefile/CMake 更好地理解编译过程。

  5. 使用 Ubuntu 上的 VSCode 进行开发,完善的开发体验。

LLVM 的安装

首先你需要安装 LLVM,我直接用 apt 安装了。

1sudo apt install llvm-14

对于我现在版本的 Ubuntu,头文件和库文件在 /usr/include/llvm-14/usr/lib/llvm-14 下。

VSCode 的配置

mkdir mypass-project 用于存放项目文件。

如下配置可以让 VSCode 识别 LLVM 头文件。

Path .vscode/c_cpp_properties.json

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/usr/include/llvm-14/**",
                "/usr/include/llvm-c-14/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c17",
            "cppStandard": "c++14",
            "intelliSenseMode": "linux-clang-x64"
        }
    ],
    "version": 4
}

编写代码

接下来是 src 目录,用于存放源文件。

Path src/MyPass.h

 1#pragma once
 2
 3#include "llvm/IR/PassManager.h"
 4#include "llvm/Passes/PassBuilder.h"
 5#include "llvm/Passes/PassPlugin.h"
 6#include "llvm/Support/raw_ostream.h"
 7
 8namespace llvm {
 9
10class MyPass : public PassInfoMixin<MyPass> {
11 public:
12  PreservedAnalyses run(Function& F, FunctionAnalysisManager& AM);
13};
14
15}  // namespace llvm
16
17// Register the pass
18extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK
19llvmGetPassPluginInfo() {
20  return {
21    LLVM_PLUGIN_API_VERSION, "MyPass", "v0.1",
22    [](llvm::PassBuilder &PB) {
23      PB.registerPipelineParsingCallback(
24        [](llvm::StringRef Name, llvm::FunctionPassManager &FPM,
25           llvm::ArrayRef<llvm::PassBuilder::PipelineElement>) {
26          if(Name == "mypass"){
27            FPM.addPass(llvm::MyPass());
28            return true;
29          }
30          return false;
31        }
32      );
33    }
34  };
35}

Path src/MyPass.cpp

这里我们实现一个功能:每个函数重命名为 f0f1f2f3……。

1#include "MyPass.h"
2namespace llvm {
3static int f_number = 0;
4PreservedAnalyses MyPass::run(Function& F, FunctionAnalysisManager& AM) {
5  // rename function to f_number
6  F.setName("f" + std::to_string(f_number++));
7  return PreservedAnalyses::all();
8}
9}  // namespace llvm

以及一个 build 目录,用于存放编译后的文件。

使用 Makefile 构建

在项目根目录编写 Makefile,用于编译和链接。

Path Makefile

 1CXX = clang++
 2CXXFLAGS = -O3 -fPIC -fno-rtti $(shell llvm-config --cxxflags)
 3LDFLAGS = -shared $(shell llvm-config --ldflags)
 4
 5TARGET = build/libmypass.so
 6SOURCES = MyPass.cpp
 7OBJECTS = $(SOURCES:.cpp=.o)
 8OBJECTS := $(addprefix build/,$(OBJECTS))
 9SOURCES = $(addprefix src/,$(SOURCES))
10
11all: $(TARGET)
12
13build_dir:
14	@mkdir -p build
15
16$(TARGET): $(OBJECTS) build_dir
17	$(CXX) $(OBJECTS) $(LDFLAGS) -o $@
18
19build/%.o: src/%.cpp build_dir
20	$(CXX) $(CXXFLAGS) -c $< -o $@
21
22clean:
23	rm -f $(TARGET) $(OBJECTS)
24
25.PHONY: all clean

然后执行 make 即可。成功之后生成 build/mypass.so

使用 CMake 构建

更常用的是使用 CMake 构建。

Path CMakeLists.txt

 1cmake_minimum_required(VERSION 3.10)
 2project(MyPass)
 3
 4set(CMAKE_CXX_STANDARD 14)
 5
 6set(LLVM_PATH /usr/lib/llvm-14)
 7
 8include_directories(${LLVM_PATH}/include)
 9
10add_library(mypass MODULE src/MyPass.cpp)
11
12set_target_properties(mypass PROPERTIES
13        COMPILE_FLAGS "-fno-rtti"
14        )
15
16target_link_libraries(mypass ${LLVM_PATH}/lib/libLLVM-14.so)

然后执行如下命令配置:

1cmake -B build

执行如下命令构建:

1cmake --build build

成功之后生成 build/libmypass.so

使用 LLVM Opt 执行 Pass

写一个测试用的 C 代码。

Path tmp/foo.c

1int f1(int a, int b, int c) {
2  return a + b + c;
3}
4
5int f2(int a, int b) {
6  return f1(a, b, 0);
7}

编译为 LLVM IR,注意去掉 optnone。

1clang -S -emit-llvm -O0 -Xclang -disable-O0-optnone tmp/foo.c -o tmp/foo.ll

执行 Pass。

1opt -S -load-pass-plugin=build/libmypass.so -passes="mypass" tmp/foo.ll -o tmp/foo_mypass.ll

效果

运行前:

Path tmp/foo.ll

 1; Function Attrs: noinline nounwind uwtable
 2define dso_local i32 @three_sum(i32 noundef %0, i32 noundef %1, i32 noundef %2) #0 {
 3  %4 = alloca i32, align 4
 4  %5 = alloca i32, align 4
 5  %6 = alloca i32, align 4
 6  store i32 %0, i32* %4, align 4
 7  store i32 %1, i32* %5, align 4
 8  store i32 %2, i32* %6, align 4
 9  %7 = load i32, i32* %4, align 4
10  %8 = load i32, i32* %5, align 4
11  %9 = add nsw i32 %7, %8
12  %10 = load i32, i32* %6, align 4
13  %11 = add nsw i32 %9, %10
14  ret i32 %11
15}
16
17; Function Attrs: noinline nounwind uwtable
18define dso_local i32 @two_sum(i32 noundef %0, i32 noundef %1) #0 {
19  %3 = alloca i32, align 4
20  %4 = alloca i32, align 4
21  store i32 %0, i32* %3, align 4
22  store i32 %1, i32* %4, align 4
23  %5 = load i32, i32* %3, align 4
24  %6 = load i32, i32* %4, align 4
25  %7 = call i32 @three_sum(i32 noundef %5, i32 noundef %6, i32 noundef 0)
26  ret i32 %7
27}

运行后:

Path tmp/foo_mypass.ll

 1; Function Attrs: noinline nounwind uwtable
 2define dso_local i32 @f0(i32 noundef %0, i32 noundef %1, i32 noundef %2) #0 {
 3  %4 = alloca i32, align 4
 4  %5 = alloca i32, align 4
 5  %6 = alloca i32, align 4
 6  store i32 %0, i32* %4, align 4
 7  store i32 %1, i32* %5, align 4
 8  store i32 %2, i32* %6, align 4
 9  %7 = load i32, i32* %4, align 4
10  %8 = load i32, i32* %5, align 4
11  %9 = add nsw i32 %7, %8
12  %10 = load i32, i32* %6, align 4
13  %11 = add nsw i32 %9, %10
14  ret i32 %11
15}
16
17; Function Attrs: noinline nounwind uwtable
18define dso_local i32 @f1(i32 noundef %0, i32 noundef %1) #0 {
19  %3 = alloca i32, align 4
20  %4 = alloca i32, align 4
21  store i32 %0, i32* %3, align 4
22  store i32 %1, i32* %4, align 4
23  %5 = load i32, i32* %3, align 4
24  %6 = load i32, i32* %4, align 4
25  %7 = call i32 @f0(i32 noundef %5, i32 noundef %6, i32 noundef 0)
26  ret i32 %7
27}

参考资料

  1. Writing an LLVM Pass — LLVM 16.0.0git documentation

  2. abenkhadra/llvm-pass-tutorial: A step-by-step tutorial for building an LLVM sample pass

  3. Adrian Sampson: LLVM for Grad Students