从零开始玩转 llvm-project:架构拆解、源码构建与第一个自定义 Pass
如果你写过 C/C++,肯定听过 Clang;如果你研究过编译器或语言设计,大概率绕不开 LLVM。但“llvm-project”这个仓库名字对于很多刚接触的人来说,其实有点劝退:仓库庞大——clone 下来几个 GB,构建一次等半小时,目录结构复杂得让人不知道从哪看起。这篇文章就是我基于 llvm-project 实际使用经验的完整拆解,会从整体架构、为什么选它、源码构建的关键参数、到写一个自定义优化 Pass 并跑通全流程,把每一步背后的原理和坑都讲清楚。
不管你是想基于 LLVM 做一门新语言的编译器后端,还是想在现有 C/C++ 工具链上做定制优化、插桩、静态分析,或者只是好奇“编译器到底是怎么工作的”,这篇内容都能帮你把 llvm-project 从“听过名字”变成“能上手操作”。我会尽量站在实操角度讲,少说空话,多贴可以直接抄走的命令、代码和配置。
1. 核心设计与架构思路:llvm-project 为什么这么重要
1.1 先搞清楚 llvm-project 里到底有什么
很多人会把“LLVM”和“Clang”混为一谈,其实这俩是包含关系。llvm-project 是一个多仓库合并后的 monorepo,里面最核心的几个子项目分别是:
- LLVM 核心库:提供中间表示(IR)、优化器、目标后端代码生成、汇编器、反汇编器等基础设施。
- Clang:C/C++/Objective-C 的前端,把源码解析成 AST,再降级为 LLVM IR。
- LLD:一个高性能的原生链接器,链接速度比系统自带 ld 快好几倍。
- libc++ / libc++abi:C++ 标准库的 LLVM 实现,主要在 macOS、嵌入式等场景用得多。
- compiler-rt:运行时库,包含 ASan、UBSan、TSan 等各类 sanitizer 的实现。
- MLIR:用于构建可复用、可扩展编译器基础设施的多层级 IR 框架,近几年 AI 编译器特别喜欢用。
- flang、libunwind、polly、lldb等其他组件。
本质上,llvm-project 给你的是一套完整的“编译器工厂”。如果你只想编译 C,Clang 是成品;但如果你想做一门新语言,或者想往编译流程里插一段自定义逻辑,LLVM 提供的是积木,你自己拼。这也是它和 GCC 最大的区别:GCC 是一个完整的编译器,但它的内部结构没那么容易拆出来复用;LLVM 从第一天起就是按库来设计的。
1.2 三段式架构:前端、中端、后端的解耦
LLVM 最经典的架构就是三段式:前端负责把源代码转成中间表示 IR,中端(优化器)对 IR 做各种变换,后端把 IR 生成目标机器码。React/Vue 开发者看到这个可能觉得眼熟——这不就是“编译器的 MVVM”吗。前端对应模板解析,IR 对应虚拟 DOM,后端对应渲染器。换一个后端,就是换一套渲染目标,前端和中端完全不用动。
这种解耦带来的实际好处太明显了:
- 一门新语言只需要写前端, 比如 Rust 早期直接用 LLVM 做后端,省掉了几乎所有指令选择和寄存器分配的工作。
- 一个新的 CPU 架构只需要写后端, 前端和优化器直接复用。苹果从 x86 切到 ARM 时,Clang/LLVM 的迁移成本远低于想象。
- 优化器独立于语言和硬件, 所有前端产出的 IR 都过同一套优化管线,这意味着你做一次优化,所有语言都能受益。
理解这点是使用 llvm-project 的关键。你在写 Pass 时,处理的不是 C 代码,也不是汇编,而是中间层级的 IR。IR 大概是这样的:
define i32 @add(i32 %a, i32 %b) { %sum = add i32 %a, %b ret i32 %sum }它把变量、类型、控制流都显式表达出来,每条指令都是一个操作码加若干操作数。你后续写的所有自定义逻辑,基本都是在跟这种 IR 打交道。
1.3 为什么 llvm-project 能成为“编译器的事实标准”
2010 年之后,LLVM 几乎把编译器基础设施赛道赢麻了。背后有几个原因可以展开说说。首先是模块化设计带来的可嵌入性,Android NDK 用它,iOS 的 Xcode 用它,Rust、Swift、Julia 的全链路编译用它,甚至 PlayStation、Xbox、Switch 这类游戏主机的官方 SDK 底层也有它的影子。其次是宽松的开源协议,LLVM 使用 Apache 2.0 with LLVM exception,对商业闭源使用非常友好,企业可以放心把它嵌进自己的产品里而不需要开源代码。第三是工具生态的完备性,sanitizer 系列让内存错误排查变得极其方便,clang-tidy 是静态检查利器,lld 让链接速度起飞,这些配套工具和编译器本身配合得天衣无缝。
这些优势叠加起来,就让 llvm-project 成了一个“你迟早要用到”的仓库。哪怕你不是编译器方向的人,只要做 C/C++ 性能优化、做交叉编译、做安全审计,都绕不开它。
2. 从源码构建 llvm-project:关键步骤与参数选型
2.1 环境准备:硬件、系统和前置依赖
在使用 llvm-project 之前,你最好先确认一下自己的机器能不能扛住编译。这个项目构建起来极度吃内存和 CPU。我自己的实际经验是,4 核 8G 内存的机器,全量编译三个小时起步,i7 八核十六线程外加 32G 内存,Ninja 并行拉满二十分钟到四十分钟可以搞定核心部分。
操作系统上,Linux 和 macOS 都比较顺利,Windows 上也能编译,但体验会打折扣。如果你在 Windows 上,建议直接用 Visual Studio 的 CMake 生成器,否则各种工具链的小问题会消耗大量时间。
前置依赖要看你的系统。Ubuntu/Debian 上安装这几个包基本就够:
sudo apt install build-essential cmake ninja-build python3 gitmacOS 上装个 Xcode Command Line Tools 就行:
xcode-select --install另外建议确认一下 CMake 版本不低于 3.20,LLVM 新版本对 CMake 版本有硬性要求。太老的 CMake 会在 configure 阶段直接报错。
2.2 CMake 配置参数:每个选项背后的逻辑
克隆代码:
git clone https://github.com/llvm/llvm-project.git cd llvm-project默认分支是 main,还在快速迭代中。如果你想求稳,建议切到 release 分支,比如llvmorg-18.1.0这种,我平时用 17 和 18 比较多:
git checkout llvmorg-18.1.0然后创建一个构建目录,在 llvm-project 外面建,千万不要在源码目录里直接 build,否则源码目录会被构建产物弄脏:
mkdir build cd build接下来是核心的 cmake 命令。我列一个自己常用的配置:
cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS="clang;lld" \ -DLLVM_TARGETS_TO_BUILD="X86;AArch64" \ -DLLVM_ENABLE_ASSERTIONS=ON \ -DLLVM_OPTIMIZED_TABLEGEN=ON \ -DCMAKE_INSTALL_PREFIX=$HOME/llvm-install \ ../llvm-project/llvm逐项解释一下这些参数,因为理解参数比复制命令更重要:
-G Ninja:使用 Ninja 作为构建系统。相比默认的 Unix Makefiles,Ninja 并行度更高,增量编译更快。强烈建议使用。-DCMAKE_BUILD_TYPE=Release:编译 LLVM 自身用 Release 模式。如果想调试 LLVM 或者你写的 Pass,改成 Debug,库体积和编译时间都会暴涨,但对排查问题很友好。-DLLVM_ENABLE_PROJECTS:决定要构建哪些子项目。我一般至少带上 clang 和 lld。注意不要贪多,全选的后果是构建时间成倍上涨。-DLLVM_TARGETS_TO_BUILD:指定生成哪些后端的机器码生成器。默认是“全部”,但你大概率只需要 X86 和 AArch64,其他目标会白白增加编译时间。-DLLVM_ENABLE_ASSERTIONS=ON:开启断言。这对后续写 Pass 非常有用,很多错误会在断言里直接暴露出来,而不是在运行时静默崩溃。-DLLVM_OPTIMIZED_TABLEGEN=ON:用 Release 模式单独构建 TableGen(LLVM 的代码生成器),能显著加快整个构建过程,强烈推荐开启。
这些参数并不是我随手写的,每一个都对应实际开发中的真实痛点。比如不限制LLVM_TARGETS_TO_BUILD的情况下,你会花大量时间编译一堆用不到的后端;不开LLVM_OPTIMIZED_TABLEGEN,构建速度可能慢 30% 以上。
2.3 构建与验证产物
配置完成之后,执行构建:
ninja如果你只想先快速验证,可以只构建部分目标,比如只要 clang:
ninja clang clangd构建完成之后,验证一下版本:
./bin/clang --version ./bin/llc --version如果能看到版本信息,恭喜你,llvm-project 核心工具已经可用了。注意此时所有产物都在 build/bin 目录下,不需要执行 ninja install 也完全够开发用。我之前开发 pass 时从没装到系统目录,直接在 build 目录里操作是最省心的。
2.4 几个我踩过的构建期大坑
关于构建,有太多悲伤故事可以讲,这里集中提炼几个最有价值的:
- 内存不足导致编译被 kill。Ninja 默认并行任务数等于 CPU 核心数,内存不够时直接 OOM。解决办法是限制并发任务数,比如
ninja -j4,或者降低优化等级,把-DCMAKE_BUILD_TYPE改成RelWithDebInfo以省内存。 - 磁盘空间爆炸。完整构建 llvm-project 大约需要 30G 以上空间,确保预留足够。另外建议构建目录和源码目录放在同一块读写速度较快的磁盘上。
- 编译器版本太低无法编译 LLVM 本体。LLVM 18 要求宿主编译器支持 C++17,且 GCC 版本高于 7.1,Clang 版本高于 5.0。老系统自带的 GCC 4.8 是没戏的。
- 不要乱开 BUILD_SHARED_LIBS。网上有些教程让你开
BUILD_SHARED_LIBS=ON,说可以减小二进制体积,但对 LLVM 这种重度模板化的 C++ 项目,开共享库反而会显著降低运行时性能,而且动态链接配置极容易出问题。默认静态库就好。
3. 实操:开发一个自定义 LLVM Pass 并跑通全流程
3.1 认识新旧两套 Pass Manager
Pass 是 LLVM 优化器的最小功能单元,你可以把它理解成“对 IR 的一次遍历和变换”。LLVM 这几年经历了从 Legacy Pass Manager 到 New Pass Manager 的迁移,这个切换坑了无数人。简单说,新 Pass Manager 从 LLVM 14 开始成为默认,它解决了旧 PM 的不少痛点:比如可以更精确地管理 Pass 间依赖,可以做更细粒度的缓存复用,也能支持 PI(Pass Instrumentation)之类的扩展点。
写新代码时直接使用 New PM 接口,不要再碰旧的那套。虽然网上大量老教程都在讲 Legacy PM 的写法(比如继承FunctionPass、实现runOnFunction、用宏INITIALIZE_PASS注册),这些内容在 2024 年已经逐渐失去参考价值,如果你按着它们写,大概率会在编译阶段碰上各种 API 不匹配的报错。
3.2 最小可运行的 Function Pass:完整代码与解释
我来写一个最简单但五脏俱全的示例:遍历模块里的每个函数,打印函数名、基本块数量、指令总数。这看起来简单,但是一个极好的骨架,后续你往run里加任意分析逻辑,整个框架都不用改。
先看项目结构:
my-pass/ ├── CMakeLists.txt └── MyPass.cppCMakeLists.txt 内容如下:
cmake_minimum_required(VERSION 3.20) project(MyPass) find_package(LLVM REQUIRED CONFIG) message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") message(STATUS "Using LLVM include dir: ${LLVM_INCLUDE_DIRS}") include_directories(${LLVM_INCLUDE_DIRS}) add_compile_definitions(${LLVM_DEFINITIONS}) add_library(MyPass MODULE MyPass.cpp) target_link_libraries(MyPass PRIVATE LLVMCore LLVMSupport)注意find_package(LLVM REQUIRED CONFIG)需要 CMake 能找到 LLVM 的配置。如果你是在前面的 build 目录里构建自己的 pass,需要这样指定:
cmake -G Ninja -DLLVM_DIR=$HOME/llvm-project/build/lib/cmake/llvm ..MyPass.cpp 内容如下(针对 LLVM 17/18 的 New PM 接口):
#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) { unsigned instrCount = 0; for (auto &BB : F) { instrCount += BB.size(); } errs() << "[MyPass] Function: " << F.getName() << ", basic blocks: " << F.size() << ", instructions: " << instrCount << "\n"; return PreservedAnalyses::all(); } }; } // namespace // 注册插件入口 extern "C" ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, "MyPass", "0.1", [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "hello-pass") { FPM.addPass(HelloPass()); return true; } return false; }); }}; }这段代码有几个关键点值得展开。
PassInfoMixin<HelloPass>是 New PM 里所有 Pass 的基类模板。run(Function &F, FunctionAnalysisManager &AM)是核心入口,返回PreservedAnalyses。这里返回PreservedAnalyses::all(),意思是我这个 Pass 不对 IR 做任何修改,所以所有分析结果都保持有效。如果我们真的修改了 IR,就不能这么写了,需要返回PreservedAnalyses::none(),或者精细声明到底哪些分析被破坏,比如PreservedAnalyses::none()是最省事但是牺牲了优化效率的写法。
llvmGetPassPluginInfo是插件模式的统一入口,它让我们的 Pass 可以被opt这个工具动态加载。registerPipelineParsingCallback表示注册一个可以通过-passes命令行参数触发的 Pass 名称。这里注册的名字是hello-pass,等下在命令行里就是-passes=hello-pass。
3.3 使用 opt 加载 Pass 并跑通验证
先编译:
mkdir build cd build cmake -G Ninja -DLLVM_DIR=~/llvm-project/build/lib/cmake/llvm .. ninja注意事项:这里的两个 build 不是同一个目录。一个是 llvm-project 自己的 build,一个是你的 pass 项目的 build。我见过有朋友搞混了,在 llvm-project 源码目录里直接建了 pass 文件去编译,一报错就懵了。严格区分“宿主 LLVM”和“插件工程”。
生成一个测试用 C 文件:
int add(int a, int b) { int c = a + b; return c + 10; } int main() { return add(1, 2); }先用 clang 生成 LLVM IR:
~/llvm-project/build/bin/clang -S -emit-llvm test.c -o test.ll然后运行我们的 Pass:
~/llvm-project/build/bin/opt -load-pass-plugin=./MyPass.so -passes=hello-pass test.ll如果一切正常,你会在终端看到类似这样的输出:
[MyPass] Function: add, basic blocks: 1, instructions: 4 [MyPass] Function: main, basic blocks: 1, instructions: 3到这里,一个完整的 “自定义编译优化插件” 就打通了。从 C 源码到 IR,再经过我们的自定义分析,全过程都在你的掌控之中。这就是 llvm-project 最迷人的地方。
3.4 从 HelloPass 扩展:做点有实际价值的事
如果只是打印信息,那确实没啥用。我再说一个更贴近实际需求的扩展思路:统计每个函数里的函数调用次数,也就是做一个简化版的调用关系统计工具。
把run改成这样:
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { unsigned callCount = 0; for (auto &BB : F) { for (auto &I : BB) { if (auto *CI = dyn_cast<CallInst>(&I)) { if (Function *Callee = CI->getCalledFunction()) { errs() << " call -> " << Callee->getName() << "\n"; } else { errs() << " call -> (indirect)\n"; } callCount++; } } } errs() << "[MyPass] Function: " << F.getName() << ", calls: " << callCount << "\n"; return PreservedAnalyses::all(); }这里的关键是dyn_cast<CallInst>(&I),在 LLVM 里,指令Instruction有很多子类,dyn_cast就相当于“安全地向下转型”,如果不是 CallInst 就返回空指针,不会像 C 风格强转那样产生未定义行为。这在使用 LLVM 开发时极其常见,几乎每个分析 Pass 都会用 dyn_cast 去判断指令类型。
更进一步,如果你想做插桩,比如在每条 call 指令前插入一个计数的函数调用,就会用到IRBuilder,它类似一个“IR 版本的代码拼接器”。这一层玩熟了,你就能做性能剖析、安全检测、代码插桩等很多有意思的事情。
4. 常见问题与排查技巧实录
4.1 用 llvm-project 开发时的高频报错
这里总结硬核问题排查清单。都是我或者其他人在实际开发中经常遇到的,可以直接对照排查。
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
opt 加载插件失败提示invalid plugin | 插件编译时的 LLVM 版本和 opt 版本不一致 | 确认你的LLVM_DIR指向的宿主 LLVM 和 opt 是同一套构建产物 |
报错undefined symbol: llvmGetPassPluginInfo | 插件工程没启用以-fPIC编译 | 在 CMake 里加set(CMAKE_POSITION_INDEPENDENT_CODE ON),或者用add_library(... MODULE ...)时默认会带 |
clang 生成的 IR 在opt -passes=hello-pass时什么都输出 | Pass 运行期间遇到空模块或优化器把未用函数删掉了 | 编译 C 文件时加上-O0,并确保函数没有被 dead code elimination |
Assertion failed: (isa<X>(Val) && "cast<Ty>() argument of incompatible type!") | 用了cast而不是dyn_cast | 如果你不确定类型,用dyn_cast;确定类型再考虑cast |
编译自己的 Pass 时fatal error: 'llvm/Passes/PassPlugin.h' file not found | LLVM_INCLUDE_DIRS未正确配置 | 检查${LLVM_INCLUDE_DIRS}变量是否为空,通常是因为find_package(LLVM REQUIRED CONFIG)没找到配置 |
函数名出现一圈数字后缀,比如add.1 | 可能是优化过程已经发生函数名冲突,或者 multiple definition | 查看 IR 文件,确认是否确实只有一个 add 函数;优化器修改后出现数字后缀通常正常 |
4.2 构建太慢、内存爆炸的优化方案
如果你只是写 Pass,不需要每次都全量构建 LLVM。有两个技巧很实用:
- 编译 LLVM 时使用 ccache。这是编译器的缓存工具,第一次全量编译之后,后面清理重编的速度会大幅提升。配置方法是在 cmake 时加
-DLLVM_CCACHE_BUILD=ON,或者设置环境变量CCACHE_MAXSIZE=50G。 - 使用 clang-cl / lld 加速自身构建。LLVM 项目有一个官方脚本叫
clang-cl用的构建配置,在 Windows 上尤其有效。Linux 上用lld链接也能明显缩短链接耗时。
我在自己的机器上实测,开 ccache + lld 之后,LLVM 全量重编时间从四十多分钟降到了十几分钟。这两个优化非常值得。
4.3 调试 Pass 的另类技巧:直接用 IR 小文件验证
写 Pass 时最怕的是“编译通过但结果不符合预期”。排查手段很重要。我的经验是:
- 尽量用最精简的 IR 做实验。不要上来就跑 Clang 编大型项目,直接用 C 文件生成一小段 IR,手工删掉无关函数,甚至直接在 .ll 文件里手写几行 IR 来验证逻辑。
- 调试新 PM Pass 时善用日志机制。在 LLVM 里你不用随便
printf,可以用LLVM_DEBUG(dbgs() << "..."),在跑 opt 时加-debug-only=hello-pass才能看到。注意这里的关键字是你在源码里用DEBUG_TYPE宏定义的,不是一个随机字符串。 - 断点调试用 lldb 或 gdb 都行。因为 Pass 是动态库,
opt在加载后会执行你的代码,所以在run函数里下断点是有效的。我自己习惯在opt上加断点,直接在 run 函数开头打断点,比打印日志效率更高。
4.4 版本兼容性:LLVM 每一代都在变
llvm-project 有一个让很多初学的人崩溃的地方:API 变动非常频繁。18 版本的 Pass 写法,拿到 14 可能编译不过;不同版本的llvm::unique_function签名、CallInst的方法名甚至头文件路径都可能不同。我的建议是:
- 标明你使用的 LLVM 版本,如果网上教程没有写版本,先按最新 release 分支来验证。
- 写代码时尽量少用被标记为 deprecated 的接口。编译时留意 warning,能避免很大一部分未来的迁移成本。
- 如果版本差异太大导致编译错误,优先查看这个版本的官方
llvm/examples目录和 release notes。
5. 项目目录与源码阅读指南:从哪几个目录开始看
如果你不只是想用 LLVM,还想读源码、理解它的实现,盲目浏览整个 llvm-project 会非常痛苦。我建议按下面顺序来看:
- llvm/include/llvm/IR:这里定义了你最常接触的类型,比如 Instruction、BasicBlock、Function、Module、Type。读懂这些头文件,你就知道 IR 长什么样、可以怎么遍历。
- llvm/include/llvm/Passes:新 Pass Manager 的核心接口。读懂 PassBuilder 的注册方式,你就能理解一个 Pass 是怎么被框架调度的。
- llvm/lib/Transforms/InstCombine:这是一个极其经典的优化 Pass,用实际代码示范了怎么遍历指令、怎么重写指令。它的代码量适中,注释也比较清楚。
- llvm/lib/CodeGen:如果你想深入后端,这里是你最终要啃的地方。但建议先把前三个目录搞熟之后再看。
还有一个比较省力的阅读技巧:用 clangd 或 ccls 这类 Language Server 配置好索引,跳转非常流畅。直接在源码里搜索你已经在 Pass 里用过的类名,顺藤摸瓜就能把整个调用链串起来。
6. 实战扩展:把 llvm-project 应用到你的实际项目
6.1 基于 Clang 的静态分析工具
很多团队会引入 clang-tidy 来规范代码风格,但如果你有更特殊的规范,就需要写 clang-tidy check。这本质上也是写一个 Pass,只不过入口和接口与优化 Pass 不同。你可以基于 libTooling 库写一个独立的可执行程序,解析源码 AST,按你团队自己的规则检查代码模式。
举个例子,我曾经给一个嵌入式团队写过一条规则:禁止使用malloc,必须用自定义的内存池分配函数。用 clang 的 AST 匹配器,几十行代码就搞定了。比起靠代码评审人肉眼检查,这种工具靠谱得多。
6.2 交叉编译工具链和定制后端
如果你的目标平台是 RISC-V、MIPS 或者某种自研芯片,LLVM 提供了一个清晰的路径:写一个 Target 后端。这确实是编译器领域最难的方向之一,但 llvm-project 已经把指令选择(SelectionDAG 或 GlobalISel)、寄存器分配、指令调度、汇编输出这些都给你搭好了框架。你要做的主要是描述指令集和寄存器,TableGen 文件会帮你生成大量重复代码。
6.3 用 MLIR 做 AI 编译器的上层 IR
如果你关注 AI 编译器,MLIR 值得单独开一篇。它的一大优势是可以在高层级做算子融合、布局转换等优化,再逐层 lower 到 LLVM IR。很多 AI 芯片公司就是基于 MLIR 自研上层编译器的。建议先玩熟 LLVM IR 的 Pass 机制,再上手 MLIR 会顺很多,因为它俩共享大量基础设施。
7. 个人体会与最后的提醒
回头再看 llvm-project 这个项目,我最大的感触是:它学习曲线确实陡峭,但一旦跨过第一道坎,获得的是对编译器的“掌控感”——不再只是黑盒调用 gcc/clang,而是能亲手往编译流程里塞自己的逻辑。这种能力在很多领域都能直接产生价值。
踩过这么多次坑之后,我最后分享几个小习惯:第一,永远在单独的 build 目录构建,不要污染源码;第二,每次动手前先确认 LLVM 版本,版本不同很多代码写法完全不同;第三,实验性质的 Pass 一定要用最简单的 IR 测试,等逻辑稳定了再上真实项目;第四,遇到 API 变动时,直接去看你手头这个版本的头文件,比任何过时教程都可靠。
说实话,llvm-project 里还有太多内容我也没有完全吃透,比如向量化、Interprocedural 优化、sanitizer 的内部实现,这些都是值得花大量时间去啃的方向。希望你读完这篇文章之后,能直接 clone 一份源码、配好环境、写出自己的第一个 Pass。编译器领域的一个新世界就这么打开了。