LLVM项目深度解析:从源码构建到Pass编写
2026/9/19 10:23:35 网站建设 项目流程

搞编译器这行的人,电脑里大概率都躺着一个又大又占地方的目录,名字基本都叫llvm-project。我第一次完整 clone 这个仓库的时候,光是看它有几十个顶层文件夹就懵了:clang、lld、compiler-rt、mlir、polly……这些东西到底是干嘛的?它们为什么非要挤在一个仓库里?等我真的把它 build 一遍、亲手写完一个 pass、跟着源码 debug 过之后,才明白这个 monorepo 的设计逻辑有多顺。

这篇文章不想做成官方文档的翻译,我想以“把整个 llvm-project 当成一个工程来复盘”的角度,把它拆开给你看:仓库里每一块是什么、为什么要这样设计、从零构建要怎么做、第一个 LLVM Pass 怎么写、以及我踩过的那些坑。无论你是刚准备入门编译器的学生,还是想给 LLVM 接自己语言后端的工程师,这篇应该都比纯看文档少走不少弯路。

1. LLVM 到底是什么,为什么大家都说它重要

1.1 从一个编译器工程的“全家桶”说起

LLVM 最初是 Chris Lattner 博士论文里的一整套编译器基础设施设计,十几年前就被苹果挖去做 iOS 和 macOS 的官方工具链底座。到今天,llvm-project已经不只是一个“C/C++ 编译器”那么简单了,它是一个完整的编译器生态:Clang 负责把 C/C++/Objective-C 变成中间表示,优化器负责在中间表示上做各种提速和瘦身,后端负责把优化后的中间表示变成机器码,LLD 负责最终链接,libc++ 和 libc++abi 提供标准库和 ABI 支撑,compiler-rt 附带各种运行时和 sanitizer 工具,连 MLIR 这种面向机器学习编译器的基础设施也被纳入进来了。

所以你会看到很多人把 LLVM 叫“编译器领域的 Linux”。它不是某一个软件,而是一整套可以自由组合的积木。你可以只用 Clang 替代 GCC 写 C++,也可以只用它的后端优化器来优化自己发明的语言,还可以直接把 MLIR 拉出来做深度学习编译器。这是我理解 llvm-project 最重要的一个观念:它是一台“编译器工厂”,而不是一个固定产线上的单一产品。

1.2 为什么是“编译器界的 Linux”

把 LLVM 比作 Linux,还有一个很关键的点是它的组织方式。GCC 强在哪?强在整体、成熟、集成度高;但它也正因为整体耦合太深,你想单独抽出一层来复用会非常痛苦。LLVM 从一开始就把“前端”和“后端”彻底解耦了,中间用一套稳定的 IR 做分界线。这意味着,你想给一门新语言做编译器,只需要写到 IR 就结束,后面所有优化、寄存器分配、指令选择都是现成的。

再加上许可证上 LLVM 用的是 Apache License 2.0,对商业公司非常友好,所以苹果、Google、ARM、高通、Meta 这些厂商都愿意往里面投入。Rust 早期编译器后端就直接选了 LLVM,Zig 也把 LLVM 作为默认后端,Swift 不用说,整个工具链都在这个体系里。这样一来,LLVM 就不是某个公司控制的私人玩具,而是整个行业一起喂大的公共基础设施。影响范围已经远远超出传统 C/C++ 编译器的边界了。

1.3 谁在用 LLVM,影响范围有多大

我个人判断一个基础软件重不重要的方法很简单:数一数它下游接了多少知名项目。llvm-project 的下游名单非常夸张:

  • 苹果 Xcode 里默认的 Clang、lldb、libc++ 全是一家人;
  • Android NDK 从 r13 开始 Clang 就成了默认编译器;
  • Rust 社区讨论过无数次“rustc 什么时候能逃出 LLVM”,最后还是没走;
  • Swift 编译器本身就是 LLVM 前端一个分支;
  • CUDA 工具链里 NVIDIA 官方也维护了 NVPTX 后端,Clang 可以直接编 CUDA 代码;
  • 数据库领域,DuckDB 用 LLVM 做 JIT 编译加速查询;很多现代计算引擎都在借鉴 LLVM IR 的思路做自己的中间层。

所以当你打开llvm-project这个仓库,你看到的其实不是一个项目,而是一个连接了编程语言、操作系统、硬件指令集、数据库、AI 编译器的基础设施枢纽。理解了这一点,你就知道为什么网上有那么多人在研究它了。

