写在前面
这篇文章的前半部分包括写的环境搭建都是基于 LLVM 源码的构建方式,重点在于帮助了解 llvm 的结构,如果目的只是编写 Pass 的话,直接跳到 自己编写 LLVM Pass 部分即可
LLVM 环境搭建
由于我的 Ubuntu 版本不够,无法直接 apt install llvm-20,所以就去 https://github.com/llvm/llvm-project.git 里面下载了源码,解压到 wsl 下
更新包列表
sudo apt-get update
安装基础编译工具(包含g++, make等)
sudo apt-get install build-essential
安装 CMake
sudo apt-get install cmake
安装 Ninja
sudo apt-get install ninja-build
安装 Clang
sudo apt-get install clang
安装其他依赖
sudo apt-get install python3 zlib1g-dev libtinfo-dev
进入 llvm 源码目录
1
| cd llvm-project-llvmorg-20.1.8
|
配置并生成构建文件
1
| cmake -S llvm -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang"
|
进入 build 目录,并行使用 8 个线程进行构建
把 llvm 源码导入 CLion 中,找到 llvm-project-llvmorg-20.1.8\llvm 目录下的 CMakeLists.txt,打开
在设置里的 构造、执行、部署 中点开 CMake,点 + 添加,在 CMake 选项中输入之前的命令
cmake -S llvm -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang"
,
确认后发现多了两个文件夹 cmake-build-debug 和 cmake-build-release,然后 ninja -j8 编译,但是我这里好像是因为当前执行命令的目录路径 (/home/eleven/…) 和它在 CMakeCache.txt 文件中记录的路径 (//wsl.localhost/Ubuntu/home/eleven/…) 不一致,编译失败了,然后就删了重新构建一次
1 2 3 4
| cd ~/llvm-project-llvmorg-20.1.8/llvm/cmake-build-release rm -rf CMakeCache.txt CMakeFiles/ cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release ninja -j8
|
https://llvm.org/docs/GettingStarted.html#llvm-tools
先编写一个简单的 c 语言脚本
1 2 3 4 5 6 7
| #include <stdio.h>
int main() { printf("Hello, LLVM!\n"); return 0; }
|
用 clang 编译
clang ez.c -o ez
clang 默认方式与 GCC 相同,标准 -S 和 -c 参数分别生成 .s 和 .o 文件
把 c 源代码编译成 LLVM IR
clang -emit-llvm -S ez.c -o ez.ll
-emit-llvm 告诉编译器不要生成机器码(如 .o 或可执行文件),而是生成 LLVM IR
-S 告诉编译器,生成的 LLVM IR 应该以文本格式输出,而不是默认的二进制字节码格式
.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
| source_filename = "ez.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-pc-linux-gnu"
@.str = private unnamed_addr constant [14 x i8] c"Hello, LLVM!\0A\00", align 1
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 = !{!"Ubuntu clang version 18.1.3 (1ubuntu1)"}
|
执行 .ll 文件 : lli ez.ll
lli 是 LLVM Interpreter(LLVM 解释器) 的缩写,是一个命令行工具,能够直接执行 LLVM IR代码
把 c 源码编译成 Bitcode
clang -O3 -emit-llvm ez.c -c -o ez.bc
-O 表示优化,3表示这是最高级别的优化
-emit-llvm 作用同上,告诉编译器生成 LLVM IR
-c 告诉编译器只进行编译,不进行链接,即生成一个目标文件而不是可执行文件
.bc 文件是 LLVM 字节码(Bitcode)的缩写,它与之前的 .ll 文件都属于 LLVM IR,但是
.ll
文件是 LLVM IR 的文本格式,人类可读,并且可以使用文本编译器查看内容
.bc
文件是 LLVM IR 的二进制格式,人类不可读,但文件体积更小,加载和处理速度更快,更适合作为编译器后端的输入
llvm 中提供了 llvm-as (汇编器) 和 llvm -dis (反汇编器),让两种形式的文件能相互转化
把 .ll 文件转化为 .bc 文件
llvm-as ez.ll -o ez.bc
把 .bc 文件转化为 .ll 文件
llvm-dis ez.bc -o ez.ll
llvm-dis < ez.bc | less
llvm-dis 是 LLVM 的反汇编工具,用于将 .bc 文件转换为 .ll 文件
< 是一个重定向操作符,告诉 llvm-dis 命令,从标准输入(stdin)而不是从命令行参数中读取数据
| 是 管道 符号,它将左边命令的标准输出(stdout)作为右边命令的标准输入
less 是一个分页查看器,能够让用户逐页查看文本内容
把 llvm 字节码文件转化为汇编
llc ez.bc -o ez.s
llc 是 LLVM Static Compiler,即LLVM 编译流程中的后端代码生成器
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
| .text .file "ez.c" .globl main .p2align 4, 0x90 .type main,@function main: .cfi_startproc
pushq %rax .cfi_def_cfa_offset 16 movl $.Lstr, %edi callq puts@PLT xorl %eax, %eax popq %rcx .cfi_def_cfa_offset 8 retq .Lfunc_end0: .size main, .Lfunc_end0-main .cfi_endproc .type .Lstr,@object .section .rodata.str1.1,"aMS",@progbits,1 .Lstr: .asciz "Hello, LLVM!" .size .Lstr, 13
.ident "Ubuntu clang version 18.1.3 (1ubuntu1)" .section ".note.GNU-stack","",@progbits
|
把汇编转化成可执行文件
clang ez.s -o ez_asm
我直接用这个失败了,因为汇编代码使用了绝对地址引用(movl $.Lstr, %edi),与 PIE 不兼容
禁用pie:
clang -no-pie ez.s -o ez_asm
使用 opt 来执行 pass
在典型的编译流程中,源代码首先被编译成 LLVM IR,然后 opt 会介入,对这个 IR 进行一系列的分析和转换,优化代码
opt --help
获取 LLVM 中可用程序转换列表
LLVM Pass
LLVM Pass 框架是 LLVM 系统的一个重要组成部分,Pass 执行构成编译器的各种转换和优化
Pass 的核心特征:
- 模块化:每个 Pass 都被设计成一个独立的、自包含的单元,它只专注于完成一个特定的任务
- 可组合:LLVM 允许通过 Pass 管理器将多个 Pass 串联起来,形成一个完整的优化管道,开发者可以根据需求自由组合不同的 Pass
- 统一接口:与旧版 Pass 管理器中通过继承定义 Pass 接口不同,新版 Pass 管理器中的 Pass 都遵循一个基于概念的多态接口
根据官方文档具体了解 pass
https://llvm.org/docs/WritingAnLLVMNewPMPass.html
基于源码的 LLVM Pass
上文已经为新 pass 构建好了环境
以 llvm-project 目录下创建好了的这个为例 llvm/lib/Transforms/Utils/HelloWorld.cpp
首先需要在一个头文件中定义这个 pass,创建文件 llvm/include/llvm/Transforms/Utils/HelloWorld.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #ifndef LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H #define LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H
#include "llvm/IR/PassManager.h"
namespace llvm {
class HelloWorldPass : public PassInfoMixin<HelloWorldPass> { public: PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM); };
}
#endif
|
1
| class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {
|
定义了一个 HelloWorldPass 类,它继承自 PassInfoMixin,这是 LLVM 新版 Pass 管理器的一个基类
PassInfoMixin
是一个 CRTP(奇异递归模板模式)模板,提供了 Pass 的基本信息和接口
1 2
| public: PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
|
run
方法是 Pass 的核心执行函数
Function &F
:当前正在处理的函数对象的引用
FunctionAnalysisManager &AM
:函数分析管理器,用于获取和管理分析结果
PreservedAnalyses
:返回值,告诉 Pass 管理器哪些分析结果在此 Pass 执行后仍然有效
然后看llvm/lib/Transforms/Utils/HelloWorld.cpp
1 2 3 4 5 6 7 8 9 10
| #include "llvm/Transforms/Utils/HelloWorld.h" #include "llvm/IR/Function.h"
using namespace llvm;
PreservedAnalyses HelloWorldPass::run(Function &F, FunctionAnalysisManager &AM) { errs() << F.getName() << "\n"; return PreservedAnalyses::all(); }
|
头文件中包含了刚刚定义的 HelloWorld.h 以及 LLVM 的 Function 类定义,提供函数相关的 API
pass 的核心逻辑
1
| errs() << F.getName() << "\n";
|
errs()
:LLVM 的错误输出流
F.getName()
:获取当前函数的名称
整体作用:将函数名打印到标准错误输出
1
| return PreservedAnalyses::all();
|
告诉 Pass 管理器所有的分析结果都被保留
以上就是 Pass 本身的代码,接下来需要在几个地方添加一些内容来注册这个 pass
在 llvm/lib/Passes/PassRegistry.def
文件的 FUNCTION_PASS 部分添加
1
| FUNCTION_PASS("helloworld", HelloWorldPass())
|
让这个 pass 名为 “helloworld”
在 llvm/lib/Passes/PassBuilder.cpp
中添加正确的 #include:
1
| #include "llvm/Transforms/Utils/HelloWorld.h"
|
然后就可以编译运行了,但是我按照这样操作之后再 ninja 编译时出错了,大概意思是刚刚的 HelloWorld.cpp 中缺少头文件
添加了下面这一行之后就能顺利编译了
1
| #include "llvm/Passes/PassBuilder.h"
|
在build
目录下执行 ninja
回显
1
| [59/59] Creating library symlink lib/libclang-cpp.so
|
使用 opt 来运行一个 pass
1 2 3
| opt -passes=helloworld ez.ll -o /dev/null
|
-passes=helloworld: 告诉 opt 运行一个名为 helloworld 的 Pass
-o /dev/null: 将 opt 的输出结果重定向到空设备,不保存修改后的 IR 文件
以上是旧的注册方法,LLVM 还提供了一种新的注册方式 –以 插件 的形式注册 Pass
首先,移除之前按照旧方法而做的修改
然后在 llvm/lib/Transforms/Utils/HelloWorld.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
| #include "llvm/Transforms/Utils/HelloWorld.h" #include "llvm/IR/Function.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Passes/PassBuilder.h"
using namespace llvm;
PreservedAnalyses HelloWorldPass::run(Function &F, FunctionAnalysisManager &AM) { errs() << "Hello from HelloWorldPass: " << F.getName() << "\n"; return PreservedAnalyses::all(); }
extern "C" LLVM_ATTRIBUTE_WEAK PassPluginLibraryInfo llvmGetPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, "HelloWorldPass", "v1.0", [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "hello-world") { FPM.addPass(HelloWorldPass()); return true; } return false; }); }}; }
|
工作流程:
用户运行:opt -passes=hello-world input.ll
LLVM 加载插件并调用 llvmGetPassPluginInfo()
插件注册解析回调到 PassBuilder
PassBuilder 解析 “hello-world” 名称
回调函数将 HelloWorldPass 添加到管道
Pass 管理器执行 HelloWorldPass::run()
方法
输出函数名并完成处理
接下来需要修改 CMakeLists.txt 文件,添加插件的构建配置
在 llvm/lib/Transforms/Utils/CMakeLists.txt
中添加 Passes 和 PARTIAL_SOURCES_INTENDED
1 2 3 4 5 6 7 8 9
| LINK_COMPONENTS Analysis Core Support TargetParser Passes PARTIAL_SOURCES_INTENDED )
|
然后进入 build 目录,重新运行 cmake,让 Cmake 读取修改后的 CMakeLists.txt;再执行 ninja
1 2 3
| cd /home/eleven/llvm-project-llvmorg-20.1.8/build cmake -G Ninja ../llvm ninja
|
自己编写 LLVM Pass
先安装 LLVM 的软件包,我这里安装的是 LLVM 19
1
| sudo apt install llvm-19
|
创建一个新的目录 myllvm
Function 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
| #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Support/raw_ostream.h"
using namespace llvm;
struct HelloPass : public PassInfoMixin<HelloPass> { PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { errs() << "Hello from LLVM 19: " << F.getName() << "\n"; return PreservedAnalyses::all(); } static bool isRequired() { return true; } };
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return { .APIVersion = LLVM_PLUGIN_API_VERSION, .PluginName = "HelloPass", .PluginVersion = "v0.1", .RegisterPassBuilderCallbacks = [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "hello") { FPM.addPass(HelloPass{}); return true; } return false; }); } }; }
|
一些注释
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
| #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Support/row_ostream.h"
using namespace llvm; struct HelloPass : public PassInfoMixin<HelloPass> { PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { errs() << "Hello from LLVM 19:" << F.getName() << "\n"; return PreservedAnalyses::all(); } static bool isRequired() { return true; } };
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() { return { .APIVersion = LLVM_PLUGIN_API_VERSION, .PluginName = "HelloPass", .PluginVersion = "v0.1", .RegisterPassBuilderCallbacks = [](PassBuilder &PB) { PB.registerPipelineParstringCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "hello") { FPM.addPass(HelloPass{}); return true; } return false; }); }}; }
|
编译 LLVM Pass
1
| clang++-18 -shared -fPIC `llvm-config-19 --cxxflags` -o HelloPass.so Hello.cpp
|
clang++ 是 C++ 编译器,18是 clang 版本
-shared 告诉编译器和连接器创建一个共享库(动态链接库)
-fPIC Position-Independent Code,位置无关代码,创建共享库是个标志这是必需的
`llvm-config-19 –cxxflags` 确保编译器可以找到LLVM pass所依赖的所有头文件
前半部分查询 LLVM 配置信息,19 是LLVM 版本
后半部分的–cxxflags 参数让 llvm-config-19 输出编译C++代码时需要的标志
-o HelloPass.so 指定生成共享库的名字
准备一个 test 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <stdio.h> int main() { printf("Hello, World!\n"); return 0; } int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
|
编译成 .ll 文件
1
| clang -emit-llvm -S test1.c -o test1.ll
|
用 opt 执行 Pass
1
| opt -load-pass-plugin=./HelloPass.so -passes="hello" ./test1.ll -disable-output
|
HelloPass.so 是共享库文件 hello 是 pass 名称
在默认情况下,opt 会将经过pass处理后的LLVM IR输出到标准输出,-disable-output 能禁止该行为
输出
1 2 3
| Hello from LLVM 19: main Hello from LLVM 19: add Hello from LLVM 19: subtract
|