LLVM


LLVM 简介

LLVM(Low Level Virtual Machine)是一个开源的编译器基础设施项目,旨在提供一个可重用的编译器和工具链技术。它的设计目标是支持编译器的开发,优化和分析,使得开发者能够创建高效的程序和工具。

LLVM环境搭建

预编译包安装

编译安装

1
2
3
4
5
6
7
8
9
cmake -G "Unix Makefiles" \
-DLLVM_ENABLE_PROJECTS="clang;llvm;" \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DBUILD_SHARED_LIBS=On \
-DLLVM_ENABLE_LLD=ON \
../llvm

make -j8

LLVM整体设计

llvm 与 gcc

  • 架构和设计
    • LLVM编译器是基于模块化、可扩展的设计,它将编译过程划分为多个独立的阶段,。并使用中间表示(IR)作为通用的数据结构进行代码优化和生成。而GCC编译器则是集成了多个前端和后端的传统编译器,其设计更加紧密一体化
  • 开发语言和前端支持
    • LLVM编译器使用C++开发,并提供了广泛的前端支持,可以处理多种编程语言(如C、C++、Rust等),这使得开发者能够使用统一的编译框架来处理不同的语言。GCC编译器则使用C语言开发,并对各种编程语言提供了广泛的前端支持。
  • 优化功能
    • LLVM编译器一起高度模块化的中间表示(IR)为基础,具备强大的代码优化能力。同时,LLVM的设计也使得优化部分可以在编译过程的各个阶段进行,从而实现更细粒度的优化。GCC编译器也提供了一系列的优化选项,但其优化能力相对较低。
  • 社区支持和生态系统
    • LLVM拥有庞大而活跃的开源社区,并且有很多基于LLVM的工具和项目,如Clang、LLDB等。GCC也有强大的开源社区支持,但相对于LLVM稍显逊色。

llvm 结构

  • 前端解析源代码,检查错误,并构建特定于语言的抽象语法树(AST)来表示输入代码。AST 可以选择转换为新的表示形式以进行优化,并且优化器和后端在代码上运行
  • 优化器负责进行各种转换以尝试提高代码的运行时间,例如消除冗余计算,并且通常或多或少独立于语言和目标
  • 后端(也称为代码生成器)将代码映射到目标指令集。除了编写正确的代码之外,它还负责生成利用受支持架构的不寻常功能的良好代码。编译器后端的常见部分包括指令选择、寄存器分配和指令调度。

项目组成

  • Clang:解析 C/C++代码
  • MLIR:构建可重用和扩展编译器基础设施的新颖方法
  • OpenMP:提供了一个OpenMP运行时库函数
  • polly:使用多面体模型实现了一套缓存局部性优化以及自动并行和向量化
  • LLDB、libc++、libc++ABI、compiler-rt、libclc、klee、LLD、BOLT

命令/工具

  • llc - LLVM静态编译器
  • lli - 直接从 LLVM 位码执行程序
  • llvm-as - LLVM 编译器
  • llvm-dis - LLVM 反编译器
  • opt - LLVM 优化器

clang前端

  • 预处理(Preprocessor):头文件以及宏的处理;
  • 词法分析(Lexer):词法分析器的任务是从左向右逐行扫描程序的字符,识别出各个单词并确定单词的类型,将识别出的单词转换成统一的机内表示——词法单元(token)形式;
  • 语法分析(Parser):主要任务是从词法分析器输出的token序列中识别出各类短语,并构造语法分析树。如果输入字符串的各个单词恰好自左至右地站在分析树的各个叶结点上,那么这个词串就是该语言的一个句子,语法分析树描述了句子的语法结构;
  • 语义分析(Sema):搜集标识符的属性信息与语义检查。标识符的属性种属(kind)、类型(Type)、存储位置和长度、值、作用域、参数和返回值类型。语义检查包括变量或过程未经声明就使用、变量或过程名重复声明、运算分量类型不匹配、操作符与操作数之间的类型不匹配。
  • 代码生成(CodeGen):将AST转换成相应的llvm代码。

LLVM IR

基本概念

高级语言经过clang前端会将代码解析为平台无关的中间表示(IR),使编译器能够在编译、链接、以及代码生成的各个阶段忽略语言特性,进行全面有效的优化和分析。

