LLVM-NEW-PASS-ALL-IN-ONE

zzzccc Lv1

LLVM NEW PASS ALL IN ONE

学习笔记

LLVM 通过一系列的passes来优化IR文件,不同的Pass作用在IR文件不同粒度上,如Function,Module。而Pass中的操作分为两种, transformation 与 analysis ,分别用于转化IR文件与收集IR文件中的信息,而一系列的pass则合集则被称为pass pipeline

LLVM COMPILE AND INSTALL

这里在win11的wsl2下编译,环境为Ubuntu 22.04,参考 https://llvm.org/docs/CMake.html

浅克隆LLVM,不然我克隆不下来

1
2
git clone --depth 1 -b release/16.x https://github.com/llvm/llvm-project.git
mkdir build

编译命令

1
2
cmake -G Ninja -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TARGETS_TO_BUILD="X86;ARM;AArch64" -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_RTTI=ON -DLLVM_OBFUSCATION_LINK_INTO_TOOLS=
ON -DCMAKE_INSTALL_PREFIX=./build/ ../llvm-project/llvm
1
ninja -j4

编译完成后使用以下命令完成安装

1
ninja install

这里最好还是安装一下,不然没有LLVMConfig.cmake此文件,无法在CMakeLists.txt中使用find_package()

安装成功,可以看到安装位置为前面预设的位置

image-20241015160101155

接下来对编译命令中出现的各项进行解释

-G Ninja:指定生成构建文件的系统为 Ninja。

-DLLVM_ENABLE_PROJECTS="clang;lld":设置了要构建的 LLVM 子项目。

-DLLVM_TARGETS_TO_BUILD="X86;ARM;AArch64 ":这指定了构建 LLVM 时要支持的目标架构。

-DCMAKE_BUILD_TYPE=Release:设置构建类型为 Release,意味着生成的构建将会进行优化,需要调试的话选择Debug,但是编译会慢很多,也需要更大的内存

-DLLVM_INCLUDE_TESTS=OFF:此选项关闭了测试的构建。这样做可以减少构建时间和输出的大小。

-DLLVM_ENABLE_RTTI=ON:这开启了运行时类型信息(Run-Time Type Information,RTTI)。RTTI 允许在运行时识别对象的类型。

../llvm-project/llvm:这指定了源代码目录的路径。CMake 会在这个目录下查找 CMakeLists.txt 文件来确定如何构建项目。

-DCMAKE_INSTALL_PREFIX指定了安装的位置

重点讲下-DLLVM_OBFUSCATION_LINK_INTO_TOOLS=ON,这个选项与New Pass 相适配,设置 LLVM_${NAME}_LINK_INTO_TOOLS,可以将项目转变为静态链接的扩展。这意味着 pass 插件将直接被链接到 LLVM 工具中,而不是作为动态库在运行时加载。(这东西翻了我好久 google上的实现方案都又老又臭 没想到官方已经适配了 不认真读文档的后果www)

The LLVM pass manager

为了提升框架的灵活程度,LLVM将源文件->可执行文件的步骤打碎,分为不同的Passes

image-20241015160149288

而控制这些Passes正确运行的,就是Pass Manager

Pass 往往按照其工作范围分为以下几类:

在 LLVM 中,一个 pass 通常根据其作用的范围来分类:

Module Pass

模块 pass 以整个模块为输入。它在给定模块上执行其工作,可以用于模块内的过程间操作。由于它处理的是整个模块,模块 pass 可以访问和修改模块中的所有函数和全局变量。这使得它特别适合那些需要跨多个函数进行分析或优化的任务。例如,全局常量传播和内联(inlining)优化可以通过模块 pass 来实现。

Call Graph Pass

调用图 pass 操作的是调用图的强连通分量(SCCs)。调用图是一个图结构,其中节点表示函数,边表示函数之间的调用关系。SCC 是图中所有节点互相可达的最大子图。调用图 pass 从底向上遍历这些组件,即从被调用函数开始,逐步向上处理调用它们的函数。这种 pass 特别适合于分析和优化函数间的调用关系,例如函数内联优化或递归调用的优化。

Function Pass

函数 pass 以单个函数为输入,只对该函数进行操作。它在函数的级别上执行分析或转换,不涉及其他函数。这使得函数 pass 简单且高效,因为它只需要处理一个函数的代码,而无需考虑模块中其他部分的状态。典型的函数 pass 例子包括死代码消除(DCE)、常量传播(constant propagation)和循环优化(loop optimization)。

