llvm 浅探

写在前面

这篇文章的前半部分包括写的环境搭建都是基于 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 个线程进行构建

1
2
cd build
ninja -j8

把 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

LLVM tool

https://llvm.org/docs/GettingStarted.html#llvm-tools
先编写一个简单的 c 语言脚本

1
2
3
4
5
6
7
// ez.c
#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
; ModuleID = 'ez.c'
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

; 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 = !{!"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 # -- Begin function main
.p2align 4, 0x90
.type main,@function
main: # @main
.cfi_startproc
# %bb.0:
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
# -- End function
.type .Lstr,@object # @str
.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);
};

} // namespace llvm

#endif // LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H
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

#main

-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();
}

// 这是 Pass 插件的注册入口点
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") { //hello-world 即为定义的 pass 的名字
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 # <- 链接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
//Hello.cpp
#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"  //引入 PassBuilder 类,它是新 PM 的核心组件
#include "llvm/Passes/PassPlugin.h" //编写 LLVM 插件的必备头文件
#include "llvm/Support/row_ostream.h" //引入了输出流库

using namespace llvm;
struct HelloPass : public PassInfoMixin<HelloPass>
{
// 定义了一个名为 HelloPass 的结构体,继承自 PassInfoMixin ,能自动提供一些基础功能
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM)
{
// Pass 核心函数
errs() << "Hello from LLVM 19:" << F.getName() << "\n";
// F.getName() 获取当前函数的名称,并输出到错误流
return PreservedAnalyses::all();
}
static bool isRequired() { return true; }
// 表明这个函数是必需的,无论优化级别如何都不会被跳过
};

// 插件注册
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
// LLVM_ATTRIBUTE_WEAK 标记该函数为一个“弱符号”,在链接时允许多个库定义同名的函数而不会产生冲突
llvmGetPassPluginInfo()
{
// LLVM 插件的入口函数
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")
{ // passname
FPM.addPass(HelloPass{});
return true; // 如果名称匹配,添加 HelloPass 到 FunctionPassManager
}
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
//test1.c
#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

llvm 浅探
http://example.com/2025/08/01/llvm/
作者
Eleven
发布于
2025年8月1日
许可协议