做编译器开发这些年,有一个仓库我一直放在工作目录的固定位置,隔三差五就要翻一翻,它就是llvm-project。这个仓库在编译器圈子里基本属于“基础设施中的基础设施”,Clang、LLVM、MLIR、Flang、libc++、compiler-rt 这些耳熟能详的东西全都在里面。如果你搞过 C/C++ 工具链、写过静态分析工具、做过代码优化,或者哪怕只是好奇“编译器到底是怎么把源代码变成机器码的”,这个项目都值得花时间好好啃一啃。
我最早接触 llvm-project 的时候,完全是被 Clang 的报错信息吸引过去的。那时候还在写 C++ 业务代码,被 GCC 的某些报错折磨得够呛,换了 Clang 之后感觉整个世界清爽了不少。后来慢慢深入,才发现 llvm-project 的价值远不止一个“更好用的编译器”这么简单。它是一个完整的编译器基础设施,意味着你可以基于它构建出自己的语言前端、自己的优化 pass、自己的后端目标,甚至自己的代码分析工具。这篇文章我不打算讲那些晦涩的论文式理论,而是从一个实际用过、踩过坑的开发者角度,聊聊怎么读懂 llvm-project 的代码结构、怎么从零把它构建出来、以及怎么在这个庞大的代码库里找到自己的第一个实验入口。
无论你是刚接触编译原理的学生、做静态分析或性能优化的工程师,还是想给某个小众硬件写编译后端的极客,这篇文章的思路应该都能帮你少走不少弯路。直接说结论:llvm-project 不是那种“看一遍文档就会了”的项目,但也不是只有大佬才能碰的领域。只要掌握了正确的打开方式,你完全可以在几天之内让一个自定义的 pass 跑在自己写的 IR 上。
1. 先搞懂LLVM到底解决什么问题
很多人第一次接触 llvm-project 时,最容易犯的错误就是把它当成“一个编译器”来理解。其实准确地说,LLVM 是一个编译器基础设施,它不是一个完整的编译器,而是构建编译器所需的一整套库和工具。Clang 只是建立在它之上的一个前端实现而已。理解这个区别,是走进 llvm-project 的第一步。
1.1 从一句“要不要学编译原理”说起
我经常在技术社群里看到有人问:“写业务代码到底要不要学编译原理?”我的回答一向是:如果你只是调用现成工具,那确实不一定要;但如果你想搞懂工具链背后的设计思想,那编译原理这套知识早晚要补上。llvm-project 恰好是连接“理论”和“工程”最好的桥梁。
传统的编译器教科书会告诉你,一个编译器通常分为前端(Frontend)、优化器(Optimizer)和后端(Backend)三个阶段。前端负责词法分析、语法分析和语义分析,把源代码转换成中间表示;优化器在中间表示上做各种等价变换,让程序跑得更快或者更省空间;后端再把优化后的中间表示翻译成目标机器的汇编或机器码。
这个模型说起来简单,但落地的时候有个很现实的问题:假设你有 5 种编程语言,10 个目标硬件平台,按照“每种语言配每个平台”的方式写编译器的模块,你需要写 5×10 = 50 套前端到后端的组合。这显然是不可维护的。所以 LLVM 的核心思想特别朴素:把“源代码到中间表示”和“中间表示到机器码”这两件事彻底解耦。编译器前端把各种语言统一降到同一种中间表示(LLVM IR),后端只需要吃这一种 IR,就可以生成所有支持的硬件平台代码。这样一来,新增一种语言时,你只需要写一个前端;新增一个硬件平台时,你只需要写一个后端。组合数从 50 直接降到了 5+10。
1.2 LLVM 的核心答卷:三段式架构
llvm-project 对编译器的三段式架构给出了一个非常工程化的答案。最底层是LLVM 核心库,它包含了 IR 的定义、优化 pass 的实现、目标描述、代码生成等模块。这个核心库本身不依赖任何特定的编程语言前端,它只认识 IR。再往上,是各种前端,最著名的就是Clang,它把 C/C++/Objective-C 代码编译成 LLVM IR。除了 Clang,还有专门给 Rust 用的前端(Rustc 早期借用 LLVM,现在也基本是自研后端了)、给 Swift 用的前端、给 Fortran 用的 Flang,以及给各种领域特定语言定制的前端。最顶上,是各种围绕 LLVM 建立的工具链,比如调试器 LLDB、符号化工具、性能分析工具等等。
这个层次划分带来的直接好处是“一处优化,处处受益”。比如你在 LLVM 中后端里优化了一个指令选择算法,那么所有使用 LLVM 的语言都能享受到这个优化;你在 IR 层面新增了一个优化 pass,C++、Rust、Swift 的代码全都可以自动受益。你不需要分别给每种语言的前端写一遍优化逻辑,这是 llvm-project 最迷人的地方。
1.3 这套设计带来的连锁好处
除了“复用”这个显而易见的优点,LLVM 这种架构还带来了一个对开发者来说非常重要的特性——可测试性。由于前端和后端完全解耦,你可以纯粹为了测试某一个优化 pass,手工写一段 LLVM IR,丢给优化器opt去跑,而完全不需要经过词法、语法分析。这让优化器的调试变得非常干净利落。在实际开发中,我经常用*.ll文件做最小复现,那个体验比处理冗长的 C++ 源码要舒服得多。
另外,llvm-project 也为“跨语言互操作”提供了新的思路。既然大家最后都降到同一种 IR,那么不同语言之间的边界就会在 IR 这一层被打通。比如你可以在 C++ 中调用 Rust 生成的库,只要两边都能对接到底层的 ABI 约定,那在 LLVM IR 上它们就是一回事。这虽然不是 LLVM 的直接功能,但确实是它的架构带来的连锁效应。
2. 走进llvm-project仓库:代码结构初印象
我第一次把 llvm-project 克隆到本地时,面对那个庞大的目录树,说实话有点懵。后来花了不少时间才摸清每一个顶层目录到底是干什么的。这里我把最关键的几个目录整理出来,第一次接触的朋友可以先按这个脉络走。
2.1 顶层目录到底有多少“存货”
llvm-project 目前的顶层目录数量不少,而且还在持续增加。每个目录本质上都是一个相对独立的社区项目,只不过它们共享同一个版本发布周期和构建体系。我最常用到的几个目录包括:
llvm/:核心仓库,LLVM 的主体代码在这里。IR 定义、pass 框架、目标描述、代码生成、各种工具(opt、llc、llvm-as、llvm-dis 等)都在这里。clang/:C/C++/Objective-C 前端,以及基于 clang 的各种工具如 clang-tidy、clang-format。mlir/:MLIR,一个可扩展的中间表示框架,业界用来构建各种领域特定编译器,AI 编译器领域尤其活跃。compiler-rt/:编译器运行时库,包括 sanitizer(ASan、UBSan、TSan 等)、profile 运行时等。libcxx/、libcxxabi/、libunwind/:C++ 标准库实现和底层运行时支持。lld/:LLVM 官方的链接器。lldb/:LLVM 的调试器,和 LLVM 深度集成。flang/:Fortran 前端。polly/:基于多面体模型的循环优化框架。clang-tools-extra/:额外的 clang 工具,包括 clang-tidy、clangd 等。
2.2 不同子项目之间的分工关系
理解这些目录的分工,可以从一个经典问题入手:“一个 C++ 程序从源代码到可执行文件,中间经历了什么?”clang负责把.cpp变成.o中间目标文件,这里面内部就有前端解析、IR 生成、优化、代码生成几个步骤;lld负责把多个.o文件链接成可执行文件;libcxx提供你#include <vector>时背后那些标准库的实现。LLVM 核心库则像发动机,贯穿始终,为 clang 和 lld 提供底层能力。
这种“一个超级仓库、多个独立项目”的组织形式,对维护者来说是很高效的。你提交一个 commit 可以同时改动 clang 和 llvm 的代码,因为改动可能横跨前端到后端;对下游使用者来说,源码也只需要 clone 一次,再用统一的 CMake 系统配置构建,不需要分别到各个项目拉代码再手工组装。
2.3 日常开发中花费时间最多的几个地方
如果你去做 LLVM 上游开发,或者自己 fork 一份来魔改,比较高频的活动一般集中在几个区域:
llvm/include/llvm/IR/:IR 的数据结构定义。很多 pass 开发都需要看这里,比如Instruction.h、Function.h;llvm/lib/Passes/和llvm/lib/Transforms/:优化 pass 的实现与注册入口;llvm/lib/Target/:各硬件后端。如果你想支持新指令集,大部分工作在这里完成;clang/lib/Sema/和clang/lib/AST/:语义分析、AST 相关逻辑,做静态分析的人会频繁遇到。
说实话,llvm-project 的代码量相当惊人,完整读一遍是不现实的。聪明的方式是按需去读——你要写一个 pass,就读 Transforms 下已有的相似 pass;你要修 clang 的报错,就去 clang 的 lib/Sema 里搜索报错信息对应的字符串。这种“带着任务读代码”的方式,比从头到尾硬啃效率高得多。
3. LLVM的核心设计理念,读懂了才能改得动
llvm-project 的代码量大归大,但设计哲学却是高度一致的。只要抓住几个“腰眼”——IR、Pass 框架、TableGen——整个项目就不再是一团乱麻,而是一套有内在逻辑的积木。
3.1 LLVM IR:编译器世界里的“普通话”
LLVM IR 可以说是整个 llvm-project 的灵魂。它是一种静态单赋值(SSA)形式的中间表示,特点是显式地表示数据流、类型系统和控制流图。你可以把它理解成一种“比汇编更抽象、比 C 更底层”的中间语言。
举个例子,下面是一段简单的 C 代码:
int add(int a, int b) { return a + b; }对应的 LLVM IR 大概是这样的:
define i32 @add(i32 %a, i32 %b) { entry: %sum = add i32 %a, %b ret i32 %sum }这里i32表示 32 位整数,add i32 %a, %b是运算指令,%sum是新定义的一个虚拟寄存器。注意这里没有store指令去写内存,SSA 形式要求每个变量只能赋值一次,所以整个控制流图上的数据流关系非常清晰,后续的优化 pass 比较容易做数据流分析。
LLVM IR 有三种存在形式:内存中的表示(C++ 对象)、字节码(.bc文件,二进制)、文本形式(.ll文件,人类可读)。开发调试时文本形式最常用,你可以用llvm-dis把.bc反汇编成.ll,也可以用opt -S把优化后的结果打印成文本。这个设计对排查问题简直是救命的,因为你可以直观地看到每一轮优化到底对 IR 做了什么改动。
3.2 Pass 框架:优化与分析的流水线
如果 IR 是 LLVM 的“数据模型”,那 Pass 就是“处理逻辑”。一个 pass 就好比一条流水线上的一个工位,输入一份 IR,按特定规则改动一下,再把嘫给下一个工位。
LLVM 的 pass 框架历史上经历过几次演进。老一代的是继承FunctionPass、ModulePass这种类,每个 pass 重写runOnFunction或runOnModule。现在新代码更推荐使用新 Pass 管理器(New Pass Manager),它基于 PassInfo 和 PassBuilder 来组织 pass,既支持函数级分析也支持模块级分析,并且可以显式设定 pass 的依赖关系。
写一个最简单的 function pass,大概是这个样子:
#include "llvm/IR/Function.h" #include "llvm/IR/LegacyPassManager.h" #include "llvm/Pass.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Support/raw_ostream.h" using namespace llvm; namespace { struct MyPass : public PassInfoMixin<MyPass> { PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { errs() << "Processing function: " << F.getName() << "\n"; return PreservedAnalyses::all(); } }; } // namespace extern "C" ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, "MyPass", LLVM_VERSION_STRING, [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "my-pass") { FPM.addPass(MyPass()); return true; } return false; }); }}; }这段代码里你不需要关注每个细节,只需要体会这个框架的核心特征:pass 的编写者是“面向 IR 编程”的,你拿到手的是Function、Module、BasicBlock这些对象,改它们就是优化本身。而且 pass 可以以插件形式动态加载,这就是为什么你可以编译一个.so文件,然后让opt直接加载它,而不用改 LLVM 主代码。
3.3 TableGen:用声明式语言批量生成C++代码
另一个让初学者觉得“难懂但极其实用”的东西是 TableGen。它不是给最终用户用的语言,而是写给编译器开发者用的“元编程”工具。LLVM 的目标描述文件(.td)就是用 TableGen 语法写的,比如X86.td、AArch64.td里描述了指令格式、寄存器、指令选择模式等。
TableGen 的核心思想是:与其手写一大堆重复 C++ 代码,不如用简洁的声明式语法描述“有什么”,然后由llvm-tblgen自动生成对应的 C++ 表或类。举个例子,你要为一个新后端定义一系列寄存器,直接在.td文件里声明一下寄存器的名字、别名、编码,TableGen 就能生成枚举类型、寄存器类、查找表等一堆代码。这样既减少了手写错误的概率,也让目标描述高度集中,容易维护。
提示:第一次接触 TableGen 时,先不要急着翻它的完整语法文档。找一个现有的
.td文件,比如llvm/lib/Target/RISCV/RISCVInstrInfo.td,对照里面已有的指令定义,一边改一边看生成结果,学习效率会高很多。
4. 从源码构建 llvm-project:一场不算温柔但值得的实操
你可以在自己的机器上跑一遍从源码构建 llvm-project 的完整流程。这个流程对磁盘空间、内存、CPU 都有要求,但实际操作下来并不复杂,关键是选对配置。这里我会给出我常用的构建方案,以及多次踩坑后总结出来的避坑经验。
4.1 环境准备和版本选择
先说说硬件。LLVM 的构建是一个非常吃资源的活,如果你只构建核心的llvm和clang,并且开启 Release 模式构建,那么至少需要 20GB 左右的磁盘空间、8GB 内存。如果ninja并行度开得太高,内存低于 16GB 的机器很容易 OOM。建议至少使用 8 核以上的处理器,否则构建时间会非常感人。
然后是版本问题。llvm-project 的 main 分支开发非常活跃,API 变化也比较频繁。如果不是要参与上游开发,我还是推荐直接拉取最新的稳定发布版 tag,比如llvmorg-17.0.6或更高的 18 / 19 / 20 系列。稳定的 tag 意味着第三方生态(比如你可能会用的LLVM pass接口)相对稳定,网上搜到的资料也更匹配。
4.2 CMake 配置和 Ninja 构建的具体流程
我会创建一个独立的构建目录(build 目录),跟源码目录分开,这是 CMake 项目的基本素养。假设你的源码在/path/to/llvm-project,执行下面这组命令:
cd /path/to/llvm-project mkdir build && cd build cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS="clang;lld;clang-tools-extra" \ -DLLVM_TARGETS_TO_BUILD="X86;AArch64" \ -DLLVM_ENABLE_ASSERTIONS=ON \ ../llvm解释一下几个关键配置项:
-DCMAKE_BUILD_TYPE=Release:影响优化等级和调试信息。做开发建议用Release加LLVM_ENABLE_ASSERTIONS=ON的组合,既有性能又有轻量断言检查;做深度调试可以切到Debug,但构建时间会长很多;-DLLVM_ENABLE_PROJECTS="clang;lld;clang-tools-extra":指定额外要构建的项目。clang一般必选,其他按需加;-DLLVM_TARGETS_TO_BUILD="X86;AArch64":这是我最想强调的一个参数。默认情况下 LLVM 会为它能支持的所有目标平台构建后端,这个范围非常大。如果你只是做 x86 上的实验或者开发,只保留两个目标平台就够了,构建时间能缩短一半以上;-DLLVM_ENABLE_ASSERTIONS=ON:开着断言能帮你尽早暴露很多未定义行为,尤其是自己写 pass 的时候,强烈建议打开。
配置成功之后,开始构建:
ninja -j8这里的-j8表示用 8 个并行任务。如果机器内存有限,减少并行数;如果内存够,可以适当调高,比如-j16。第一次构建 clang 加 lld,运气好的话十几分钟到半小时能完成,具体看硬件。
4.3 构建后的工具集以及必知的小技巧
构建完成后,build/bin目录下会生成一堆可执行文件。最常碰到的有:
clang:C/C++ 前端;opt:IR 优化器,加载 pass 的实验场;llc:IR 到目标汇编的编译器,调试后端时的主角;llvm-as/llvm-dis:文本 IR 和二进制字节码之间的转换工具;llvm-nm/llvm-objdump:查看目标文件符号和反汇编;clang-tidy:静态分析工具,代码检查利器。
一个非常实用的验证命令是把一段 C 代码还是直接生成 IR,看看工具链是否正常工作。写一个简单的hello.c:
int main() { return 0; }执行:
build/bin/clang -S -emit-llvm hello.c -o hello.ll然后查看hello.ll里的内容,你会看到 LLVM IR 的文本输出。如果这一步顺利,说明你的 LLVM 工具链已经可以开始干活了。
5. 在 llvm-project 里落地第一个小实验
其实最快上手的路径不是从头看文档,而是“让一个东西跑起来,然后动手改”。这部分我就以“写一个自定义优化 pass”为例,讲讲从写代码到用opt加载的完整流程。
5.1 最快速的实验环境搭建
如果只是为了跑一个简单的 pass,你不需要编译成插件.so文件,也可以直接把 pass 的源码放进llvm/lib/Transforms/下面,然后在对应目录的CMakeLists.txt里加上新增的源文件,再重新编译整个项目。但这会触发全量或大范围重编,速度比较慢。
我更推荐的方式是把它编译成一个独立的 pass 插件。用上面第 3.2 节里的那一段代码,保存为MyPass.cpp,然后写一个简单的 CMakeLists 或直接用 clang++ 命令编译成动态库。如果项目里已经有 LLVM 的开发库路径,可以用这种快速方式:
$LLVM_BUILD/bin/clang++ \ -fPIC -shared \ -I $LLVM_SOURCE/llvm/include \ -I $LLVM_BUILD/include \ -I $LLVM_SOURCE/llvm/lib/Transforms/Utils \ MyPass.cpp \ -o MyPass.so注意头文件路径必须同时包含源码目录的llvm/include和构建目录生成的include(里面有一些由 CMake 生成的配置头文件)。路径搞错了,编译时会遇到一堆找不到头文件的报错。
不过这种东西也比较繁琐,更规范的做法是建一个独立小项目用find_package(LLVM)来找到 LLVM 库。LLVM 安装或构建后会生成LLVMConfig.cmake,你可以在项目的 CMakeLists.txt 里依赖它。
5.2 用 opt 加载自定义 pass:一个快速演示
假设你已经得到MyPass.so,用它来跑 pass 的命令是:
$LLVM_BUILD/bin/opt -pass-plugin=./MyPass.so -passes=my-pass -S input.ll -o output.ll这里-pass-plugin指定动态库路径,-passes=my-pass指定要运行的 pass 名字(也就是你在注册回调里写的字符串),-S表示输出文本 IR。运行之后,你会在屏幕上看到每个函数名被打印出来,这就是你的第一个 LLVM pass 已经生效了。
如果你发现-passes参数不识别你注册的 pass 名字,多半是插件注册回调里的字符串匹配没对上。注册的名字必须和-passes里传入的名字完全一致,注意大小写。
我一般会准备一个非常小的测试 IR 文件,比如:
define i32 @add(i32 %a, i32 %b) { %sum = add i32 %a, %b ret i32 %sum }这种最小文件跑起来特别快,也方便验证。等确认 pass 能加载、能打印函数名之后,再试着改一下 IR,比如遍历指令、统计加减乘除次数,甚至做一点简单的常量折叠实验。当你能感知“优化 pass 看到的数据结构长什么样”时,这个项目的大门就算正式打开了。
5.3 沿着“IR -> 优化 -> 汇编”走一遍全流程
写 pass 只是 LLVM 的上半场,下半场是观察 IR 如何变成汇编。把下面的命令串起来跑一遍,你会对整个工具链有个完整的认知:
# 1. 生成 IR clang -S -emit-llvm hello.c -o hello.ll # 2. 跑一个标准优化流水线并输出优化后 IR opt -passes=mem2reg,instcombine -S hello.ll -o hello.opt.ll # 3. 把 IR 转成目标汇编 llc hello.opt.ll -o hello.s # 4. 用 clang 把汇编汇编链接成可执行文件 clang hello.s -o hello这里面opt是最值得玩味的工具。mem2reg会把显式的alloca/load/store模式转换成 SSA 虚拟寄存器,instcombine做各种指令级化简。你可以反复对比hello.ll和hello.opt.ll,理解优化的核心模式。理解 IR 的 SSA 形式之后,你再看编译器优化、静态分析论文,会发现很多概念都变得具体起来了。
6. 常见问题与排查技巧实录
这里专门开一节,因为 llvm-project 的构建和使用确实有不少坑。我把多次实操中遇到的最典型的几个问题列成表格,并给出对应的解决思路。
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
cmake 配置时报找不到LLVMConfig.cmake | 选定了错误的 LLVM 安装路径或构建目录 | 确认使用-DLLVM_DIR指向含LLVMConfig.cmake的目录,一般是 build 目录下的lib/cmake/llvm |
| ninja 构建中途因内存不足被杀 | 并行编译任务太多,内存耗尽 | 降低-j参数,例如-j4,或在 cmake 中开启LLVM_PARALLEL_LINK_JOBS限制 |
| opt 加载 pass 插件提示 symbol lookup error | pass 使用到的 LLVM 内部符号与当前运行的 opt 版本不匹配 | 确认编译插件时的头文件和源码版本,与构建目标用的源码版本完全一致;尽量使用同一个 LLVM build 产出的 clang++ 编译插件 |
| clang 输出的警告信息位置不准确 | 不一定是 clang 的 bug,可能是宏展开 | 使用-fmacro-backtrace-limit=0等参数调试,或结合专门的宏展开输出分析 |
| 修改了 llvm 某个头文件后增量构建非常慢 | LLVM 头文件依赖关系复杂 | 尽量少改动核心头文件,改用插件方式隔离自己的代码;需要改头文件时评估影响面,再考虑做局部测试 |
| 运行 IR 时出现非法指令或崩溃 | 在错误的编译目标上运行了针对其他目标的 IR | 确认opt和你生成的 IR 的 triple 一致,必要时用llc指定-march编译到对应目标 |
提醒:LLVM 的 API 在不同版本之间变化很快。尤其是 pass 框架相关的代码,可能今天你参考的某个
runOnFunction示例是合法的,过了一个版本就变成了 legacy pass。遇到编译报错时,优先检查官方文档和新版本里对应的示例代码。
在我的经验里,最坑的就是插件与主项目版本不一致。用系统自带的老版本 LLVM 库去编译插件,然后让新构建的opt去加载,结果就是各种奇怪的行为甚至段错误。这不是编译技术的难度,纯粹是环境管理的问题。所以我的习惯是:如果需要开发 pass,就在同一个 build 里编译出来的 clang++/opt 环境中开发和测试,不去混用多个版本的 LLVM。
另一个经常被忽视的问题是llvm-config。有些工具链脚本会依赖llvm-config --cxxflags和llvm-config --ldflags来编译小工具,但你构建出来的llvm-config位于build/bin下,使用前先确认它对应的库版本和你实际用的工具版本是否一致。很多“找不到符号”的问题,根源都在这里。
7. 关于学习路径和踩坑经历的一些个人经验
最后说一点自己这几年的心得吧。我在读 llvm-project 之前,对编译器相关知识的理解是很零散的。真正打开局面,还是找到一个可以“制造问题”的小入口,比如给 clang 加一个自定义 warning、给 opt 写一个会遍历 CFG 的 pass,这种有明确目标的小实验,比单纯读代码有效太多。
还有一个建议是尽管去用-debug系列参数。LLVM 的 pass 管理器在 debug 构建下支持很多日志输出,比如-debug-only=loop-vectorize可以只看某个特定 pass 的调试日志。当你写的 pass 行为不符合预期的时候,打开这些日志,顺着每条消息找到对应的源代码位置,很快就能定位问题。这种微观层面的调试能力,真的得靠实际操作才能练出来。
llvm-project 就是这么个东西,代码量庞大、组件众多、API 迭代快,初次上手难免有一种“从哪看起”的无力感。但它的设计思想又是高度内聚的——只要抓住 IR 这条主线,顺着 pass 框架往后端和各前端延伸,整个项目就会逐渐清晰起来。希望这篇文章能帮你少踩几个我已经踩过的坑,也让你在点开opt、clang、llc这些命令的时候,心里更有底。