LLVM 从入门到迷惑
wangziwenhk · · 科技·工程
前言
本教程使用 LLVM 19.1.6,不同版本的 API 可能有所不同,尽量以本教程使用的版本为准。
介绍
LLVM(Low Level Virtual Machine)是一个开源的编译器框架,用于开发编译器、工具链和其他语言的开发工具。它最初由克里斯·拉特纳(Chris Lattner)于2000年在伊利诺伊大学厄巴纳-香槟分校开发,旨在提供一种模块化和可重用的编译器基础设施。
LLVM的核心设计理念是通过一个中间表示(IR,Intermediate Representation)使得不同的语言和优化能够共享同一套工具链。LLVM能够支持多种编程语言的编译过程,并通过优化中间表示来提升程序性能。
ok,套话结束,正式开始。
安装
包管理器
你可以使用包管理器来便捷的安装LLVM,通常执行以下代码就能自动安装。
vcpkg
vcpkg install llvm
msys
pacman -S mingw-x86_64-llvm
如果你使用其他架构,可以将 x86_64
替换为对应架构名,如x86
就对应 i386
。
xrepo
xrepo install llvm
或者:
xrepo install vcpkg::llvm
自主编译
如果你由于某些原因无法使用包管理器,那么你可以通过 git 拉取源代码仓库并自行编译。
# 拉取源码
git clone https://github.com/llvm/llvm-project.git
# 配置
cd ./llvm-porject
mkdir build
cd ./build
# 编译
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release ../llvm # 你也可以将 Ninja 换成其他的生成器
cmake --build . -- -j$(nproc)
cmake --install .
由于 LLVM 过于庞大,你可能需要等待非常长的时间,如果不是特殊要求,建议使用包管理器。
配置
你可以向你的 Cmake 项目配置文件中添加以下语句:
find_package(LLVM REQUIRED CONFIG)
target_link_libraries(<target-name> PRIVATE LLVM)
将 <target-name>
替换为你的可执行程序项目目标。
一个完整的例子如下
cmake_minimum_required(VERSION 3.11)
# 设置项目名称
project(MyProject)
# 寻找 LLVM 包
find_package(LLVM REQUIRED CONFIG)
# 设置 C++ 标准(可选)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 定义可执行文件 main
add_executable(main main.cpp)
# 将 LLVM 链接到 main 目标
target_link_libraries(main PRIVATE LLVMCore)
当你完成以上工作,配置阶段就结束了,接下来步入正式部分。
相关类
llvm::LLVMContext
这是一个上下文类,它管理你所使用的 LLVM IR,这个类非常重要。
通常我们在整个生成过程中只创建一个 LLVMContext 对象,如果你需要使用多个上下文,注意不要混用使用不同上下文声明的 llvm 相关类。
该类不可复制,建议使用指针持有或使用 std::move()
来管理。
llvm::Module
这是一个模块,类似 C++ 中的一个翻译单元。
所有的 IR 指令都由该类保存。
构造函数
Module(StringRef ModuleID, LLVMContext &Context);
- ModuleID:模块的标识符,通常是源文件名或其它描述性的名字。
- Context:LLVM 的上下文对象。
这里的 StringRef
类似 std::string_view
,它并不分配内存而是对已有字符串的引用,你可以直接将其看成 std::string
来使用。
Module(StringRef ModuleID, LLVMContext &Context, const TargetTriple &Triple, const DataLayout &Layout);
- Triple:表示目标平台的三元组,例如
x86_64-pc-linux-gnu
。 - Layout:表示数据布局,用于描述数据如何在内存中组织。
llvm::IRBuilder<T>
是一个工具类,用于对常用指令的快速生成和插入,不可复制。
构造函数
IRBuilder 需要一个 插入点(Insert Point),即在哪个基本块中插入新的指令。这个插入点通常是 BasicBlock 的一个迭代器。
IRBuilder(LLVMContext &Context);
IRBuilder(LLVMContext &Context, BasicBlock *B);
IRBuilder(LLVMContext &Context, Instruction *InsertBefore);
该类的 T 类型实际上是对于 IRBuilder 的优化进行一些限制的选项,例如:
llvm::IRBuilder<llvm::NoFolder> Builder(Context);
可以禁止 IRBuilder 自动合并指令。这在某些优化场景下可能会用到。
以下是 IRBuilder 可选的参数:
llvm::NoFolder
: 禁用 指令折叠。指令折叠通常是在构建 IR 时自动合并相邻的指令以减少冗余,但在某些情况下(例如,某些特定的优化需要保持指令的独立性),你可能希望禁用这一行为。llvm::WithPostInsertHook
: 允许你为每个插入的指令提供一个回调函数(hook)。每当一条新指令插入到基本块时,回调函数会被调用。这对于一些高级的需求,如在插入指令后执行自定义操作,或者进行一些日志记录等,特别有用。llvm::InsertPoint
: 用于指定 插入点,即指令应当插入的位置。InsertPoint 可以是 BasicBlock::iterator,表示插入位置位于某个基本块的某个位置。IRBuilder 会根据给定的插入点来确定插入的具体位置。llvm::EmitWarnings
: 通常用来启用警告输出。当使用该模板参数时,IRBuilder 会在生成某些可能引发问题的指令时发出警告。
编写 LLVM IR
函数
创建函数签名
为了创建一个函数,我们需要先创建一个函数签名。
llvm::FunctionType* funcType = llvm::FunctionType::get(<returnType>, <argTypes>, <isVarArg>);
<returnType>
: 一个llvm::Type*
类型的参数,表示函数的返回类型。<argTypes>
: 一个std::vector<llvm::Type*>
类型的参数,表示函数已知的参数的类型。<isVarArg>
: 一个bool
类型的参数,表示函数参数数量是否可变 ( 类似 C/C++ 中的<type> func(arg1,arg2,...)
签名 ) 。
示例:
llvm::FunctionType* funcType = llvm::FunctionType::get(llvm::Type::getInt32Ty(context), {llvm::Type::getInt32Ty(context)}, false);
创建函数
之后就可以使用函数签名来创建一个函数了:
llvm::Function* func = llvm::Function::Create(<funcType>, <linkType>, <funcName>, <module>);
<funcType>
: 之前创建的函数签名。<linkType>
: 链接类型,告诉 LLVM 这个函数如何与其他模块进行链接。它控制着符号的可见性以及在链接过程中如何处理重复定义的符号。<funcName>
: 函数名称。<module>
: 需要创建函数的模块。
示例:
llvm::Function* func = llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "my_function", module);
创建基本块
每个基本块都保存着一系列指令,你可以通过 br
或 condbr
命令在不同块之间进行跳转,但是注意不能有环。
llvm::BasicBlock* entry = llvm::BasicBlock::Create(<context>, <name>, <func>);
<context>
: LLVM 上下文。<name>
: 基本块名称。<func>
: 要插入基本块的函数。
基本块在函数中的顺序是按照定义顺序来的。如果你不能保证定义顺序,请在每个基本块的末尾加入控制流指令。
示例:
llvm::BasicBlock* entry = llvm::BasicBlock::Create(context, "entry", func);