Loop Pass

循环 pass 作用于函数内的循环。它只处理特定循环的内容,进行针对性的优化和转换。例如,循环展开(loop unrolling)、循环分割(loop splitting)和循环向量化(loop vectorization)等优化可以通过循环 pass 实现。循环 pass 需要详细分析和优化循环结构,并且通常只关注单个循环的特性,而不涉及函数的其他部分。

在 LLVM 中,除了操作 IR 代码外,一个 pass 还可能需要获得、更新或丢弃部分或者全部的分析结果。有许多不同的分析,如果一个 pass 需要这些分析结果,它可以从分析管理器请求这些信息。如果信息已经计算好,则会返回缓存的结果。否则,会进行相应的计算。如果一个 pass 改变了 IR 代码,那么它需要声明哪些分析结果被保留,以便在必要时丢弃部分的缓存的分析信息。在底层,pass 管理器确保以下几点:

分析结果在 pass 之间共享

分析结果在不同的 pass 之间共享。这需要跟踪每个 pass 需要哪些分析以及每个分析的状态。其目标是避免不必要的分析预计算,并在可能的情况下尽快释放被分析结果占用的内存。这种机制可以提高编译器的效率,因为它减少了重复计算分析结果的开销。

pass 管理器的执行机制

pass 以流水线方式执行。例如,如果有几个函数 pass 应按顺序执行,pass 管理器会在第一个函数上运行每个函数 pass,然后在第二个函数上运行所有函数 pass,依此类推。这种方法的基本思想是通过在有限的数据集(一个 IR 函数)上执行转换,然后移动到下一个有限的数据集,来改善缓存行为。

Implementing a new pass

这里以一个插桩(instrumentation)Pass为例

代码实现如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/Debug.h"

using namespace llvm;

#define DEBUG_TYPE "ppprofiler"
//LLVM的内置调试基础架构要求我们定义一个调试类型,它是一个字符串。此字符串稍后将显示在打印的统计信息中

ALWAYS_ENABLED_STATISTIC(
NumOfFunc, "Number of instrumented functions.");

namespace {
//在匿名空间内定义对应的Pass类,继承PassInfoMixin,run是其回调方法
class PPProfilerIRPass
: public llvm::PassInfoMixin<PPProfilerIRPass> {
public:
llvm::PreservedAnalyses
run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);

private:
void instrument(llvm::Function &F,
llvm::Function *EnterFn,
llvm::Function *ExitFn);
};
} // namespace

void PPProfilerIRPass::instrument(llvm::Function &F,
Function *EnterFn,
Function *ExitFn) {
++NumOfFunc;

// Set the insertion point to begin of first block.
IRBuilder<> Builder(&*F.getEntryBlock().begin());

// Create global constant for the function name.
GlobalVariable *FnName =
Builder.CreateGlobalString(F.getName());

// Call the EnterFn at function entry.
Builder.CreateCall(EnterFn->getFunctionType(), EnterFn,
{FnName});

// Find all Ret instructions, and call ExitFn before.
for (BasicBlock &BB : F) {
for (Instruction &Inst : BB) {
if (Inst.getOpcode() == Instruction::Ret) {
Builder.SetInsertPoint(&Inst);
Builder.CreateCall(ExitFn->getFunctionType(),
ExitFn, {FnName});
}
}
}
}

PreservedAnalyses
PPProfilerIRPass::run(Module &M,
ModuleAnalysisManager &AM) {
// Do not instrument the runtime functions.
// 避免无线递归
if (M.getFunction("__ppp_enter") ||
M.getFunction("__ppp_exit")) {
return PreservedAnalyses::all();
}

// Create the function type and functions.
Type *VoidTy = Type::getVoidTy(M.getContext());
PointerType *PtrTy =
PointerType::getUnqual(M.getContext());
FunctionType *EnterExitFty =
FunctionType::get(VoidTy, {PtrTy}, false);
Function *EnterFn = Function::Create(
EnterExitFty, GlobalValue::ExternalLinkage,
"__ppp_enter", M);
Function *ExitFn = Function::Create(
EnterExitFty, GlobalValue::ExternalLinkage,
"__ppp_exit", M);

// Call enter function.
for (auto &F : M.functions()) {
if (!F.isDeclaration() && F.hasName())
instrument(F, EnterFn, ExitFn);
}
// 当一个 LLVM pass 运行时,它可能会修改中间表示(IR),从而使某些之前的分析结果失效。
// 为了确保没有错误,代码示例中建议返回 PreservedAnalyses::none(),表示没有保留任何分析结果
// 这样做虽然比较保守(可能会导致重新计算一些分析结果),但可以保证安全性,因为不会依赖已经失效的分析结果。
return PreservedAnalyses::none();
}

