很多人第一次打开 llvm-project 这个仓库时,会有点懵:说好的是研究编译器,怎么刷出来一个比大多数操作系统源码还庞大的怪物?llvm-project 是 LLVM 基金会维护的 mono-repo 仓库,里面不只有编译器,还有调试器、链接器、标准库、基础库、用于写编译器的框架,甚至还有一套面向机器学习的编译器基础设施。这篇文章不打算讲某个具体 patch 的实现,而是从"拿到这个仓库之后,从哪下手、怎么理解、怎么构建、怎么用起来"这条线,把 llvm-project 的真实面貌拆开聊一遍,顺便把我在本地折腾这套东西时踩过的坑一并交代清楚。
如果你是学编译原理的学生、做性能优化的工程师、想自研语言的开发者,或者只是好奇"编译器本身是怎么造出来的",这篇内容可以当作一份索引式的上手地图。
1. 先搞清楚:llvm-project 到底是个什么项目
1.1 它不是"一个编译器",而是一条完整的工具链生产线
很多教程会顺手写一句"LLVM 是开源的编译器基础设施",但这句话等于没说。打开 llvm-project 之后你会看到十几个一级子目录,各自顶着不同的名字。这里随便列几个就能感受到这个项目的体量:
| 子项目 | 一句话定位 |
|---|---|
| llvm | 核心框架,包含 LLVM IR、优化器、CodeGen、目标后端 |
| clang | C/C++/Objective-C 前端,日常命令行用得最多的部分 |
| clang-tools-extra | clang-tidy、clangd、include-what-you-use 等附加工具 |
| lld | 高性能链接器,替代系统自带的 ld |
| libcxx / libcxxabi | C++ 标准库及 ABI 层 |
| compiler-rt | 运行时库,包含 sanitizer、profile、溢出检查等底层支持 |
| mlir | 面向机器学习与编译器分层的多层级 IR 框架 |
| polly | 基于多面体模型的循环优化 |
| flang | Fortran 前端 |
| openmp | OpenMP 运行时与编译支持 |
| lldb | 调试器,功能对标 gdb |
如果你只装了 clang 这个二进制,你拿到的是"编译驱动 + 前端 + 一堆库的集合"。但如果你拿到的是 llvm-project 源码,你其实拿到的是整条编译工具链的生产线:前端把高级语言翻译成 LLVM IR,优化器在 IR 上做变换,后端把 IR 变成目标平台的汇编和机器码,链接器负责把目标文件捆成可执行文件,调试器再基于这些信息提供源码级排错能力。这一整套东西,才配得上"project"这个词。
1.2 为什么它的架构会被反复抄作业
LLVM 的思路在 2000 年提出时其实很简单:把传统编译器的前后端彻底拆开。GCC 当时的前端后端是层层咬合的关系,要支持一门新语言或者一个新 CPU,代价都相当肉疼。LLVM 则定死了一套中间表示,前端的活是"把语言变成 IR",后端的活是"把 IR 变成机器码",中间这层大家共享。谁想支持新语言,就用 clang 以外的新前端来生产 IR;谁想支持新芯片,就写一个新的后端来消费 IR。
这套分层带来的收益今天已经看得很清楚了。Rust 的 rustc 一开始就直接借用 LLVM 做后端,省掉了从零开始写优化器和 CodeGen 的巨大工程量;Julia、Swift、甚至一些 GPU 的编译器也都在 LLVM 之上做文章。MLIR 出现之后,LLVM 甚至把自己的 IR 分层哲学又往下推了一层,专门服务于机器学习编译器这种需要多级抽象的场景。mlir 子目录能出现在这个仓库里,恰恰说明 LLVM 的项目边界已经不只是"编译器"三个字能概括的了。
1.3 新手最需要建立的三个认知
- 不要把 llvm-project 当成一个待读完的代码库,它没有"读完"这种状态。理解它的方式是把链路跑通,让前端、IR、优化、后端各环节在自己的控制下走一遍。
- 不要只盯着 llvm 目录里的 C++ 代码,clang、lld、compiler-rt 这些子项目才是你日常最常接触到的东西。
- 不要被构建时间吓退,合理的配置和工具链能让你在一台普通机器上完成全套构建,后面第 3 节详细说。
2. 仓库结构与核心模块:从顶层看懂项目版图
2.1 一级目录:各自独立又互相依赖
llvm-project 的顶层目录基本就是子项目的边界。每个子项目内部又统一采用类似的结构,学习成本因此低了很多。比如 llvm 目录下有include/、lib/、tools/、utils/、test/等一级子目录;clang、lld、mlir 也遵循同样的组织方式。
以 llvm 核心目录为例:
| 路径 | 内容 | 备注 |
|---|---|---|
| llvm/include/llvm | 公共头文件,声明所有核心接口 | 读代码前先在这层看接口定义 |
| llvm/lib/IR | LLVM IR 的核心数据类型定义,比如Module、Function、BasicBlock、Instruction | 理解了这几个类,你就理解了 IR 的骨架 |
| llvm/lib/Passes | Pass 注册与调度框架,NewPM 就在这 | 想动手写优化 pass,新版本看这个目录 |
| llvm/lib/CodeGen | 后端公共流程,包括指令选择、寄存器分配、指令调度 | 这是后端最难啃的部分 |
| llvm/lib/Target | 各个 CPU 后端的实现,X86、AArch64、RISCV 等 | 想移植新芯片就盯这里 |
| llvm/lib/Transforms | 经典标量优化、向量化、IPO 等具体 pass 实现 | 想学优化算法主要看这里 |
| llvm/tools | 对外命令行工具,比如opt、llc、lli、llvm-as | 日常调试 IR 靠它们 |
| llvm/test | 回归测试集,lit驱动的测试框架 | 提交 patch 时必须跑这一层 |
2.2 几个必须第一时间认识的命令行工具
构建完 llvm-project(或安装完整版 LLVM)之后,你会看到一长串llvm-开头的可执行文件。对上手理解这个项目来说,下面这几个工具优先级最高:
clang:C/C++ 前端入口,也是最常用的驱动。真正在终端里干活就靠它。clang++:C++ 前端入口,驱动 C++ 编译。opt:IR 级优化器,输入 bitcode 或 IR 文本,输出优化后的 IR。写 pass 跑测试绕不开它。llc:将 LLVM IR 转为目标平台汇编或目标文件。想看后端怎么输出机器码,用它。lli:直接用 JIT 方式运行 LLVM IR。想快速验证一段 IR 能跑出什么结果,用它。llvm-as/llvm-dis:在文本 IR 和二进制 bitcode 之间互转。LLVM IR 有两种等价形态,这哥俩负责翻译。llvm-nm、llvm-objdump、llvm-readelf:替代 binutils 系列的目标文件查看工具,调试链接相关问题很顺手。lld:链接器入口,调用-fuse-ld=lld即可让 clang 使用它。
我的建议是先不看源码,把这几个工具在终端里各摸一遍,用 clang 编译一段简单 C 程序,再用 clang 的-S -emit-llvm生成中间代码,用opt过一遍,最后用llc生成汇编。这条链路走通之后,你再看源码时就有一个"我知道你在这条链路的哪个环节"的大局观。
2.3 子项目之间的依赖关系与构建顺序
llvm-project 的构建系统是跨子项目统一的,使用 CMake 驱动。你不需要手动按顺序编译 clang、lld、libcxx、compiler-rt,因为总的 CMake 文件会把它们的依赖关系处理好。LLVM_PROJECTS_TO_BUILD这个参数决定你要额外编译哪些子项目。比如:
-DLLVM_PROJECTS_TO_BUILD="clang;clang-tools-extra;lld;libcxx;libcxxabi"注意,单独编译 llvm 和在同一个构建目录里带子项目一起编,这两者之间的二进制产物目录、安装布局都有差异。我建议第一次就按"llvm + clang + lld"来配,这三个是理解工具链主流程的最佳组合。libcxx 可以等后续需要改标准库时再单独加,mlir 和 flang 对新手来说一开始不建议动。
3. 从零构建 llvm-project:一份踩过坑的配置清单
3.1 构建前的必要准备
构建 LLVM 本身对操作系统的要求不复杂,但有几个条件会直接影响成败:
| 条件 | 建议值 | 原因 |
|---|---|---|
| 内存 | 16GB 起步,32GB 更稳 | 链接大型 C++ 目标文件时内存峰值很高,8GB 机器配 Debug 模式很容易 OOM |
| 磁盘 | 至少 50GB 可用空间 | Release 构建产物 + 中间文件 + 源码本身,轻松吃掉 30-50GB |
| 操作系统 | Linux/macOS 最顺手 | Windows 需要额外处理 MSVC 兼容问题,不是不行,是坑更多 |
| CMake | 3.20 以上 | LLVM 对 CMake 版本有要求,太老直接报错 |
| 编译器 | 自举,推荐用系统 gcc/clang 先编出一版 LLVM | 官方支持用宿主编译器构建,编出来的 clang 可以再编自己 |
| Ninja | 建议使用 | 比 Makefile 并行性好,增量构建快很多 |
开发者社区里有个默认建议:Release 模式 + Assertions 开启。这个组合既保证运行速度,又能在开发调试时帮你抓到内存越界等 bug。CMake 参数是-DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON。纯 Debug 模式编译出的 clang 慢得没法日常用,纯 Release 有时又过于"自信",把该报错的地方悄悄吞掉,所以 Release+Assertions 是最均衡的开发配置。
3.2 一个完整的构建命令组合
下面这组命令是我在 Ubuntu 22.04 上实测过多次的推荐组合:
git clone https://github.com/llvm/llvm-project.git cd llvm-project mkdir build && cd build cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_ASSERTIONS=ON \ -DLLVM_TARGETS_TO_BUILD="X86;AArch64;RISCV" \ -DLLVM_ENABLE_PROJECTS="clang;lld" \ -DLLVM_CCACHE_BUILD=ON \ ../llvm ninja -j$(nproc)参数逐一说一下:
-DLLVM_TARGETS_TO_BUILD="X86;AArch64;RISCV":默认情况下 LLVM 会为所有支持的后端生成代码,像 AMDGPU、BPF、PowerPC、SystemZ、MSP430、AVR 这些你根本用不到的 Target 也会编译出来,白白增加时间开销。按需裁剪之后,构建时间能缩短一半以上。只在本机跑的话,单独留X86就够了。-DLLVM_ENABLE_PROJECTS="clang;lld":这是从 LLVM 14 开始推荐的写法,之前是-DLLVM_ENABLE_PROJECTS="clang;lld"也没错,但现在更推荐这种带目录分割的写法。它告诉构建系统:除了 llvm 核心,还需要 clang 和 lld 这两个子项目。-DLLVM_CCACHE_BUILD=ON:加一层 ccache 缓存,增量编译成本大幅降低。我第一次没开,改一行代码后要重新链接一个几 GB 的 clang,中途真的会怀疑人生。
构建完成后,所有二进制都在build/bin下。验证方式很直接:
./bin/clang --version能正常输出版本号,说明 LLVM 与 Clang 链路已经通了。
3.3 常见踩坑与对应解法
- 内存不足导致链接崩溃:典型现象是
ninja跑到 lld 链接 clang 时进程被 OOM Killer 杀掉。解法有几个方向:一是换 Release + Assertions 而不是 Debug;二是减少并行任务数-j4甚至-j2;三是用lld做宿主链接器,比系统ld快且省内存。可以这样切换:-DLLVM_USE_LINKER=lld,前提是你已经有一个可用的 lld 或系统装了 lld。 - 磁盘被 test 目录塞满:默认构建会生成大量测试文件,如果只是先跑通链路,可以关掉测试构建:
-DLLVM_INCLUDE_TESTS=OFF-DLLVM_INCLUDE_BENCHMARKS=OFF。 - CMake 版本太老:Ubuntu 20.04 自带的 CMake 3.16 编不了新版 LLVM,建议用 pip 装一个更新的,或者用 Kitware 官方 APT 源。我经历过一次整整排查了半小时,最后发现就是版本问题。
- Windows 上 LLD 链接报错:如果你非要在 Visual Studio 工具链下构建,注意从 Windows SDK 里补齐
libcmt等运行时库的链接路径。这一步对新手不友好,我更建议先在有 Linux 环境的机器上跑通整套流程。
3.4 我的实际构建体感
在一台 16 核 32GB 内存的 Linux 机器上,全量 Release+Assertions 构建 llvm+clang+lld(仅 X86 目标),首次构建大约需要 25-40 分钟,取决于 CPU 主频和磁盘 IO。开 ccache 后,第二次构建只需要几分钟。这个时间成本在编译器开发里真的很值得——因为你会反复修改、反复编译、反复跑测试,增量构建速度才是决定每天心情的关键指标。
4. LLVM IR 与 Pass 机制:理解这层"万物皆可中间表示"的设计
4.1 LLVM IR 长什么样
LLVM IR 是理解整个项目的钥匙。它既可以被当成文本文件看,也可以被编码成二进制的 bitcode,在内存中则以 C++ 对象的形式存在。三者等价,这让编译器开发者可以非常方便地在各个阶段做调试。
把下面这段 C 代码交给 clang 生成 IR:
int add(int a, int b) { return a + b; }执行:
clang -S -emit-llvm add.c -o add.ll你会得到一段类似这样的文本 IR:
define i32 @add(i32 noundef %a, i32 noundef %b) { entry: %add = add nsw i32 %a, %b ret i32 %add }用生活化的方式类比:LLVM IR 就像一种"所有编译语言都能说、所有芯片都能听懂的世界语"。前端负责把 C、C++、Rust、Swift 翻译成世界语;优化器在世界语上做文章;后端再把世界语翻译成具体芯片的方言。因为大家都在同一层中间表示上工作,优化器不需要关心原始语言是啥,后端也不需要关心最终执行的高级语义,整个工程复杂度因此被拆散并降低。
4.2 Pass 是优化黑魔法的最小单位
对 IR 做变换的基本单元叫 Pass。一个 Pass 接收一个模块(通常是一整个源文件的 IR),经过分析或修改,输出一个新的模块。Pass 大致分两类:
- 分析 Pass:只读取 IR,计算某些信息。比如统计函数调用次数、分析变量之间的关系、计算循环深度。证书这类 Pass 不会改代码,输出的是分析结果,供后续 Pass 使用。
- 变换 Pass:修改 IR 本身。比如死代码消除、循环展开、内联等。这类 Pass 是真正干活的。
这种"小函数 + 小 Pass"的组合是 LLVM 优化器最鲜明的风格。在提交代码的 review 时,开发者通常只会把改动集中在一个很小的 Pass 里,这也是 LLVM 代码审查文化的一部分。
想手动体验 Pass 的作用,可以用opt:
opt -passes=instcombine add.ll -S -o add.opt.llinstcombine是一个经典组合变换 Pass,能把大量冗余模式化简。比如对add(a, 0)这种能优化掉的特例,它会直接把加法操作替换为a。这就是编译器优化的微观体现。跑完之后对比add.ll和add.opt.ll,非常治愈。
4.3 NewPM:老 Pass 管理器与新 Pass 管理器的故事
LLVM 历史上存在过两套 Pass 框架。旧框架由legacy::PassManager承载,Pass 以继承FunctionPass等类的方式注册,接口稳定但调度机制比较僵硬。新框架叫 NewPM,引入了 PassBuilder,提供了更清晰的流水线和调试输出选项。包括opt -passes=...在内的一套工作流都基于 NewPM。
可以这么理解:旧的 Pass 管理像把一堆工人扔进车间让他们自己协调,虽然也能出货,但流程不可控;新的 Pass 管理把每个工人安排进标准流水线,可以插队、可以调位置、可以单独测试,改造起来得心应手。写新 Pass 时,如果你用新版本 LLVM,社区推荐直接基于 NewPM 开发。在代码里注册 Pass 的入口大概长这样:
llvm::PassPluginLibraryInfo getPassPluginInfo() { const auto callback = [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "my-pass") { FPM.addPass(MyPass()); return true; } return false; }); }; return {LLVM_PLUGIN_API_VERSION, "MyPass", LLVM_VERSION_STRING, callback}; }用opt -load-pass-plugin=libMyPass.so -passes=my-pass就能把自定义 Pass 注入 pipeline。很多开发者第一次"在 LLVM 里留下自己代码"就是从写这么一个小 Pass 开始的。
4.4 从 IR 到汇编:llc 把最后一步走完
IR 优化完,后续工作是交给后端。llc这个工具会完成指令选择、寄存器分配、指令调度、汇编输出:
llc add.opt.ll -o add.s打开add.s看看,X86 上通常就是:
addl %esi, %edi movl %edi, %eax retq这一步可以把你的理解闭环:高级语言 -> 前端 -> IR -> 优化 -> 后端 -> 汇编 -> 链接。后端自身也是一大堆算法在支撑,尤其寄存器分配,堪称 LLVM 后端里最烧脑的部分。这里不展开,但你可以记住:后续想深入了解后端,第一站看llvm/lib/CodeGen/RegAllocFast.cpp,这个相对好读,也是一个 entry-level 的经典入口。
5. Clang 的前端到后端:一条日常高效工作流
5.1 clang 的常用姿势和 gcc 侧对比
很多人以为 clang 只是 gcc 的一个替代品,其实 clang 在设计目标上更强调模块化、可嵌入性和诊断质量。日常使用时,gcc 能做的 clang 基本都能做,而且诊断信息通常更有亲和力。下面几个命令非常常用:
# 编译并链接 clang main.c -o main # 只生成目标文件,不链接 clang -c main.c -o main.o # 生成汇编 clang -S main.c -o main.s # 生成 LLVM IR clang -S -emit-llvm main.c -o main.ll # 优化等级,O0/O1/O2/O3/Os/Oz clang -O2 main.c -o main一个常被新开发忽略的参数是-fcolor-diagnostics(新版默认开启)。它让报错信息里不同类型的内容用不同颜色区分,比如高亮错误位置的行和列。终端下面看代码定位速度快非常多。
另一个非常有用的参数是-fdiagnostics-show-option:
clang -fdiagnostics-show-option -c bad.c它会在诊断信息末尾显示建议关闭的 warning 名称,比如-Wunused-variable,你看到之后就能用-Wno-unused-variable随手关掉这个噪音。
5.2 用 clang 观察"语言到机器"的每一跳
学习编译原理的人最大的痛点就是过程不透明。clang 的-S -emit-llvm配合llc能把不透明打开。
拿一段稍微复杂点的 C 代码举例:
int sum(int n) { int s = 0; for (int i = 0; i < n; ++i) { s += i; } return s; }先生成 IR 看优化前后的差别:
clang -O0 -S -emit-llvm sum.c -o sum.O0.ll clang -O2 -S -emit-llvm sum.c -o sum.O2.llO0的 IR 里循环保留得非常直白;O2的 IR 在经过 indvars、loop-unroll、instcombine 等 pass 之后,很可能直接变成等差数列求和公式——循环整个被优化没了。这个过程放到现实中的大型代码里,就是编译器性能优化的缩略图。反复用-O0和-O2对比,你对优化 pass 的直觉会建立得很快。
5.3 交叉编译与目标选项
LLVM 的设计天然适合交叉编译,因为后端和目标描述是解耦的。要在 x86 机器上给 AArch64 交叉编译:
clang --target=aarch64-linux-gnu -march=armv8-a -c foo.c -o foo.o前提是你的构建时把 AArch64 后端包含进去(第 3 节里LLVM_TARGETS_TO_BUILD加了 AArch64 就是为了干这个)。配合lld,可以直接做全静态交叉链接:
clang --target=aarch64-linux-gnu -fuse-ld=lld foo.c -o foo.elf这种能力在嵌入式开发里非常香。传统方案要装一整套 arm-linux-gnueabihf 工具链,而 clang 一条命令就搞定了。
5.4 用 sanitizer 在早期抓到内存问题
clang 内置一组运行时 sanitizer,能在运行期帮你发现未定义行为。最常用的三个:
| Sanitizer | 检测对象 | 开启方式 |
|---|---|---|
| AddressSanitizer (ASan) | 堆溢出、栈溢出、use-after-free 等内存错误 | -fsanitize=address |
| UndefinedBehaviorSanitizer (UBSan) | 整数溢出、空指针、移位越界等未定义行为 | -fsanitize=undefined |
| ThreadSanitizer (TSan) | 数据竞争 | -fsanitize=thread |
日常开发里很多人只依赖 valgrind,但 valgrind 的慢在大型项目里难以接受。ASan 在编译期插入检查代码,运行时开销通常在 1.5-3 倍之间,已经非常接近生产可用了。实测效果:
clang -fsanitize=address -g test.c -o test_asan ./test_asan如果 test.c 里有越界访问,ASan 会打印完整的调用栈、分配栈和出错位置,比单纯段错误好排查一万倍。
6. 给想深入源码或参与贡献的人:从会用到会改
6.1 读源码的推荐路径
读 llvm-project 源码,最大的敌人是"不知道该从哪里进入"。我推荐一条相对平滑的顺序:
- 先读
llvm/docs/里的 GettingStarted 和 LangRef。LangRef 是 LLVM IR 的语言规范,虽然长,但可以随用随查,不需要一次读完。 - 读
llvm/include/llvm/IR/Module.h、Function.h、BasicBlock.h、Instruction.h这四个头文件的注释,把 IR 的对象模型先搭起来。它们之间是树状包含关系:Module 包含多个 Function,Function 包含多个 BasicBlock,BasicBlock 包含多条 Instruction。这条链几乎出现在所有分析和变换 pass 的入口函数里。 - 挑一个简单 pass 精读。比如
llvm/lib/Transforms/InstCombine/InstCombineAddSub.cpp,因为instcombine的代码短小精悍,注释丰富,很适合第一次了解"这个 pass 到底怎么改 IR"。 - 读 clang 的 AST 那部分,从
clang/lib/AST/Expr.cpp之类的小文件入手。clang 的前端旅程大致是 词法解析 -> 语法解析 -> AST -> 语义分析 -> 生成 IR。你不需要一次全搞懂,只需建立一个 map,知道哪类问题去哪个目录找。 - 最后如果对后端有兴趣,可以读
llvm/lib/Target/X86/X86ISelLowering.cpp,这里是 X86 后端里做指令选择和 lowering 的主战场,能回答"指令怎么被选出来"这个问题。
6.2 第一次提交 Patch 的完整动作
想给 llvm-project 提交代码,第一步其实不是写代码,而是跑通流程。现在的协作流程基于 GitHub,操作和大多数开源项目类似:
- fork llvm-project 到自己账号
- 创建分支,改代码
- 写测试,测试文件放在
llvm/test/或对应子项目的test/目录,使用 lit + FileCheck 写回归测试 - 本地跑
ninja check-llvm check-clang验证 - 提交 PR,等 review
如果想要初次 low-hanging fruit 的贡献方向,可以从这几个入手:
| 类型 | 例子 | 难点 |
|---|---|---|
| 修文档 | 官网和 docs 里过时的参数说明 | 几乎没有 |
| 补充诊断 | 给某个 pass 加上更友好的 warning 信息 | 需要了解对应 pass 的逻辑 |
| 补测试 | 给新发现的 bug 加回归测试 | 需要有 bug 场景和最小复现 |
| 修复静态分析 warning | 给代码里未处理的情况加显式处理 | 需要理解上下文 |
| 翻译 LangRef 的部分章节 | 社区一直有中文文档贡献的需求 | 需要准确理解术语 |
初次建议不要直接冲击巨型新功能。LLVM 的 code review 非常严格,你花一晚上写的一个大 pass,review 后大概率被拆成十八个小 patch 再逐个讨论。参与这个社区,耐心和沟通能力跟工程能力同等重要。
6.3 一个拿上台面的学习方法
针对那些"想彻底搞懂 LLVM"的人,我建议采用"三周链路法":
- 第一周:构建 llvm-project,用 clang/opt/llc 跑通 main -> IR -> optimized IR -> assembly 的链路,每天换着花样用命令行观察不同代码生成的不同结果。
- 第二周:读 LLVM Kaleidoscope 教程,就是用 C++ 从零实现一门简单语言并用 LLVM 翻译成机器码的经典教程。它几乎覆盖了前端生成 IR 所需的所有核心概念,从词法分析到 JIT 全都有。
- 第三周:挑一个自己熟悉的 C 项目,把其中几个关键文件用
clang -S -emit-llvm转成 IR,再用opt做不同优化,对比 IR 变化,选一个 pass 读源码直到能解释变化原因。
这三周之后,你再看 llvm-project,就不再是"天书"而是"一座可以按地图探索的城市"。剩下的事情,无非是决定先逛哪个街区。
7. 关于学习方向的一点个人提醒
聊到最后,分享一个我自己的体会:接触 llvm-project 初期最让人焦虑的地方在于"不知道学什么才算学会"。这个项目太大了,大到几乎没有一个人能通吃所有模块。真正有价值的做法是给自己定一个小目标——是想改进某个优化 pass,还是想给某块嵌入式芯片做后端支持,还是想深入 clang 的前端诊断。目标定了,源码阅读范围自然就窄了,构建配置、测试方法、社区资料也都跟着聚焦了。
我最初就是单纯想搞清楚 clang 的告警是怎么生成的,结果一路从诊断接口追到 SourceManager,又从 SourceManager 追到 Lexer,最后对整个前端词法分析流程建立起了概念。这个过程没有走太多弯路,也没有依赖什么现成"教程",核心就是不断回答自己脑子里冒出来的"它为什么知道这一行是错的"这个问题。
所以如果你现在正对着某个编译错误或者一段晦涩源码发愁,可以换个思路:这不是障碍,这本身就是学习的第一步。