2. 源码结构复盘:llvm-project 这个仓库里装了什么

2.1 一张目录地图

刚接触这个仓库的人最容易懵的就是目录太多。我把它拆成“核心组件”和“外围组件”两层理解,核心组件是编译一条 C++ 程序链路里必须有的,外围组件是围绕它扩展出来的各种工具库。先看一张最小地图:

llvm-project/ ├── llvm/ # 核心:IR 定义、优化器、代码生成、目标描述 │ ├── include/llvm/ # 公共头文件 │ ├── lib/ # 各种库,如 LLVMCore、LLVMTransformUtils │ ├── tools/ # opt、llc、llvm-as、llvm-dis 等命令行工具 │ └── test/ # lit 测试集 ├── clang/ # C/C++/Objective-C 前端 ├── lld/ # 链接器 ├── libcxx/ # C++ 标准库实现 ├── libcxxabi/ # C++ ABI 实现 ├── libunwind/ # 栈回溯支持库 ├── compiler-rt/ # 各种运行时和 sanitizer ├── openmp/ # OpenMP 运行时 ├── mlir/ # 多级 IR 基础设施 ├── polly/ # 循环优化和多面体模型 ├── flang/ # Fortran 前端 ├── clang-tools-extra/ # clang-tidy 等辅助工具 └── bolt/ # 二进制优化和布局工具

第一次看这张表也许觉得内容多,但你只要沿着“把源代码变成可执行文件”这条主线走:clang 负责翻译,llvm 负责优化和生成目标文件,lld 负责链接,libc++ 提供标准库,compiler-rt 提供运行时和检查工具。主线一通,外围组件就很自然了。

2.2 核心子项目的分工

下面这张表是我自己整理的“一句话职责表”,对快速定位该看哪个目录特别有用:

子项目目录一句话职责适合什么场景去看
llvm/编译器基础设施本身:IR、优化、代码生成、目标描述写 Pass、移植后端、学习编译器优化
clang/C/C++/Objective-C 前端了解 AST、语义分析、前端错误处理
lld/链接器,支持 ELF/Mach-O/COFF/WebAssembly研究符号解析、重定位、LTO
libcxx/现代 C++ 标准库实现看标准库源码、学模板元编程
compiler-rt/sanitizer、profile 运行时、内建函数搞 ASan/UBSan、性能分析工具
mlir/多级 IR,面向 AI 和异构计算做深度学习编译、DSL 编译
polly/基于多面体模型的循环优化研究自动并行化、缓存友好优化
bolt/对已编译二进制再做布局优化做性能工程、理解编译产物
flang/Fortran 前端,继承自经典 Flang 项目做数值计算、科学计算编译器

每个子项目都有自己独立的测试目录和文档目录,但它们的构建配置统一由顶层的 CMake 管理。所以你只需要在顶层跑一次 CMake,按需指定要启用哪些子项目即可,不需要每个目录进去单独 configure。

2.3 Monorepo 的工程哲学

很多人会问:为什么这些东西不拆成几个仓库各自管理?拆开确实看起来更清爽,但编译器的组件之间耦合太深了。Clang 生成的 IR 版本必须和 LLVM 内核完全匹配,libc++ 的实现又依赖 Clang 对某些语言特性的支持,MLIR 的优化要复用 LLVM 的 Analysis 框架。如果每个仓库单独发版,版本矩阵会让维护者疯掉。

所以 LLVM 选择了 monorepo,配合一个固定的发布节奏:每年两个大版本,打类似llvmorg-17.0.6这种 tag。你 clone 下来切到对应 tag,几个子项目天然就是互相兼容的。这在工程上是一种非常务实的取舍:牺牲了一点仓库体积和 clone 时间,换来了组件间“永远一致”的确定性。

我自己实际开发时也体会到了这种好处。比如我改了一点llvm/lib/Transforms里的东西,直接用同一份源码目录里的 Clang 做测试,完全不需要担心版本不匹配。如果你用的是一个 standalone 的 LLVM 库,要给某个旧版本的 Clang 配套新优化器,光处理 API 变更就能耗掉半天时间。

3. 核心架构拆解:编译器的三段式设计

3.1 前端、优化器、后端三条流水线