void RegisterCB(PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, ModulePassManager &MPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "ppprofiler") {
MPM.addPass(PPProfilerIRPass());
return true;
}
return false;
});
PB.registerPipelineStartEPCallback(
[](ModulePassManager &PM, OptimizationLevel Level) {
PM.addPass(PPProfilerIRPass());
});
}

llvm::PassPluginLibraryInfo getPPProfilerPluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "PPProfiler", "v0.1",
RegisterCB};
}

#ifndef LLVM_PPPROFILER_LINK_INTO_TOOLS
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return getPPProfilerPluginInfo();
}
#endif

有两种方式可以将我们的Pass通过Passbuilder注册,静态注册与动态注册,静态链接需要提供getPluginInfo()函数,而动态链接需要提供llvmGetPassPluginInfo() ,详情参考代码

Adding the pass to the LLVM source tree

Utilizing the plugin mechanisms inside the LLVM source tree

1. 创建新的插件目录和源文件

首先,你需要在 LLVM 源代码树中创建一个新的目录,并将插件源文件放入其中。

  • llvm-project/llvm/lib/Transforms 目录下创建一个名为 PPProfiler 的新目录。
  • PPProfiler.cpp 文件复制到这个新目录中。
1
2
mkdir llvm-project/llvm/lib/Transforms/PPProfiler
cp path/to/PPProfiler.cpp llvm-project/llvm/lib/Transforms/PPProfiler/

2. 创建 CMake 配置文件

PPProfiler 目录中创建一个名为 CMakeLists.txt 的文件,内容如下:

1
add_llvm_pass_plugin(PPProfiler PPProfiler.cpp)

3. 修改上级目录的 CMakeLists.txt 文件

PPProfiler 目录的上一级目录(即 llvm-project/llvm/lib/Transforms)的 CMakeLists.txt 文件中,添加一行以包含新创建的 PPProfiler 目录:

1
add_subdirectory(PPProfiler)

4. 构建并安装 LLVM

进入 LLVM 的构建目录,并运行 ninja install 命令来构建和安装 LLVM,同时包含新的 PPProfiler 插件。

1
2
cd path/to/llvm/build
ninja install

在运行 ninja install 期间,CMake 会检测到构建描述文件的变化,并重新运行配置步骤。你会看到类似如下的输出:

1
-- Registering PPProfiler as a pass plugin (static build: OFF)

这表示插件被检测到并已作为共享库构建。安装步骤完成后,你会在 <install directory>/lib 目录中找到共享库 PPProfiler.so

5. 静态链接插件

如果你希望将插件静态链接到 LLVM 工具中,需要重新运行 CMake 配置,并添加 -DLLVM_PPPROFILER_LINK_INTO_TOOLS=ON 选项:

1
2
3
cd path/to/llvm/build
cmake -DLLVM_PPPROFILER_LINK_INTO_TOOLS=ON ..
ninja install

在运行 ninja install 期间,CMake 会再次检测到构建描述文件的变化,你会看到类似如下的输出:

1
-- Registering PPProfiler as a pass plugin (static build: ON)

6. 检查静态链接的结果

编译和安装 LLVM 后,以下变化将发生:

  • 插件被编译成静态库 libPPProfiler.a,并安装到 <install directory>/lib 目录中。
  • LLVM 工具(如 opt)将链接到该静态库。
  • 插件注册为扩展,你可以在 <install directory>/include/llvm/Support/Extension.def 文件中看到如下行:
1
HANDLE_EXTENSION(PPProfiler)

Fully integrating the pass into the pass registry

这段文字详细描述了如何将一个新的 LLVM pass 完全集成到 LLVM 系统中,而不仅仅是作为一个插件。以下是详细的解释和逐步操作指南:

1. 创建头文件

首先,你需要在 llvm-project/llvm/include/llvm/Transforms/PPProfiler 目录下创建一个新的头文件 PPProfiler.h,并在其中定义 pass 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef LLVM_TRANSFORMS_PPPROFILER_PPPROFILER_H
#define LLVM_TRANSFORMS_PPPROFILER_PPPROFILER_H