LLVM基于统一的中间表示来实现优化遍,中间表示采用静态单赋值形式,该形式的虚拟指令集能够高效的表示高级语言,具有灵活性好、类型安全、底层操作等特点。如图所示,当同一变量出现多次赋值时,通过SSA变量重命名的方式加以区分,可以避免出现多次定义的情况。

IR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用 LLVM IR的各种编译优化、JIT支持、目标代码生成等功能。

IR表示

LLVM IR有三种形式:

  • 内存中的表示形式
  • bitcode形式
  • LLVM汇编文件格式

IR表示:

  • Module类,Module可以理解为一个完整的编译单元。一般来说,这个编译单元就是一个源码文件。
  • Function类,这个类顾名思义就是对应于一个函数单元。Function可以描述两种情况,分别是函数定义和函数声明。
  • BasicBlock类,这个类表示一个基本代码块,“基本代码块”就是一段没有控制流逻辑的基本流程,就相当于程序流程图中的基本过程。
  • Instruction类,指令类就是LLVM中定义的基本操作,比如加减乘除这种算数指令、函数调用指令、跳转指令、返回指令等等。

控制流图CFG:

1
2
3
4
5
opt -analyze -dot-cfg-only test.ll
opt -analyze -dot-cfg test.ll

dot -Tpng xxx.dot -o 1.png
sz 1.png

代码生成

基本概念

LLVM 目标无关代码生成器是一个框架,它提供了一套可重用组件,用于将 LLVM 内部表示转换为指定目标的机器代码,可以是汇编形式(适用于静态编译器),也可以是二进制机器代码格式(适用于JIT编译器)。

LLVM 后端的主要功能是代码生成,因此也叫代码生成器。后端包括若干个代码生成分析转换pass将 LLVM IR转换成特定目标架构的机器代码。

  • 源码结构:
    • 特定目标的抽象目标描述接口的实现。这些机器描述使用 LLVM 提供的组件,并可以选择提供定制的特定于目标的传递,为特定目标构建完整的代码生成器。目标描述位于lib/Target中。
    • 用于实现本机代码生成的各个阶段(寄存器分配、调度、堆栈帧表示等)的目标无关算法。此代码位于lib/COdeGen中。
    • 目标独立组件JIT组件。LLVM JIT 完全独立于目标(使用TargetJITInfo结构来处理特定于目标的问题。独立于目标的JIT的代码位于lib/ExecvtionEngine/JIT中)

LLVM Pass

pass 介绍

第一个 Pass:Hello 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
//=============================================================================
// 文件:
// HelloWorld.cpp
//
// 描述:
// 访问模块中的所有函数,打印它们的名称和参数数量到标准错误输出。
// 严格来说,这是一个分析通道(即函数不会被修改)。但是,为了简化起见,这里没有
// print 方法(每个分析通道都应该实现它)。
//
// 用法:
// 新的 PM
// opt -load-pass-plugin=libHelloWorld.dylib -passes="hello-world" \
// -disable-output <输入-llvm-文件>
//
//
// 许可证: MIT
//=============================================================================
//包含的头文件:导入了LLVM的各种支持库,包括老式和新式的pass管理器,以及用于输出的库

//包含 LLVM 旧版pass管理器的定义
#include "llvm/IR/LegacyPassManager.h"
//引入 LLVM 的新pass管理器的构建工具
#include "llvm/Passes/PassBuilder.h"
//包含定义用于插件化pass的接口
#include "llvm/Passes/PassPlugin.h"
//提供LLVM的输出流支持
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

//将所有代码放在匿名空间中,避免与其他代码冲突
namespace {

// 该方法实现了该 pass 的功能。
//访问器函数
//visitor函数:该函数被调用以打印每个函数的名称和参数数量
void visitor(Function &F) {
errs() << "(llvm-tutor) Hello from: "<< F.getName() << "\n";
errs() << "(llvm-tutor) number of arguments: " << F.arg_size() << "\n";
}


// 新的 PM 实现
//pass实现
//HelloWorld结构体: 实现了 PassInfoMixin,包含了 run 方法,执行对每个函数的分析。
//isRequired: 此函数确保即使函数被标记为 optnone,该pass仍会被执行。

struct HelloWorld : PassInfoMixin<HelloWorld> {
// 主入口点,接受要运行该 Pass 的 IR 单元(&F)和相应的 Pass 管理器(如果需要可以查询)。
PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
visitor(F);
return PreservedAnalyses::all();
}

// 如果 isRequired 返回 false,那么这个 pass 将会被跳过,针对带有 `optnone` LLVM 属性的函数。请注意,`clang -O0` 会将所有函数标记为 `optnone`。
static bool isRequired() { return true; }
};
} // namespace