LLVM 最核心的架构思想是三步走。第一步,前端把你写的源代码翻译成 LLVM IR;第二步,优化器对 IR 做各种变换,跑一轮又一轮的 Pass;第三步,后端负责把 IR 变成当前目标机器的汇编和机器码。可以用一个生活化的例子理解:前端是翻译官,把中文文章翻译成“世界语”;优化器是编辑,在世界语版本上改得精炼;后端是把世界语再翻译成特定国家的本地语言。

具体到一条 C 程序的编译链路是这样的:Clang 先做词法、语法、语义分析,生成 AST,再逐步 lower 到 LLVM IR。IR 经过 O2/O3 优化管线后,进入指令选择阶段,传统流程会用 SelectionDAG 生成目标指令的 DAG 表示,再经过指令调度、寄存器分配、指令布局,最终输出汇编文件。汇编器生成目标文件后,LLD 负责把多个目标文件和库链接成最终可执行文件。

理解了这条主线,你再看llc -O2 test.ll -o test.s这类命令就顺了:llc 其实就是把“IR 到汇编”这一步单独拎出来给你手工用。很多调试场景根本不需要前端,直接拿一个.ll文件喂给 llc,快速验证后端改动是否正确。

3.2 IR 为什么是关键

LLVM IR 是整个项目的灵魂,它的全称是 Low Level Virtual Machine Intermediate Representation,但今天它已经不是一个“虚拟机”了。IR 的价值在于三件事:SSA 形式、强类型、平台无关。

SSA(Static Single Assignment)意味着每个变量只能被赋值一次。这样做的好处是变量和数据流关系变成了一张清晰的静态图,优化器可以更方便分析“谁赋值给谁”“这个变量后面有没有被用过”。强类型意味着 IR 里每个值都有明确类型,比如i32ptr<4 x float>,这让类型相关的优化能做得很早很彻底。平台无关意味着你在 x86 上生成的 IR,换一个后端重新生成就可能是 ARM 代码。

IR 还有三种形态:内存中的数据结构、文本形式的.ll文件、二进制形式的.bc文件。文本形态最重要,因为你可以亲眼看一个函数经过优化后变成什么样。这也是我建议所有初学者做的第一件事:写一小段 C 函数,用clang -S -emit-llvm -O2生成.ll文件,然后打开读一读。看完一段 IR,你对编译器的“翻译结果”就不会再有黑盒恐惧。

3.3 新 Pass 管理器到底好在哪

很多人刚接触 LLVM 写 Pass 时,会被“两种 Pass 管理器”搞晕。老的管理器叫 Legacy Pass Manager,现在已经基本处于维护状态;新的是 New Pass Manager(NewPM)。为什么要换?最核心的原因是老管理器里所有的 Pass 都是全局静态注册,你很难精确控制 Pass 的执行顺序和组合方式,而且并行 pass 执行时容易有状态冲突。

新 Pass 管理器把分析(Analysis)和变换(Transform)分成两类,可以在 Module、Function、CGSCC(调用图强连通分量)这些不同的单元粒度上组织 Pass 流水线。最方便的是PassBuilder提供的registerPipelineParsingCallback,它允许你定义一个字符串名字,然后通过opt -passes=your-pass-name直接在命令行加载自己的插件 Pass。整个流程非常干净,不需要改 LLVM 源码、不需要重新编译整个 LLVM,写插件就能完成实验。后面第五节我会给一个可以运行的例子。

所以现在你要写一个新的优化,几乎都是从“定义一个继承PassInfoMixin<YourPass>的结构体,重写一个run方法”开始。新 Pass 的生命周期由管理器掌控,分析结果可以缓存复用来,多个 Pass 共享计算,整体效率和可组合性都比老模型高很多。

4. 实操:从源码构建 LLVM

4.1 环境准备与依赖

在我写的所有编译器入门建议里,最重要的一条是:不要偷懒用发行版自带的 libLLVM 包做深度开发,自己 build 一个属于你的 llvm-project 树,这样改代码、加 Pass、调试符号都对得上。构建前先确认几项环境:

  • 系统:Linux(推荐 Ubuntu/Debian 或 Fedora)或 macOS,也可以 Windows + WSL2;
  • 编译器:需要系统预装一个可用的 gcc 或 clang,早期 bootstrap 阶段靠它编译 LLVM 自身;
  • 构建工具:CMake 3.20+,Ninja 是首选生成器;
  • Python:用于跑 lit 测试工具;
  • 磁盘:至少预留 40GB(Release 构建通常 10~20GB,Debug 更大);
  • 内存:16GB 以上比较稳妥;如果只有 8GB,就要限制并行任务数。