#include "llvm/IR/PassManager.h"

namespace llvm {

class PPProfilerIRPass : public llvm::PassInfoMixin<PPProfilerIRPass> {
public:
llvm::PreservedAnalyses run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);

private:
void instrument(llvm::Function &F, llvm::Function *EnterFn, llvm::Function *ExitFn);
};

} // namespace llvm

#endif
解释:
  • **#ifndef / #define**:防止头文件重复包含。
  • llvm 命名空间:将类定义放在 llvm 命名空间中。
  • PPProfilerIRPass:定义了 run 方法和 instrument 私有方法。

2. 更新源文件

PPProfiler.cpp 文件复制到 llvm-project/llvm/lib/Transforms/PPProfiler 目录中,并进行如下更新:

  1. 删除类定义,因为它现在已经在头文件中定义。
  2. 添加头文件的 #include 指令。
1
2
3
#include "llvm/Transforms/PPProfiler/PPProfiler.h"

// Implementation of the run method and other functions.

3. 更新 CMake 配置文件

llvm-project/llvm/lib/Transforms/PPProfiler 目录中创建一个新的 CMakeLists.txt 文件,内容如下:

1
2
3
4
5
6
add_llvm_component_library(LLVMPPProfiler
PPProfiler.cpp
LINK_COMPONENTS
Core
Support
)
解释:
  • **add_llvm_component_library**:声明一个新的 LLVM 组件库。
  • **LLVMPPProfiler**:新组件的名称。
  • **PPProfiler.cpp**:包含的源文件。
  • **LINK_COMPONENTS**:依赖的 LLVM 组件。

llvm-project/llvm/lib/Transforms 目录中的 CMakeLists.txt 文件中,添加如下行以包含新的源目录:

1
add_subdirectory(PPProfiler)

4. 更新

llvm/lib/Passes/PassRegistry.def 文件中,找到模块 pass 定义部分(可以搜索 MODULE_PASS 宏),并添加如下行:

1
MODULE_PASS("ppprofiler", PPProfilerIRPass())
解释:
  • **MODULE_PASS**:宏定义模块级别的 pass。
  • **"ppprofiler"**:pass 的名称。
  • **PPProfilerIRPass()**:pass 类的实例。

5. 更新

llvm/lib/Passes/PassBuilder.cpp 文件中,包含新的头文件:

1
#include "llvm/Transforms/PPProfiler/PPProfiler.h"

6. 更新 CMakeLists.txt 文件中的依赖

llvm/lib/Passes/CMakeLists.txt 文件中,添加对新组件的依赖:

1
2
3
LINK_COMPONENTS
...
PPProfiler

7. 构建和安装 LLVM

进入 LLVM 的构建目录,并运行 ninja install 命令来构建和安装包含新 pass 的 LLVM:

1
2
cd path/to/llvm/build
ninja install

8. 验证新 pass

通过前面的步骤,你已经将新的 ppprofiler pass 完全集成到 LLVM 中。这个 pass 现在对所有 LLVM 工具都可用,并且已编译成 libLLVMPPProfiler.a 库,可以作为 PPProfiler 组件在构建系统中使用。

Using the ppprofiler pass with LLVM tools

Run the pass plugin in opt

这段文字详细描述了如何使用 LLVM 工具链来加载和运行一个自定义 pass,并如何通过运行时支持来记录函数进入和退出事件的简单实现。以下是详细解释和逐步操作指南:

1. 使用 opt 工具运行 pass

首先,我们需要使用 opt 工具加载并运行自定义 pass:

1
$ opt --load-pass-plugin=./PPProfile.so --passes="ppprofiler" --stats hello.ll -o hello_inst.bc
解释:
  • **--load-pass-plugin=./PPProfile.so**:加载包含自定义 pass 的共享库。
  • **--passes="ppprofiler"**:指定要运行的 pass 名称。
  • **--stats**:启用统计信息收集。
  • **hello.ll**:输入的 LLVM IR 文件。
  • **-o hello_inst.bc**:输出优化后的 bitcode 文件。

2. 查看统计信息

如果统计信息收集启用,你会看到如下输出:

1
2
3
4
===--------------------------------------------------------===
... Statistics Collected ...
===--------------------------------------------------------===
1 ppprofiler - Number of instrumented functions.

如果统计信息未启用,你会看到提示信息:

