ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

LLVM入门实战:从零构建llvm-project并编写第一个Pass

2026/9/18 9:29:01 拓冰建站 浏览量
LLVM入门实战:从零构建llvm-project并编写第一个Pass LLVM 项目这个仓库常年躺在很多人的 star 列表里但真正敢去翻源码、敢去改代码的人其实没那么多。它有时候是“编译器入门者的噩梦”有时候又是“造轮子工程师的天堂”。我过去几个月里被问得最多的问题是llvm-project 到底怎么下手它是只有编译器还是有别的东西我能不能自己改一个优化逻辑跑起来这篇文章打算认真回答这些问题并且用一个实际能跑的 Pass 例子把整个流程串起来。文章适合三类人想系统学习编译器原理的在校生、需要用 Clang/LLVM 做定制工具链的工程师以及做语言前端或性能优化的研究人员。看完之后你至少能明白 llvm-project 的目录结构、构建逻辑、核心 IR 概念并且能照着步骤写出自己的第一个 LLVM Pass。不需要你之前写过编译器只需要一点 C 基础和对命令行不陌生。1. 先把 llvm-project 到底是什么这件事说清楚1.1 它不是一个编译器是一整套编译器生态很多人第一次看到 llvm-project 这个仓库名第一反应是“哦这就是个开源的 C/C 编译器”。严格来说这句话既对又不完全对。LLVM 本身确实能编译 C/C但只有当它跟 Clang 前端配合的时候才能做到。而 llvm-project 这个仓库里装的是远远超出“编译器”这个概念的一整套工具链生态。仓库里除了大家熟悉的 Clang 编译器前端还有 LLVM 核心库就是优化器和代码生成那部分、LLD 链接器、libc 标准库实现、compiler-rt 运行时库、LLDB 调试器以及 MLIR 这种专门用来做多级中间表示的框架。每个子项目拿出来单独都能撑起一个独立的开源项目社区。把它们放在同一个仓库里主要是为了统一版本管理、统一构建系统和统一发布节奏这样各组件之间的 API 变更能做同步更新不会出现 Clang 跟 LLVM 核心版本错位导致的不兼容问题。我一开始也没意识到这个仓库规模有多夸张。用 git 把整个仓库克隆下来之后光是.cpp和.h文件就上万甚至更多。你如果只是想学某个子模块建议采用 sparse checkout只拉取需要的部分。比如有些场景只需要 LLVM 核心不需要把所有前端语言的支持代码都拖下来能省不少时间和硬盘空间。1.2 前端、中端、后端是怎么配合工作的llvm-project 能适用于这么多场景核心原因是它把传统编译器的三段式结构做到了极致。传统编译器的三段式是前端Frontend、中端Optimizer、后端Backend而 LLVM 真正厉害的地方在于这三段之间的接口是稳定且明确定义的这个接口就是 LLVM IRIntermediate Representation中间表示。通俗点说前端负责“看懂”源代码。你写的是 C 还是 Rust 或者 Swift这一层都会把它们解析成语法树然后转成一种统一的、与语言无关的中间表示 LLVM IR。这里要划个重点只要语言前端能够生成 LLVM IR那后面的优化和后端就全部自动共享了。这也是为什么 llvm-project 里能塞下那么多语言支持Swift、Rust、Julia、Zig 这些语言都基于或部分基于 LLVM 的技术栈它们只需要做前端工作就能白嫖 LLVM 的优化器和几十种 CPU 平台的后端。中端做的事情是所有语言和所有 CPU 平台共享的优化逻辑比如循环展开、内联、死代码消除、常量传播。它们统统不关心你的源代码是 Java 还是 Kotlin也不关心最终目标是 x86 还是 ARM只针对 LLVM IR 这一种中间表示做分析和改写。后端则负责把优化后的 LLVM IR 翻译成目标机器的汇编码然后交给汇编器和链接器变成可执行文件。到了这一层各种平台相关的复杂处理都集中在这个环节比如指令选择、寄存器分配、指令调度等。后端是门槛最高的一部分但如果只是在 LLVM IR 这一层玩其实任何人都可以上手。1.3 为什么这套设计成了事实标准理解这个架构的好处是你不需要把整套工具链从头到尾学完就能在某个切片上做得很深。很多人做的事情是加一个前端把新语言编译到 LLVM IR还有人是写一个优化 Pass在 IR 阶段做业务定制的代码改写也有人专注于后端为一个新的芯片架构生成代码。这套设计能成为事实标准还有一个很实际的原因工程复用。一个编译器项目最贵的是优化器和后端而不是前端。前端主要跟语言特性打交道工作量虽大但逻辑相对线性优化器和后端要应对的则是各种平台差异和性能极限。LLVM 选择把最贵的部分做成公共底座让所有语言场景共用这是它在业界迅速铺开的根本原因。2. 从零构建 llvm-project编译实操记录2.1 构建前要准备的依赖和机器配置准备编译 llvm-project 之前我的建议是先看一眼自己的机器配置。内存建议至少 16GB硬盘最好留出 100GB 以上的可用空间因为开启全部目标架构后构建产物非常大。如果你只是做实验可以通过配置参数把范围缩小后面会详细说。依赖方面Linux 上需要 gcc 或 clang 作为宿主编译器还需要 cmake、ninja、python3 和 zlib 库。在 Debian/Ubuntu 系统上可以执行sudo apt-get install build-essential cmake ninja-build python3 zlib1g-dev。macOS 上可以用 Homebrew 安装 cmake 和 ninja。Windows 上稍微麻烦一点虽然官方支持 Visual Studio 构建但我个人体验下来WSL 反而更顺利。如果你必须使用 Windows 原生环境做开发建议直接装 Visual Studio 2022然后用它的命令行工具启动 CMake 流程。构建 LLVM 首推 Ninja 而不是 Unix Makefiles因为 Ninja 对增量构建的处理更聪明。LLVM 是个巨型工程改动一个头文件可能导致几千个目标需要重新编译Ninja 在这种场景下的并行度控制和构建速度明显有优势。cmake -G Ninja指定生成器后面接一堆-DLLVM_*参数控制构建内容这套配置方案是可复制的。2.2 CMake 配置里几个关键参数怎么选初次尝试的人最大的误区是试图把所有项目全部构建一遍“怕漏掉什么”。实际上 90% 的实验只需要构建指定的几个组件编译时间可以从两三个小时降到半小时以内。常用的一组最小配置大概是这样的cmake -G Ninja -S llvm -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_BUILD_TESTSON-DCMAKE_BUILD_TYPE需要说明一下。用 Debug 编译出的工具链基本无法用于生产环境速度慢好几倍但调试符号非常完整适合动态分析源码流程。Release 则能保证优化器性能但调试信息会被削弱。我平时的习惯是单纯测试 Pass 逻辑用 Release要一步步追踪代码执行路径时用 RelWithDebInfo它能在合理性能基础上保留足够的调试符号。-DLLVM_ENABLE_PROJECTS是决定你编译哪些子项目的关键参数。多个项目之间用分号分隔比如要同时启用 clang 和 lld就写成-DLLVM_ENABLE_PROJECTSclang;lld。这里有个坑仓库根目录默认情况不会自动构建所有项目你必须显式声明。很多人误以为整个仓库都在构建列表里结果编译完只看到 llvm 相关工具发现没有 clang就是这个原因。-DLLVM_TARGETS_TO_BUILD控制后端目标架构。只想在本机跑测试就填X86能省下大量编译时间。默认值是all也就是所有支持的架构全编那一串列表看着很震撼实际上你基本用不到其中的绝大多数。2.3 一步步执行构建并验证结果配置完成后进入构建目录并执行ninja首次构建像一次小型马拉松。如果只想编译指定目标可以更细分。例如只构建 clang、llvm-as 和 opt命令是ninja clang llvm-as opt这三个工具足够完成大部分 IR 实验。我实测过一台 8 核 16 线程的机器只构建 X86 后端的 Release 版 LLVM 和 Clang大约需要 25 到 40 分钟。如果是对全架构做 Debug 构建再同时开启 compiler-rt 等项目那真能编译一个下午。构建完不要急着干别的先做快速验证。执行./build/bin/llvm-as --version ./build/bin/clang --version能看到版本信息就说明核心工具链已经可用了。你也可以写一个简单的 C 程序让 clang 编译再用./build/bin/opt --version验证优化器。如果这三样都正常说明本次构建没有问题。3. 源码结构导航和核心子项目定位3.1 顶层目录都有什么哪些值得优先看llvm-project 的顶层目录结构非常清晰每个文件夹就是一个子项目或配套工程。llvm 目录是整个工程的核心里面又分 include、lib、tools、test 等子目录用来承载 LLVM 核心库、命令行工具和测试用例。clang 目录是 C/C 编译器前端包含它的词法分析、语法分析、语义分析和代码生成逻辑。对于新手来说我建议的阅读顺序是先看 LLVM 的 include 目录尤其是llvm/include/llvm/IR这个路径。这一层定义了 IR 的基础数据结构比如Module、Function、BasicBlock、Instruction、Value理解了这些类的含义就相当于拿到了理解整个优化体系的门钥匙。第二步是打开llvm/lib/Transforms/Utils和llvm/lib/Transforms/Scalar里面是各种具体优化 Pass 实现比如InstructionCombining、GVN、LoopUnroll等。这些 Pass 是“如何分析和改写 IR”的最佳范本代码量适中注释质量也不错新手读起来不至于一下子被淹没。其余目录很多是在特定业务场景下才有用。比如lld是那个号称“比 GNU ld 快好多倍”的链接器libcxx是 C 标准库实现compiler-rt里放的是各种运行时支持库flang是 Fortran 前端mlir则是重头戏但也最复杂。建议先把 LLVM 核心的 IR 和 Transforms 读透再考虑这些特殊领域的项目。3.2 LLVM IR 的几个核心数据结构必须认识它们LLVM IR 是介于高阶语言和汇编之间的一种静态单赋值SSA形式的中间表示。所谓 SSA 简单来说就是每个变量只能被赋值一次后面再使用它的时候直接引用这个名字就行。这个设计让很多数据流分析变得异常简单也是 LLVM 优化器强大的一大原因。IR 的基本单元是 Module一个 Module 对应一个编译单元里面包含若干个 GlobalVariable 和 Function。Function 由若干个 BasicBlock 组成BasicBlock 则是指令的线性序列每个基本块只有一个入口和一个出口。重点概念是每个 BasicBlock 的末尾通常会有终止指令比如跳转指令br或返回指令ret这就保证了块与块之间的控制流关系可以通过终止指令来建立。每条指令都是Instruction的对象它是Value的子类。这里要理解Value和User的关系Value 是一个可以被使用的对象User 是使用它的对象。比如一个加法指令它是一个 User它引用了两个操作数它们也是 Value同时它也产出一个新 Value这个 Value 可以被后面的乘法指令用。这套 use-def 链关系贯穿了整个 LLVM 优化体系几乎所有分析 Pass 都基于这个关系做数据流传播。手头没有完整构建的时候也可以拿一段简单 C 代码来观察 IR 的样子。比如int add(int a, int b) { return a b; }用clang -S -emit-llvm -O0 add.c生成可读的 IR 文件。你会看到define i32 add(i32 %a, i32 %b)这样的函数定义其中i32指的是 32 位整数类型。这一步是理解 LLVM IR 的最佳入门方式比直接啃文档直观得多。3.3 Clang、LLD、libc、MLIR 各自扮演什么角色Clang 是整个 LLVM 生态里最常用的前端。它对 C、C、Objective-C 提供支持因为代码结构清晰、模块化程度高很多静态分析工具也直接基于 Clang 做二次开发。如果你需要实现自定义代码检查比如找出某种违反团队规范的写法Clang 的 AST 匹配机制就是最好的工具这比偷偷改编译器后端要高效得多。LLD 是一个高速链接器。传统 GNU 链接器处理大型 C 项目时可能需要数秒甚至更久LLD 经常能把这个时间缩短到几百毫秒级别。这一块对日常开发效率的提升非常明显所以很多构建系统默认就切到了 LLD。它的实现思路在于多线程并行化符号解析和重定位以及通过数据布局优化减少 IO 瓶颈。libc 是 LLVM 项目维护的 C 标准库实现。如果你在用 Clang 编译 C 程序clang 会默认尝试使用 libc 作为标准库尤其是在 macOS 和部分 Linux 发行版中。它跟 libstdc 的主要区别是对 C 新标准的支持速度更快也更强调代码的简洁性和可移植性。做跨平台 C 库开发的朋友对这个库的口碑往往比较关注。MLIR 是把 AI 编译器这波浪潮带火的重要框架。它设计了一套多级 IR 体系可以在高层抽象和低层抽象之间灵活切换。像 TensorFlow 和 PyTorch 的编译器后端很多重要工作都通过 MLIR 完成。但它的学习曲线比 LLVM 的 IR 陡峭很多不推荐零基础直接从 MLIR 入手先把 LLVM IR 搞明白再学会更顺畅。4. 亲手写一个 LLVM Pass完整实操记录4.1 Pass 是什么能干什么Pass 是 LLVM 优化框架里的基本执行单元。一个 Pass 每次会处理整个 Module、单个 Function 或单个 Loop对 IR 做指定的分析和改写。我的第一个 Pass 是给函数入口自动插入打印日志的 Pass用途是在编译期给每个函数加一句调试输出。听起来像是屎山但在一些嵌入式调试、性能追踪场景里非常实用而且能很好地演示 IR 操作流程。LLVM 的 Pass 框架在新版本上采用了 new Pass Manager对应的写法和旧版不一样。这里直接用新接口因为它是当前主流的开发方向旧接口已经在逐步移除。一个最基本的 FunctionPass 需要实现一个runOnFunction()方法这是 legacy 接口写法新的写法是重写run()方法。为了少踩坑我建议从现在开始直接看新 Pass Manager 的官方文档不要被网上大量旧教程误导。4.2 Pass 代码实现与逐段分析下面这段代码展示了一个 FUNCTION PASS 的骨架它的作用是在每个函数入口插入一条调用printf的指令。核心逻辑并不复杂但在动手写之前需要提前准备一个前提打印函数本身怎么拿到 IR 里的函数声明。#include llvm/IR/Function.h #include llvm/IR/IRBuilder.h #include llvm/IR/Instructions.h #include llvm/IR/Module.h #include llvm/Pass.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h using namespace llvm; namespace { class PrintFunctionPass : public PassInfoMixinPrintFunctionPass { public: PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { // 只在函数不为空时做处理 if (F.empty()) return PreservedAnalyses::all(); // 获取当前函数所属模块方便创建外部函数声明 Module *M F.getParent(); LLVMContext Ctx F.getContext(); // 构造 printf 的函数类型int (const char*, ...) FunctionCallee PrintFunc M-getOrInsertFunction( printf, FunctionType::get( IntegerType::getInt32Ty(Ctx), {PointerType::getUnqual(Ctx)}, true // 可变参数 ) ); // 从函数入口基本块开头插入指令 BasicBlock Entry F.getEntryBlock(); IRBuilder Builder(*Entry.getFirstInsertionPt()); // 创建字符串常量 Value *Str Builder.CreateGlobalStringPtr(call function: %s\n); // 调用 printf(Str, FuncName) Builder.CreateCall( PrintFunc, {Str, Builder.CreateGlobalStringPtr(F.getName())} ); return PreservedAnalyses::none(); } }; } // end anonymous namespace这段代码我逐行解释。F.empty()判断函数是否没有基本块空函数会被跳过。F.getParent()返回函数所属的 Module这是很常用的逆向上跳操作任何时候做跨函数分析都需要它。M-getOrInsertFunction是 LLVM 里非常省心地拿到或创建函数声明的方式如果 Module 里已经有同名的 printf直接复用否则就自动创建一个新的外部声明。IRBuilder是 LLVM 提供的指令构建工具它内部维护一个插入点所有新创建的指令都会自动插入到这个位置。Entry.getFirstInsertionPt()获取入口基本块的第一个合法插入点一般是第一条非 PHI 指令之前。这里特别要注意如果基本块的第一个操作是 PHI 节点或 phi-like 指令直接getFirstNonPHI()更合适否则指令插入位置不合法后面在验证时可能报错。Builder.CreateGlobalStringPtr会创建一个全局字符串常量并返回指向该字符串的指针。这样 printf 就能拿到字符串内容。我在实际实现时发现函数名用F.getName()直接取出来的是StringRef传给CreateGlobalStringPtr后会被自动转换为常量字符串。不过如果你要打印的函数名来自外部输入建议先做 UTF-8 合法性检查避免生成非法 IR 导致后续步骤崩溃。4.3 注册 Pass 并实际运行一次写完 Pass 源码还只是第一步你需要把它编译成动态库并让 opt 在运行 Pass 时加载它。新 Pass Manager 环境下要让 opt 识别自定义 Pass需要向 PassPlugin 注册。注册代码一般不放在上main 逻辑里而是放在插件入口extern C LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, PrintFunctionPass, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name print-function) { FPM.addPass(PrintFunctionPass()); return true; } return false; }); }}; }这段注册代码的用途是把命令行里形如-passesprint-function的字符串映射到真正的 Pass 对象上。没有这段代码opt 根本不知道print-function是什么。注册完成后用 clang 或者 gcc 把它编译成共享库clang -shared -fPIC -o libPrintPass.so PrintFunctionPass.cpp \ $(llvm-config --cxxflags --ldflags --libs)如果llvm-config工具能找到这条命令能自动带上所需头文件路径、库路径和链接库。这里碰到的一个常见问题是llvm-config版本与当前编译器不匹配导致链接时找不到某些符号解决方案是显式指定你构建目录里的llvm-config路径。编译完成后准备一个小测试文件int foo() { return 1; } int main() { return foo(); }先生成 IR 文件然后运行自定义 Passclang -S -emit-llvm test.c -o test.ll opt -load-pass-plugin./libPrintPass.so -passesprint-function test.ll -o test_opt.ll打开test_opt.ll应该能在每个函数入口看到对 printf 的调用。这说明你的第一个一点用没有但能跑通的 LLVM Pass 正式诞生了。为什么说“有点没用”因为现实中做这种直接插入 printf 的优化型 Pass 很少后端插桩通常还要考虑 printf 的重入问题和日志格式的上下文但作为理解 Pass 运行机制它已经把最核心的流程走通了。5. 实操中高频踩坑与排查方法5.1 编译阶段容易遇到的几种启动问题先说最典型的坑头文件路径找不到。如果你直接在 llvm-project 顶层目录下调用llvm-config很可能得到的是系统自带的旧版 llvm 路径而不是你刚构建出来的那套。这时要么把新构建的 bin 目录加入 PATH要么使用构建目录下的llvm-config比如build/bin/llvm-config。最稳妥的做法是在 CMake 项目里用find_package(LLVM REQUIRED CONFIG)并把路径指向你构建出来的 LLVM cmake 包位置。另一个高频问题是从 GitHub 拉取仓库时子模块缺失。llvm-project 有些组件会引用外部依赖比如某些测试用例需要第三方库。克隆时如果不带--recursive你会发现某些子目录是空的。如果已经克隆完了再补用git submodule update --init --recursive可以解决但这会拉取大量数据网络不好会很痛苦建议在克隆时就一次到位。还有 build 目录污染的问题。有些人图省事直接在 llvm 源码目录里创建 build 文件夹如果之后切换了生成器比如从 Makefiles 切换到 Ninja有概率出现配置残留导致 CMake 缓存错乱。最干净的方案是每次重新配置时新建一个干净的 build 目录。磁盘充足的前提下多备份几个不同配置的 build 目录反而很值得。5.2 Pass 调试的常用手段别只靠输出日志写过 Pass 的人都知道IR 层面的 bug 往往不会直接报错而是让某个后续 Pass 崩溃或者生成性能很差的代码。第一类问题是崩溃尤其是空指针解引用。原因大多是你在run()里获取某个Function或BasicBlock时没有检查空值比如某些函数可能没有返回指令某个基本块可能不存在。防御性写法是在每次访问前先断言或判断别认为“这个函数一定能拿到返回值”。第二类是结果不对但程序不崩溃这种情况最气人。我的建议是先在 IR 层做小规模测试把输入 IR 压缩到尽可能小的函数上方便逐步对比。不要直接喂一个几千行的源文件进去调试体验会很差。用llvm-diff或者手动比对修改前后的 IR 文件能更快定位是哪一步改写出了问题。比较次的调试方式是写一堆errs()打印。打印日志本身没问题问题是打印完别删放到一个可以开关的 debug 分支里。LLVM 提供的LLVM_DEBUG宏带有一个 Debug 标志位可以用-debug-onlyprint-function这样的命令行选项控制输出比裸errs()好用得多。这样调试信息留在代码里也不会影响 release 模式的干净程度。5.3 不同 LLVM 版本之间的 API 差异llvm-project 是个滚动更新的项目API 变动频率很高。最让我感慨的是网上搜到的 2022 年写法在 2024 年可能就默默失效了。官方文档虽然保持更新但历史版本的文档和源码往往很难找到配套的说明这让新人在二选一时非常困惑。经验是优先参考你本地源码里的示例和头文件注释因为源码就是你正在用的版本的唯一事实来源。比如想看注册 Pass 的确切流程直接去llvm/lib/Passes/PassRegistry.cpp或者 Examples 目录里找那是最新最正确的写法。善用 git 版本管理也很重要。如果你在公司或者团队内部做长期维护的编译器工具链建议把 llvm-project 固定到你验证过的版本上不要频繁追最新版。每次升级要做一次回归测试因为任何一个小版本的 API 变化都可能导致所有自定义 Pass 需要重新适配。6. LLVM 生态延伸为什么它会继续扩张6.1 从编译器到通用代码分析框架llvm-project 的影响力早已超出传统编译器范畴。在代码分析方向很多团队基于 Clang 做静态分析用 AST 匹配器查找潜在的代码缺陷比如未初始化变量、危险的类型转换、资源泄漏等。由于 Clang 的 AST 足够完整且类型信息丰富比正则表达式的静态扫描要准确得多这类工具在企业内部代码质量体系中越来越常见。在代码生成方向很多 DSL领域特定语言项目直接编译到 LLVM IR然后享受现成的优化和后端支持。比如一些 GPU 编程语言、稀疏计算框架、数据库查询编译器等都有 LLVM 的身影。它们的共同点是把重活都交给了 LLVM自己只负责把高层语义翻译成 IR。这种生态扩张背后其实是工程上的理性选择。与其为每个行业反复造一套优化器不如只在语言语义差异化层面做文章公共底座直接复用 LLVM。这样一来学习 llvm-project 的价值就不只在于懂的编译器而是相当于掌握了一套通用的程序分析和变换基础设施。6.2 新语言和新芯片都能直接受益每出来一门新编程语言几乎绕不开编译器基建这个话题。要么从零手写一个编译器要么基于 GCC要么基于 LLVM。现在新语言设计者选 LLVM 的比例非常高原因很简单LLVM 支持的后端平台多社区活跃度最高工具链完整度也最强语法分析、优化、链接、调试器全都配齐了。芯片公司也在大量使用 LLVM。一个新的 CPU 架构出来最头疼的事情之一是没有可用的编译器。基于 LLVM 做后端可以在相对短的时间内支持一套可用的 C/C 工具链而且能跟在 LLVM 主线上持续获得优化支持。这个过程中写一个后端虽然难但通常比从零做一个完整的编译工具链还是划算太多了。从个人发展的角度看投入时间研究 llvm-project不只是学一个具体软件而是进入一个长期活跃的技术生态。只要编译器、编程语言和芯片架构这三个领域还在推陈出新LLVM 这一层基础设施的价值就不会消退。6.3 下一步的学习路径与推荐资源如果你已经能把 LLVM 构建出来、跑通了一个自定义 Pass下一步最值得做的方向是写一个真正有点用的 IR 分析或改写 Pass。比如统计每个函数的基本块数量并输出报告这是一个很好的分析类 Pass 练习只用遍历指令和基本块几乎不涉及复杂数据流算法。然后再尝试做 Dead Code Elimination 的简化版实现或者做一个简单的函数内联。这个阶段的重点是让你学会处理 IR 迭代和修改时的基本注意点比如迭代器失效、删除指令后的 use-def 链维护、修改后是否要重新运行分析等。这些坑只有亲手踩过才能形成真正的肌肉记忆。官方文档方面首推llvm.org/docs里的 tutorial 章节尤其是Getting Started with the LLVM System和Writing an LLVM Pass虽然不是保姆级的教程但作为路线图足够靠谱。如果觉得纯看文档枯燥可以在 YouTube 或 B 站上找一些 LLVM 开发者大会的演讲录像看看真实的编译器大牛是怎么切入问题的会比单纯看文档直观很多。我在实际使用中的体会是llvm-project 是一个越深入越觉得自己什么都不懂的仓库但这也是它最有魅力的地方。你不需要一开始就把全部目录都吃透只要从一条最细的链路切入真正理解从源代码到 IR 再到可执行文件的流动过程后面的学习就有了稳定的坐标系。希望这篇文章能帮你迈出第一步。