仓库克隆我习惯用浅克隆加指定 tag,不用下载全部历史,省时间也省磁盘:

git clone --depth 1 --branch llvmorg-17.0.6 https://github.com/llvm/llvm-project.git cd llvm-project

这里建议选最新的 release tag,不要直接切 main 分支。main 分支每天都在变,API 和构建选项可能突然就和你搜索到的教程不匹配了。

4.2 CMake 配置与关键参数

进到仓库后,创建一个build目录,在目录里执行 CMake。我给一个经过很多机器验证的推荐配置:

mkdir build && cd build cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS="clang;lld;libcxx;compiler-rt" \ -DLLVM_TARGETS_TO_BUILD="X86" \ -DLLVM_ENABLE_ASSERTIONS=ON \ -DLLVM_USE_LINKER=lld \ -DLLVM_PARALLEL_LINK_JOBS=2 \ ../llvm

解释几个关键参数:

  • LLVM_ENABLE_PROJECTS:决定了要额外构建哪些子项目。默认只编 LLVM 核心,至少要把clang拉进来,否则你没法验证最终生成的编译器;如果要做链接器实验就加lld,要跑 libc++ 测试就加libcxx
  • LLVM_TARGETS_TO_BUILD:只构建你现在真正需要的目标后端。绝大多数人只需要X86,构建时间和内存会骤降。如果做交叉编译实验,可以加AArch64;ARM;RISCV
  • LLVM_ENABLE_ASSERTIONS:Debug 场景强烈建议开启,很多 IR 不合法问题都是靠断言提前暴露的;Release 构建如果想稳定发布也可以关掉来提速。
  • LLVM_USE_LINKER=lld:先用系统工具链编出 lld,然后让后续链接过程用 lld,比默认的 GNU ld 快很多。
  • LLVM_PARALLEL_LINK_JOBS=2:限制并行链接任务数,链接 LLVM 各库非常吃内存,不限制内存小的机器直接被杀。

配置完成后,建议不要一次性ninja全量,而是指定几个核心目标先跑:

ninja clang lld opt llc

这里optllc是后面写 Pass、看 IR 的重要工具,一定要先编出来。全量ninja只留到某次睡觉前让它慢慢跑就行。

4.3 构建加速与日常迭代

构建 LLVM 是一个可以量化的体力活,第一次全量编译通常要二三十分钟,内存小的机器可能到一个小时。这里有三个提速经验:

  • ccache缓存编译中间产物。第一次虽然慢,但以后每次改一行代码,重编速度会感觉像换了一台机器。在 CMake 时加-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache即可。
  • 只编你自己需要的 target。每天改 LLVM Pass 的人一般只需要ninja opt,这比ninja clang快得多。要验证前端行为时才去编 clang。
  • 关闭测试和示例的构建,可以省很多编译时间。在 CMake 里加-DLLVM_INCLUDE_TESTS=OFF -DLLVM_BUILD_EXAMPLES=OFF -DLLVM_INCLUDE_BENCHMARKS=OFF

构建完成之后,验证一下版本:

./bin/clang --version ./bin/llvm-config --version

如果clang --version能正常输出,说明你的 LLVM 工具链已经能用了。这时候拿它编译一个 hello world:

./bin/clang -O2 hello.c -o hello && ./hello

跑通之后,你的环境就达到了“可动手实验”的及格线。

5. 第一个 LLVM Pass,写给新手的入门代码

5.1 Pass 的骨架

写 Pass 是理解 LLVM 优化最直接的方式。假设我们要写一个非常简单的 Function Pass:遍历每个函数,在 stderr 打印函数名。用新 Pass Manager 的插件方式,代码长这样:

#include "llvm/IR/Function.h" #include "llvm/IR/Instructions.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Support/raw_ostream.h" using namespace llvm; namespace { class HelloPass : public PassInfoMixin<HelloPass> { public: PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { errs() << "Hello from function: " << F.getName() << "\n"; return PreservedAnalyses::all(); } }; } // namespace extern "C" ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, "HelloPass", LLVM_VERSION_STRING, [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "hello") { FPM.addPass(HelloPass()); return true; } return false; }); }}; }

这里有两个关键点。第一,PassInfoMixin<HelloPass>是模板 CRTP 方式,它会帮我们生成一些 Pass 元信息,我们只需要实现run方法。第二,llvmGetPassPluginInfo是整个插件的入口,registerPipelineParsingCallback让 LLVM 知道,当用户在命令行写-passes=hello时,就把HelloPass加到当前函数 Pass 流水线里。