1
Statistics are disabled. Build with asserts or with -DLLVM_FORCE_ENABLE_STATS

3. 使用 llvm-dis 工具查看生成的 IR

使用 llvm-dis 工具将 bitcode 文件转换为可读的 LLVM IR:

1
$ llvm-dis hello_inst.bc -o -

这将输出类似如下内容:

1
2
3
4
5
6
7
8
9
@.str = private unnamed_addr constant [6 x i8] c"Hello\00", align 1
@0 = private unnamed_addr constant [5 x i8] c"main\00", align 1

define dso_local i32 @main(i32 noundef %0, ptr nocapture noundef readnone %1) {
call void @__ppp_enter(ptr @0)
%3 = tail call i32 @puts(ptr noundef nonnull dereferenceable(1) @.str)
call void @__ppp_exit(ptr @0)
ret i32 0
}

4. 实现运行时支持

为了将 IR 转换为可执行文件并运行,我们需要提供 __ppp_enter__ppp_exit 函数的实现。创建一个名为 runtime.c 的文件,内容如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 文件描述符和清理函数
static FILE *FileFD = NULL;

static void cleanup() {
if (FileFD != NULL) {
fclose(FileFD);
FileFD = NULL;
}
}

// 初始化函数,打开文件并注册清理函数
static void init() {
if (FileFD == NULL) {
FileFD = fopen("ppprofile.csv", "w");
atexit(&cleanup);
}
}

// 获取时间戳的函数
typedef unsigned long long Time;

static Time get_time() {
struct timespec ts;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts);
return 1000000000L * ts.tv_sec + ts.tv_nsec;
}

// 进入函数,记录事件
void __ppp_enter(const char *FnName) {
init();
Time T = get_time();
void *Frame = __builtin_frame_address(1);
fprintf(FileFD, "enter|%s|%llu|%p\n", FnName, T, Frame);
}

// 退出函数,记录事件
void __ppp_exit(const char *FnName) {
init();
Time T = get_time();
void *Frame = __builtin_frame_address(1);
fprintf(FileFD, "exit|%s|%llu|%p\n", FnName, T, Frame);
}

5. 编译和运行

将 instrumented bitcode 文件与运行时代码一起编译,并运行生成的可执行文件:

1
2
$ clang hello_inst.bc runtime.c
$ ./a.out

这将生成一个名为 ppprofile.csv 的文件,内容如下:

1
2
3
$ cat ppprofile.csv
enter|main|3300868|0x1
exit|main|3760638|0x1

SPECIFYING A PASS PIPELINE

可以使用 --passes 选项指定一个包含多个 pass 的管道。例如,运行 ppprofiler pass 然后运行默认的 O2 优化管道,可以使用以下命令

1
$ opt --load-pass-plugin=./PPProfile.so --passes="ppprofiler,default<O2>" --stats hello.ll -o hello_inst.bc

Plugging the new pass into clang

LLVM Pass Manger有着默认的Pass执行流,被称作 default pass pipeline,我们可以使用opt指定pipeline,但是,当我们想要在非常特定的点加入我们的Pass的时候,比如在loop optimization processes 的结尾。而我们可以通过PassBuilder在这些 extension points进行注册我们的Pass,比如我们可以通过**registerPipelineStartEPCallback()**来将我们的Pass添加到优化管道的开始。每当pass manager填充默认pass管道时,它都会调用扩展点的所有回调。

可以通过以下方式使用

1
clang -fpass-plugin=./PPProfiler.so hello.c runtime.c

集成到更大的项目中

要将这些步骤集成到更大的项目中,可以在 CMake 命令行中指定编译器和链接器标志。例如,假设要编译 tinylang 编译器:

1
2
-DCMAKE_CXX_FLAGS="-fpass-plugin=<PluginPath>/PPProfiler.so"
-DCMAKE_EXE_LINKER_FLAGS="<RuntimePath>/runtime.o"

其中 <PluginPath><RuntimePath> 分别替换为插件和运行时代码的绝对路径。

同时,确保使用 Clang 作为编译器:

1
2
export CC=clang
export CXX=clang++

How to make a plugin out of the source code

自己总结的一章,总在源码树里编译不太舒服

这里依旧使用以上代码,即

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/Debug.h"

using namespace llvm;

#define DEBUG_TYPE "ppprofiler"
//LLVM的内置调试基础架构要求我们定义一个调试类型,它是一个字符串。此字符串稍后将显示在打印的统计信息中