//-----------------------------------------------------------------------------
// 新的 PM 注册
//-----------------------------------------------------------------------------

//pass注册
//插件信息注册:定义了如何注册pass的插件信息。使用 registerPipelineParsingCallback 注册 hello-world 名称的pass
llvm::PassPluginLibraryInfo getHelloWorldPluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "HelloWorld", LLVM_VERSION_STRING,

//Lambda表达式
[](PassBuilder &PB) {

//注册回调
//参数
//StringRef Name: 命令行中传入的pass名称。
//FunctionPassManager &FPM: 用于管理函数pass的工具。
//ArrayRef<PassBuilder::PipelineElement>: 管道元素的数组,包含在命令行中指定的pass元素。
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {

//条件判断,检查传入的名称是否为hello-world,如果匹配则调用FPM.addPass将HelloWorld pass添加到功能pass 管理器中
if (Name == "hello-world") {
FPM.addPass(HelloWorld());
return true;
}
return false;
});
}};
}

// 这是 pass 插件的核心接口。它确保 `opt` 在命令行中通过 '-passes=hello-world' 添加到 pass 流水线时能够识别 HelloWorld。

//入口函数
//插件接口:这是LLVM加载pass时调用的接口,确保LLVM能够识别 HelloWorld pass
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return getHelloWorldPluginInfo();
}

使用cmake编译

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
# 设置最低cmake版本
cmake_minimum_required(VERSION 3.20.0)

# 定义项目名称
project(SimpleProject)

# 查找 LLVM:使用 find_package 命令查找 LLVM。REQUIRED 表示如果找不到 LLVM,将报错并停止配置过程。CONFIG 表示使用 LLVM 的配置文件。
find_package(LLVM REQUIRED CONFIG)

# 打印找到的 LLVM 版本:输出找到的 LLVM 版本信息,${LLVM_PACKAGE_VERSION} 是由 find_package 设置的变量。
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")

# 打印配置文件路径:输出正在使用的 LLVMConfig.cmake 的路径,${LLVM_DIR} 是包含该文件的目录。
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")

include_directories(${LLVM_INCLUDE_DIRS})

separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})

# 添加编译定义:将之前分离出的 LLVM 定义添加到编译器选项中,确保在编译时包含所需的宏定义。
add_definitions(${LLVM_DEFINITIONS_LIST})

# 创建可执行文件:定义一个可执行文件 simple-tool,其源代码为 tool.cpp。
add_executable(simple-tool tool.cpp)

# 映射 LLVM 组件到库名:使用 llvm_map_components_to_libnames 将指定的 LLVM 组件(support, core, irreader)映射到相应的库名,并存储在 llvm_libs 变量中。
llvm_map_components_to_libnames(llvm_libs support core irreader)

target_link_libraries(simple-tool ${llvm_libs})
# 链接库:将映射到的 LLVM 库链接到可执行文件 simple-tool,确保在编译时可以找到和使用这些库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.10)

# 项目
project(MyLLVMProject)

# 查找 LLVM 包。REQUIRED 表示如果找不到 LLVM,CMake会报错并停止构建。
# CONFIG 指示 CMake 查找 LLVM 的配置文件。
find_package(LLVM REQUIRED CONFIG)

# 将 LLVM 的头文件路径添加到编译器的搜索路径中。
include_directories(${LLVM_INCLUDE_DIRS})

# 创建一个名为 MyPass 的动态库。
# 并指定源文件,MODULE 表示该库不会生成可执行文件,而是用于动态加载的插件。
add_library(MyPass MODULE my_pass.cpp)

# 将 MyPass 库与 LLVM 库链接。
target_link_libraries(MyPass PRIVATE ${LLVM_LIBRARIES})

命令行编译

命令行编译是最简单暴力的方法,以Hello Pass为例:

1
$ clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `llvm-config --ldflags`