返回PreservedAnalyses::all()的意思是这个 Pass 没有改动任何 IR,所有分析结果都仍然有效。如果你真的修改了 IR,必须精确声明破坏了哪些分析,否则后续 Pass 用到了过期分析结果会出很难查的 bug。

5.2 用 opt 命令行跑起来

有了代码,怎么把它编译成一个.so插件呢?假设你的 LLVM 构建目录是一个独立的 build 目录,可以用一个最简单的 CMakeLists.txt 管理:

cmake_minimum_required(VERSION 3.20) project(HelloPass) find_package(LLVM REQUIRED CONFIG) message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") add_library(HelloPass MODULE HelloPass.cpp) target_link_libraries(HelloPass PRIVATE LLVM) set_target_properties(HelloPass PROPERTIES PREFIX "")

需要注意,这里的find_package(LLVM REQUIRED CONFIG)依赖 LLVM 的 CMake 配置文件。如果你没有把 LLVM 装进系统目录,需要设置LLVM_DIR/path/to/llvm-project/build/lib/cmake/llvm。配置方式如下:

mkdir helloPass-build && cd helloPass-build cmake -DLLVM_DIR=/path/to/llvm-project/build/lib/cmake/llvm .. make -j4

编译成功后会生成一个HelloPass.so,然后用opt加载并运行:

/path/to/llvm-project/build/bin/opt \ -load-pass-plugin=./HelloPass.so \ -passes=hello \ -disable-output \ /path/to/test.ll

test.ll可以从任意 C 文件生成:

/path/to/llvm-project/build/bin/clang -S -emit-llvm -O0 hello.c -o test.ll

如果一切正常,你会看到每个函数名都被打印出来。第一次跑通这个流程时,你会对“Pass 插件”有非常直观的感觉:不需要改 clang 源码,不需要重编整个 LLVM,一份.so就能插入到编译管线里做自定义转换。

5.3 为什么用 Pass Plugin 而不是把代码编进 LLVM

写实验性质 Pass 的推荐做法永远是插件,原因有三个。

一是编译成本低。LLVM 主树几十万行代码,每改一次就重新链接一遍非常痛苦,而独立插件只需链接几个 LLVM 库,增量编译很快。

二是隔离性好。插件的 bug 再严重,也不会污染主工具链。你在处理正常代码时,不加载这个插件,行为完全不受影响。

三是分发方便。你可以把.so文件发给别的研究者,只要 LLVM 版本一致,对方直接可以跑,无需拿到你的源码再编。

等到你确定这个 Pass 要从实验变成产品,才考虑把它提交到llvm/lib/Transformsllvm/lib/Passes并注册到 PassBuilder。这时需要注意,产品化后不仅要实现run,还要通过llvm/docs/Passes.rst增加文档、通过test/增加 lit 测试,代码风格也要过clang-formatclang-tidy。当然,这是后话了。

6. 常见问题与排查技巧实录

6.1 编译慢、内存爆掉

我见过最多的问题就是第一次编译直接 OOM,或者跑到一半ninja机器卡死。原因基本只有一个:默认并行任务数对内存不了解。一台 8GB 内存的机器,如果直接ninja,链接多个大目标文件会瞬间把内存吃满。

解决方案是按内存大小调整 CPU 并行度:

ninja -j4 # 8GB 内存保守设置 ninja -j8 # 16GB 内存比较稳

但真正的杀手是链接阶段,而不是编译阶段。所以我强烈建议在 CMake 时就设置-DLLVM_PARALLEL_LINK_JOBS=2,把链接并行数单独管住。再加-DLLVM_USE_LINKER=lld,链接速度能快三分之一到一半。

还有一个隐藏技巧:如果磁盘空间紧张,可以加-DLLVM_APPEND_VC_REV=OFF,减少每次 git 版本号变动导致的重复编译。日常开发时,尽量用增量构建而不是全量ninja,只编当前需要改的工具。

6.2 API 版本变动

LLVM 社区有一个几乎公认的“潜规则”:每个大版本都会改一批 API。今天你看到的run(Function &F, FunctionAnalysisManager &AM),在 LLVM 14 以后是主流,但更早版本可能会看到不同的函数签名。很多从网上抄来的旧代码在新版本直接编译失败,抱怨 “no matching member function”。