ALWAYS_ENABLED_STATISTIC(
NumOfFunc, "Number of instrumented functions.");

namespace {
//在匿名空间内定义对应的Pass类,继承PassInfoMixin,run是其回调方法
class PPProfilerIRPass
: public llvm::PassInfoMixin<PPProfilerIRPass> {
public:
llvm::PreservedAnalyses
run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);

private:
void instrument(llvm::Function &F,
llvm::Function *EnterFn,
llvm::Function *ExitFn);
};
} // namespace

void PPProfilerIRPass::instrument(llvm::Function &F,
Function *EnterFn,
Function *ExitFn) {
++NumOfFunc;

// Set the insertion point to begin of first block.
IRBuilder<> Builder(&*F.getEntryBlock().begin());

// Create global constant for the function name.
GlobalVariable *FnName =
Builder.CreateGlobalString(F.getName());

// Call the EnterFn at function entry.
Builder.CreateCall(EnterFn->getFunctionType(), EnterFn,
{FnName});

// Find all Ret instructions, and call ExitFn before.
for (BasicBlock &BB : F) {
for (Instruction &Inst : BB) {
if (Inst.getOpcode() == Instruction::Ret) {
Builder.SetInsertPoint(&Inst);
Builder.CreateCall(ExitFn->getFunctionType(),
ExitFn, {FnName});
}
}
}
}

PreservedAnalyses
PPProfilerIRPass::run(Module &M,
ModuleAnalysisManager &AM) {
// Do not instrument the runtime functions.
// 避免无线递归
if (M.getFunction("__ppp_enter") ||
M.getFunction("__ppp_exit")) {
return PreservedAnalyses::all();
}

// Create the function type and functions.
Type *VoidTy = Type::getVoidTy(M.getContext());
PointerType *PtrTy =
PointerType::getUnqual(M.getContext());
FunctionType *EnterExitFty =
FunctionType::get(VoidTy, {PtrTy}, false);
Function *EnterFn = Function::Create(
EnterExitFty, GlobalValue::ExternalLinkage,
"__ppp_enter", M);
Function *ExitFn = Function::Create(
EnterExitFty, GlobalValue::ExternalLinkage,
"__ppp_exit", M);

// Call enter function.
for (auto &F : M.functions()) {
if (!F.isDeclaration() && F.hasName())
instrument(F, EnterFn, ExitFn);
}
// 当一个 LLVM pass 运行时,它可能会修改中间表示(IR),从而使某些之前的分析结果失效。
// 为了确保没有错误,代码示例中建议返回 PreservedAnalyses::none(),表示没有保留任何分析结果
// 这样做虽然比较保守(可能会导致重新计算一些分析结果),但可以保证安全性,因为不会依赖已经失效的分析结果。
return PreservedAnalyses::none();
}

void RegisterCB(PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, ModulePassManager &MPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "ppprofiler") {
MPM.addPass(PPProfilerIRPass());
return true;
}
return false;
});
PB.registerPipelineStartEPCallback(
[](ModulePassManager &PM, OptimizationLevel Level) {
PM.addPass(PPProfilerIRPass());
});
}

llvm::PassPluginLibraryInfo getPPProfilerPluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "PPProfiler", "v0.1",
RegisterCB};
}

#ifndef LLVM_PPPROFILER_LINK_INTO_TOOLS
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return getPPProfilerPluginInfo();
}
#endif

同一目录下CMakeLists.txt如下

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
cmake_minimum_required(VERSION 3.20.0)
project(ppprofilerpass)
# 安装了才有
# 设置 CMAKE_PREFIX_PATH 以包含特定的 LLVM 路径
set(LLVM_DIR "/home/zzzccc/llvm-project/build/lib/cmake/llvm")
find_package(LLVM REQUIRED CONFIG)

# list(APPEND CMAKE_MODULE_PATH "/home/zzzccc/llvm-project/llvm/cmake/modules")
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")

# include(ChooseMSVCCRT)
include(AddLLVM)
include(HandleLLVMOptions)

include_directories("${LLVM_INCLUDE_DIR}")
add_definitions("${LLVM_DEFINITIONS}")

link_directories("${LLVM_LIBRARY_DIR}")

add_llvm_pass_plugin(PPProfiler
MODULE
PPProfiler.cpp

DEPENDS
intrinsics_gen
)