其中

  • llvm-config提供了CXXFLAGSLDFLAGS参数方便查找LLVM的头文件与库文件。 如果链接有问题,还可以用llvm-config --libs提供动态链接的LLVM库。 具体llvm-config打印了什么,请自行尝试或查找官方文档。
  • -fPIC -shared 显然是编译动态库的必要参数。
  • 因为LLVM没用到RTTI,所以用-fno-rtti 来让我们的Pass与之一致。
  • -Wl,-znodelete主要是为了应对LLVM 5.0+中加载ModulePass引起segmentation fault的bug。如果你的Pass继承了ModulePass,还请务必加上。

现在,你手中应该有一份编译好的LLVMHello.so了。根据刚才阅读过的官方文档的介绍,你知道可以通过命令

1
2
$ clang -c -emit-llvm main.c -o main.bc # 随意写一个C代码并编译到bc格式
$ opt -load-pass-plugin ./LLVMHello.so -passes=hello-world demo.bc -o /dev/null

来使用它。

自动使用Clang运行 Pass

当代码文件比较多的时候,你会觉得先把源代码编译成IR代码,然后用opt运行你的Pass实在麻烦且无趣。 恰好在你手头已有一些构建工具时,你可能会想,如果能把Pass集成到clang的参数中调用,那该多好啊。 因为这样你就可以做这样的事情 (假设你的构建工具是autotools):

1
2
$ CC=clang CFLAGS="-arg-to-load-my-pass mypass.so" ./configure
$ make

下面这篇文章就告诉了你该怎么做,请仔细阅读。当你读完后,你可能会觉得,这魔法参数也太丑陋了吧。我也觉得。“Maybe this is life”。

LLVM pass

现在回头看看前面Hello.cpp,有留意到里面的两行注释吗? static RegisterPass<Hello> X是给opt加载Pass用的, static RegisterStandardPasses Y是给clang加载Pass用的, 有时候两者只要选一个就行了。希望在读完上面这篇文章后你能理解得更深入。

现在,你可以在clang中直接加载Hello Pass了

1
$ clang -Xclang -load -Xclang path/to/LLVMHello.so main.c -o main

当然,你还觉得这不够优雅的话,也可以编写一个clang的wrapper程序hello-clang。 它会读取命令行参数,然后加上-Xclang -load -Xclang path/to/LLVMHello.so构造成新的命令行参数。 最后调用execvp()执行clang

举例来说,如果输入hello-clang main.c -o main, 那么它会调整参数,最终执行clang -Xclang -load -Xclang path/to/LLVMHello.so main.c -o main

不用我说,你也能想到这个画面:

1
$ CC=hello-clang ./configure && make

结合 Clnag 插桩的注意点

一般来说,插桩代码的时候,我们往往会在源代码中插入一些call指令来调用我们实现的函数。 举个例子,你可能会想写一个MemTrace Pass来监控运行时内存的访问。所以它会在所有访问内存的指令前插入一个call my_memlog(mem_addr)指令来记录这次的内存访问。

假如MemTrace Pass编译在libmemtrace.so中,my_memlog()函数编译在libmemlog.a中, 那么我们不要忘记在编译的时候链接它:

1
$ clang -Xclang -load -Xclang libmemtrace.so main.c -o main libmemlog.a

你也可以和上面的hello-clang一样,把它封装到一个clang wrapper中。

更难一些的Pass

现在是仔细瞧瞧

LLVM Programmer’s Manual​llvm.org/docs/ProgrammersManual.html

的时候了。 其中,结合

这个github项目​github.com/imdea-software/LLVM_Instrumentation_Pass/blob/master/InstrumentFunctions/Pass.cpp

,读者可以再仔细去看看ProgrammersManual - The Core LLVM Class Hierarchy Reference这一小节,回顾一下LLVM IR在内存中的表示。 也记得看看Helpful Hints for Common Operations这一小节,学习一下怎么遍历IR、修改指令。 当你看完这些后,那个github项目你也肯定能看懂了。

参考项目

AFL++

后言

参考链接:LLVM架构简介 - LLVM IR入门指南 (evian-zhang.github.io)
参考链接:LLVM编译器入门(一):LLVM整体设计
参考链接:基于LLVM-18,使用New Pass Manager,编写和使用Pass