应对这类问题,我的经验是四步走:

  • 先确认自己用的 LLVM 版本,llvm-config --version
  • 去 GitHub 源码里搜索对应版本的分支,用git log --oneline -- <file>看关键改动;
  • 利用LLVM_DIR指向实际 build 目录,让 CMake 的find_package(LLVM)找到正确版本的头文件;
  • 如果看到LLVM_ABI_BREAKING_CHECKSLLVM_ENABLE_ASSERTIONS相关报错,检查编译宏是否与 LLVM 构建时一致。

版本变动最频繁的地方集中在新 Pass 管理器的注册方式、目标后端 API、以及 IRBuilder 的接口。写代码前先看当前 release 分支自带的示例,别套旧博客的代码。新版发布时,官方会有一份很详细的 Release Notes,里面列出“Changes to the LLVM C API”“Changes to the LLVM IR”这类小节,值得逐条扫一遍。

6.3 调试与 check 工具

很多人写完 Pass 只关心“能不能跑”,却不知道怎么调试“为什么优化结果不对”。我列几个常用的组合拳:

  • opt -print-after-all在每次 Pass 执行后打印 IR 快照,配合-filter-print-funcs=foo只打印感兴趣的函数;
  • opt -debug-only=loop-vectorize打印 LLVM_DEBUG 日志;
  • llvm-nmllvm-objdump检查目标文件的符号和汇编;
  • llvm-mca查看具体指令在目标 CPU 上的流水线时序。

如果你改了 core 代码,一定要跑一遍相关模块的回归测试。LLVM 的测试用lit管理,命令是这样的:

cd build ninja check-llvm-transforms

要是只想跑某单个测试文件,可以直接用llvm-lit加路径。测试失败分两种:一种是你改坏了并导致现有行为不一致,另一种是目标平台不同导致的 expected 内容差异。第一种要认真改代码,第二种通常要更新测试输入,但改之前一定确认你的思路是对的。

6.4 一份 FAQ 速查表

下面是我觉得最高频的场景,整理成一张速查表:

症状常见原因处理方式
ninja中途 OOM 被 kill链接任务并行数过高-DLLVM_PARALLEL_LINK_JOBS=2,减小-j
CMake 找不到 LLVM Config没设置LLVM_DIR或版本不匹配设置-DLLVM_DIR=/path/to/build/lib/cmake/llvm
插件加载时报 API version mismatchLLVM 和插件不是同一版本重新用同一套 build 目录编译插件
头文件版本报错系统自带 LLVM 和项目 LLVM 混用include 路径优先指向 build 目录,用-I指定
Pass 跑起来后出现 IR 验证失败Pass 修改 IR 后没更新分析结果精确返回 PreservedAnalyses,避免残留非法 IR
clang --version报找不到动态库构建目录里的 lib 没加进库搜索路径LD_LIBRARY_PATH=/path/to/build/lib或固定 RPATH
测试lit找不到工具链构建时没设置工具链路径在 build 里重新跑cmake --build .,确认bin下工具有生成

还有两个容易被人忽略的点。一个是 DEBUG 构建和 RELEASE 构建如果混着用同一个 build 目录,会导致一堆缓存问题,建议不同配置建不同 build 目录。另一个是别在系统目录里直接make install覆盖系统自带的 LLVM,这会把整个系统弄得乱七八糟。除非你有明确的分发需求,否则一直用构建目录下的bin工具就行。

写在最后

说实话,我从“不会用 LLVM”到“能动手改 Pass”,并没有读很多大部头文档,就是一步一步踩出来的。最关键的转变是意识到:LLVM 不是一个神秘的庞然大物,它只是一个设计得很好的工程系统。你先把它 build 起来,然后抓一个极小的问题,比如“给某个函数打印一行日志”,再用 debug 工具一点一点看 IR 的变化,慢慢就建立起自己的认知地图了。

我个人最推荐的进阶路径是:先写那个 HelloPass,再改一个真实的优化比如死代码删除,然后用llc把 IR 变成汇编亲自对比指令差异,最后去浏览llvm/lib/Passes/PassBuilder.cpp看默认优化的流水线编排。走完这么一圈,你再看到网上各种 “LLVM 后端移植”“MLIR 方言定义” 的博客,会发现自己已经能看懂八成以上了。这个项目很大,但它的入口其实比你想象的要窄,一个 Hello World 就够推开那扇门。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询