这里CMakeLists搞了好一阵子,主要是研究了下find_package的用法,参考以下两篇文章:https://blog.csdn.net/qq_39466755/article/details/130912344https://zhuanlan.zhihu.com/p/50829542

这里首先明确find_package有两种模式,如下

module模式
在这个模式下会查找一个名为find.cmake的文件,首先去CMAKE_MODULE_PATH指定的路径下去查找,然后去cmake安装提供的查找模块中查找(安装cmake时生成的一些cmake文件)。找到之后会检查版本,生成一些需要的信息。

config模式
在这个模式下会查找一个名为-config.cmake(<小写包名>-config.cmake)或者Config.cmake 的文件,如果指定了版本信息也会搜索名为-config-version.cmake 或者 ConfigVersion.cmake的文件。

而LLVM编译安装后会在/install/lib/llvm下存在相应的LLVMConfig.cmake,而其主要搜寻路径如下

1
2
3
4
5
<package>_DIR
CMAKE_PREFIX_PATH
CMAKE_FRAMEWORK_PATH
CMAKE_APPBUNDLE_PATH
PATH

故只需要设置LLVM_DIR并将find_package改为搜寻config模式即可

1
2
set(LLVM_DIR "/home/zzzccc/llvm-project/build/lib/cmake/llvm")
find_package(LLVM REQUIRED CONFIG)

编译通过

image-20241015160214493

测试是否能够正常使用

测试代码如下:

1
2
3
4
5
#include <stdio.h>
int main(){
printf("hello world!");
return 0;
}

使用以下命令编译

1
/home/zzzccc/llvm-project/build/bin/clang -S -emit-llvm test.c -o test.ll

结果如下

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
; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

@.str = private unnamed_addr constant [13 x i8] c"hello world!\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, ptr %1, align 4
%2 = call i32 (ptr, ...) @printf(ptr noundef @.str)
ret i32 0
}

declare i32 @printf(ptr noundef, ...) #1

attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }

!llvm.module.flags = !{!0, !1, !2, !3, !4}
!llvm.ident = !{!5}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 8, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{i32 7, !"uwtable", i32 2}
!4 = !{i32 7, !"frame-pointer", i32 2}
!5 = !{!"clang version 18.1.2 (https://github.com/llvm/llvm-project.git ea6c457b8dd2d0e6a7f05b4a5bdd2686085e1ec0)"}

使用以下命令

1
/home/zzzccc/llvm-project/build/bin/opt --load-pass-plugin=./PPProfiler.so --passes="ppprofiler" --stats test.ll -o test.bc
1
/home/zzzccc/llvm-project/build/bin/llvm-dis test.bc -o test_1.ll

可以看到优化后的IR文件如下:

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
31
32
33
34
35
36
; ModuleID = 'test.bc'
source_filename = "test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

@.str = private unnamed_addr constant [13 x i8] c"hello world!\00", align 1
@0 = private unnamed_addr constant [5 x i8] c"main\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
call void @__ppp_enter(ptr @0)
%1 = alloca i32, align 4
store i32 0, ptr %1, align 4
%2 = call i32 (ptr, ...) @printf(ptr noundef @.str)
call void @__ppp_exit(ptr @0)
ret i32 0
}

declare i32 @printf(ptr noundef, ...) #1

declare void @__ppp_enter(ptr)

declare void @__ppp_exit(ptr)

attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }

!llvm.module.flags = !{!0, !1, !2, !3, !4}
!llvm.ident = !{!5}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 8, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{i32 7, !"uwtable", i32 2}
!4 = !{i32 7, !"frame-pointer", i32 2}
!5 = !{!"clang version 18.1.2 (https://github.com/llvm/llvm-project.git ea6c457b8dd2d0e6a7f05b4a5bdd2686085e1ec0)"}

成功了捏

Adding an optimization pipeline to your compiler

后续关于Pipeline的这部分似乎还没看到啥资料,会更加认真的学习(但是还是有需要了再补吧)

参考

主要参考《Learn_LLVM_17_A_beginner’s_guide_to_learning_LLVM_2nd》

  • Title: LLVM-NEW-PASS-ALL-IN-ONE
  • Author: zzzccc
  • Created at : 2024-12-27 14:11:47
  • Updated at : 2024-12-27 14:33:33
  • Link: http://blog.yghde.cc/2024/12/27/LLVM-NEW-PASS-ALL-IN-ONE/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments