
说到llvm-project我观察到一个挺普遍的现象很多人打开这个GitHub仓库看到几十个子目录、上千万行代码直接当场劝退。一部分人把LLVM等同于Clang另一部分人以为LLVM只是某个C编译器还有一部分人听说过中间表示这个概念但始终没搞明白它到底解决了什么问题。这篇文章我想换个角度来聊。不打算给你贴一长串官方文档式的目录说明而是从这个仓库为什么长这样出发把LLVM的核心设计逻辑、各个子项目的分工关系、以及一个新手想上手该从哪里下手这几个问题讲清楚。无论你是想做编译器开发、写后端工具链、还是想搞明白Rust和Swift为什么都选它当底层基座这篇文章都值得你耐心看完。llvm-project这个仓库之所以特殊不只是因为它大——虽然它确实很大——更因为它根本不是一个项目而是一整套编译器基础设施的集合。理解到这一层后面的学习路线就顺了。1. 名字里的历史包袱LLVM早已不是虚拟机先说一个绕不开的误区。LLVM这个缩写最早来自Low Level Virtual Machine直译过来就是低级虚拟机。2000年的时候Chris Lattner在伊利诺伊大学厄巴纳-香槟分校UIUC开始写这套东西当时的博士课题就是为任意编程语言构建一个基于静态单赋值SSA的、支持生命周期跨越整个程序运行阶段的编译系统。早期的LLVM确实带一点虚拟机的味道因为它设计了一套平台无关的字节码格式可以在运行前通过JIT实时编译成本地代码。这种做法在当时的学术圈并不新鲜Java虚拟机就是这么干的。但LLVM从一开始就有个关键差异它把中间表示设计成了静态的、可序列化的、能直接写入磁盘格式的形态而不是只活在内存里的运行时结构。这意味着你可以把IR当成一种程序交换格式到处传递编译、优化、链接、代码生成这些阶段可以彻底解耦。到了2003年前后LLVM的定位越来越清晰——它要做的不是一个语言运行时而是一整套用来造编译器的组件。2005年Chris Lattner被Apple招入麾下LLVM开始在Apple内部承担Objective-C和C的前端工作最终催生了Clang。从那之后LLVM这个名字反而成了历史包袱因为它既不Low Level也不再是个Virtual Machine。今天的你如果看LLVM官方文档它对自己的定义是一个用于构建编译器和其他编程语言工具的基础设施compiler infrastructure。这个定义非常精准LLVM不直接面向某种编程语言的最终编译器成品交付它提供的是编译器内部需要的各种零件和引擎——词法分析器、语法分析器框架、中间表示定义、优化Pass框架、目标指令选择与生成、调试信息处理、链接器、运行时库——你可以拿这些组件自由组装成自己需要的编译器、代码分析器、反编译器、静态检查工具、或者任何跟程序打交道的东西。理解了这一层你再回头看llvm-project这个仓库的目录清单就不会困惑了。2. 仓库目录地图一个大型项目中谁管前端、谁管优化、谁管后端llvm-project从2019年开始采用单仓库monorepo管理模式。以前各个子项目是独立仓库、独立发版维护者每发布一次就要同时打几十个tag同步提交、跨仓库联调极其痛苦。现在所有东西都放在一个仓库里统一构建、统一测试、统一发布周期对下游使用者的体验立刻改观了——你git clone下来一刀切构建整个工具链不用再操心版本匹配的问题。仓库顶层目录的每个条目基本对应一个完整的工具链组件。我用一张表帮你理清楚目录名全称/定位一句话说明llvm/LLVM核心库IR定义、Pass框架、代码生成后端、优化器核心clang/C语言家族编译器前端把C/C/Objective-C源码变成LLVM IRlld/LLVM链接器链接各个目标文件生成最终可执行文件lldb/LLVM调试器调试器实现支持与Clang共用表达式求值组件compiler-rt/编译器运行时库提供各种内置函数、sanitizer运行时、软浮点实现libcxx/C标准库实现LLVM官方的C标准库对标libstdclibcxxabi/C ABI实现异常处理、运行时类型信息RTTI等底层ABI支持libunwind/栈展开库异常处理和调试所需的栈回溯能力mlir/多级中间表示一套更灵活、可扩展的IR编译框架flang/Fortran编译器前端让LLVM真正支持Fortran语言openmp/OpenMP运行时库并行编程模型在LLVM中的实现polly/多面体优化面向循环嵌套的高级优化框架bolt/二进制优化和布局工具分析已编出的二进制文件再做后链路优化clang-tools-extra/Clang额外工具clang-tidy、clang-format、clangd等常用开发工具看到这张表你应该发现了这哪是一个编译器这分明是一条完整的编译器产业链。从源码到最终可执行文件每个环节LLVM都有自己的解决方案前端clang、flang负责把高级语言降级成IR中间如果有需要就交给优化器llvm polly处理然后经过代码生成器落成目标平台的机器码再通过lld链接出最终产品。调试阶段有lldb部署阶段有compiler-rt配合sanitizer做内存和并发检查性能调优阶段还有BOLT这种显式优化二进制布局的后工具链。初学的时候不需要每个目录都读一遍。我先建议你专注三个目录llvm/这是核心中的核心IR定义和Pass框架都在这里、clang/看它如何把C语言转成IR是理解前端到中端的最短路径、llvm/下面的test目录大量人门级的IR测试文件是理解IR语法的最佳样本。3. 为什么所有现代编译器都在学它IR三层设计、SSA与Pass管线LLVM的整个设计可以压缩成一句大白话把编译问题拆成一个个独立的Pass每个Pass吃进来一段IR经过变换再输出一段IR。要让这个过程高效且可组合IR本身的质量决定了整个系统的上限。LLVM的IR就是它的灵魂。3.1 IR的三种形态同一个东西三种长相LLVM IR有三种等价的表现形式内存中的IRIn-memory由C类的实例构成编译器真正操作的对象比如llvm::Function、llvm::BasicBlock、llvm::Instruction。Bitcode二进制序列化格式适合快速读写、存储在磁盘上、以及跨进程传输。Clang编译时加-emit-llvm得到的.bc文件就是这个。文本IRLLVM assembly人类可读的.ll格式调试和测试时最常用。三份内容可以随时互相转换这是LLVM一个很强的设计你手写一个文本IR文件丢给优化器和从Clang编译出的bitcode做同样优化优化器看到的本质上是同一条指令流。这给编译器的开发和测试带来极大便利——很多测试用例根本不需要准备C代码直接写几行IR就能精准验证某个优化Pass的行为。一段典型的文本IR长这样define i32 add(i32 %a, i32 %b) { entry: %sum add i32 %a, %b ret i32 %sum }函数名add参数%a和%b都是i32类型%sum是add指令的结果——注意LLVM IR里每条赋值产生的值都只能用一次这种每个变量只被写入一次的形式就是SSAStatic Single Assignment结构。SSA的好处等到写优化Pass的时候才会真正体会到某个值从哪里来一目了然做数据流分析时不需要再折腾这个变量可能被谁改过这种问题。3.2 Pass架构乐高积木式的优化管线接下来是Pass。优化器打开一个IR模块按顺序执行一系列Pass这些Pass分为三类分析PassAnalysis Pass不修改IR只收集信息比如每个循环的执行次数是多少、哪些变量是死代码。变换PassTransform Pass真正修改IR比如往IR里插入运行时检查指令、删除不用的代码、把某些循环向量化。工具类PassUtility Pass服务于基础设施比如给IR中的函数排序、添加调试信息。代码生成器里面还有一个抽象层级错落的Pass官能团指令选择Instruction Selection把三地址代码映射成目标机器的指令寄存器分配Register Allocation决定哪些虚拟寄存器淘汰到物理寄存器指令调度Instruction Scheduling重排指令让处理器流水线跑得更快。读到这你应该理解了为什么LLVM的架构如此受青睐。传统编译器比如早期GCC倾向于把所有这些逻辑揉成一团新增一个优化点要绕着整个编译器转一圈。LLVM把每个Pass都做成了松耦合的小组件你新增一个优化Pass注册进去就行前后的优化和代码生成不用担心被影响当然在工程实现上还是有不少边界情况比如Pass之间的分析结果依赖就需要小心翼翼地处理执行顺序。3.3 新旧Pass管理器的坑这里有一个很重要的实践知识点LLVM这两年正在从legacy Pass Manager迁移到New Pass Manager。旧的管理器把Pass按每个函数跑一遍、每个模块跑一遍这种粒度粗粗暴排序对依赖关系处理得很隐晦。2022年发布的LLVM 15开始新Pass管理器成为默认选项它显式管理Pass的分析结果缓存、支持更细粒度的调度还允许你在运行时自定义Pass管线。如果你顺着网上的旧教程去写Pass大概率会撞上这一堵墙——一堆函数从llvm::FunctionPass改成llvm::PassInfoMixinrunOnFunction变成run注册方式也从RegisterPass换成llvm::PassPluginLibraryInfo。写博文的人习惯把这个问题一笔带过但真正从零上手的人在这里平均会多花两到三天时间。顺带说个好消息新的Pass插件系统让按需加载自定义Pass变得非常丝滑。你在工程目录里编译出一个.so动态库然后运行opt -load-pass-plugin./libMyPass.so -passesmy-pass就能加载。这对于做研究和跑实验的人来说是巨大的效率提升不用再反复重新编译整个优化器主体。4. 从Git Clone到第一次运行你的自定义Pass这一节我按我实际踩过坑的顺序来写。如果你是完全第一次接触LLVM的编译流程跟着做一遍基本就能跑通整个工具链。如果之前已经成功构建过可以直接跳到自定义Pass那段。4.1 构建环境怎么准备llvm-project对编译器的版本有要求——这不是强制检查的但你用一个过老的GCC去编新版LLVM大概率会遇到各种莫名其妙的C标准兼容问题。我的建议是如果你是2025年之后读这篇文章直接用GCC 11或者Clang 14内存至少要保证16GB如果要同时跑编译和测试32GB会更轻松磁盘准备个60GB吧构建产物加测试文件加起来非常夸张。操作系统方面Ubuntu 22.04及以上、macOS 13及以上、Windows 11配合Visual Studio 2022都是可行的选择。我个人主力开发环境是LinuxWindows主要用来验证跨平台问题。4.2 仓库下载与目录选择git clone https://github.com/llvm/llvm-project.git cd llvm-project这一步会花比较长时间因为仓库体积巨大。如果你只是想做开发实验可以只拉最新的一个快照不拉全部历史git clone --depth 1 https://github.com/llvm/llvm-project.git接下来是最关键的目录选择。很多人图省事直接在llvm-project/build下面建构建目录结果发现生成的CMake缓存总是污染仓库目录后续切换分支的时候痛苦不堪。我推荐建一个独立目录mkdir build cd build cmake -G Ninja ../llvm \ -DLLVM_ENABLE_PROJECTSclang \ -DLLVM_TARGETS_TO_BUILDX86 \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_ASSERTIONSON几个参数逐个解释一下。LLVM_ENABLE_PROJECTS决定你要构建仓库里的哪些额外子项目——如果你只学习IR和Pass只给一个clang就够了如果用到lldb或者mlir想清楚再接入多填一个编译时间可能直接翻倍。LLVM_TARGETS_TO_BUILD让你只生成X86的后段这个参数能大幅减少编译时间值得重视。LLVM_ENABLE_ASSERTIONSON则是强烈建议开启的它会启用大量IR内部合法性校验调试Pass时能救你很多次。然后开始构建我这里先只编clang和optninja clang opt第一次构建的时间很长Release模式加上多核并行视机器配置不同大约要15分钟到1小时。忙别的去吧急不来。4.3 验证工具链和后端管线构建完成后先用一个最简单的例子验证全链路// hello_llvm.c #include stdio.h int main(void) { printf(hello llvm\n); return 0; }./bin/clang hello_llvm.c -o hello_llvm ./hello_llvm能打印hello llvm就算通了。接下来看看IR长什么样./bin/clang -S -emit-llvm hello_llvm.c -o hello_llvm.ll cat hello_llvm.ll你会发现printf的调用在IR里变成declare i32 printf(...)然后是call i32 (ptr, ...) printf(...)——这就是编译器前端把带可变参数的原型降级成外部函数引用的过程。读到这份IR你已经正式推开LLVM核心世界的大门了。4.4 编译并运行你的第一个Pass我来讲最经典的Hello World Pass但它不会打印文本而是输出它遇到的每个函数的名称。新版Pass的写法如下// HelloPass.cpp #include llvm/IR/Function.h #include llvm/IR/Module.h #include llvm/IR/LegacyPassManager.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h #include llvm/Support/raw_ostream.h using namespace llvm; namespace { class HelloPass : public PassInfoMixinHelloPass { public: PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { errs() func: F.getName() \n; return PreservedAnalyses::all(); } }; } // namespace然后写注册逻辑// RegisterPass.cpp #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h using namespace llvm; namespace { struct HelloPass : public PassInfoMixinHelloPass { PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { errs() func: F.getName() \n; return PreservedAnalyses::all(); } }; } // namespace llvm::PassPluginLibraryInfo getHelloPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, HelloPass, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name hello-pass) { FPM.addPass(HelloPass()); return true; } return false; }); }}; } extern C LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return getHelloPassPluginInfo(); }编译这个插件clang -shared -fPIC -fno-rtti HelloPass.cpp \ $(llvm-config --cxxflags --ldflags) \ -o libHelloPass.so然后用opt跑./bin/opt -load-pass-plugin./libHelloPass.so -passeshello-pass hello_llvm.ll输出里会出现一堆func: main之类的行。看到这个输出说明你的自定义优化Pass已经成功挂到LLVM的优化管线上去了。剩下的就是从能跑到能干正事的修行了。4.5 调试优化器的三个常用武器写Pass最容易遇到的问题是我明明觉得逻辑没问题为什么优化效果不出来或者IR被改坏了。我建议你先掌握这三个工具opt -print-after-all编译完每个Pass之后打印一次IR用opt -print-after-all -passesmy-pass跑完能够精确看到你的Pass对IR做了什么修改。-print-before-all配合上面这个看清Pass进入前IR长什么样两个输出diff一下问题瞬间暴露。-debug-debug-onlymy-pass在Pass里加LLVM_DEBUG(dbgs() debug message\n)语句然后opt -debug-onlymy-pass -passeshello-pass只有你标记的调试信息会打印出来不会淹没在满屏Log里。这里必须提醒你一个最隐蔽的坑如果目标函数被声明为external或者标记了optnone优化Pass默认是跳过的。我第一次写Pass时输出一直少几个函数排查半天才发现是optnone属性的缘故——这是属性的作用Pass框架认为这个函数不需要自定义优化自然不会进你的Pass跑。理解了这个机制你调试时就不会被为什么我的Pass对某些IR不生效折磨了。5. 最被低估的配套组件MLIR、compiler-rt和LLD扮演的真正角色讲到这里很多人对LLVM的认知可能还停留在编译C/C的工具链。但llvm-project仓库里那些容易被人忽略的成员才是它生态爆发式增长的真正底座。5.1 MLIR给AI编译器装了个方言引擎MLIRMulti-Level Intermediate Representation是LLVM社区近年来最引人注目的新模块之一它解决的问题是传统编译器的IR是为特定语言设计的单层表示AI芯片、GPU、TensorFlow/PyTorch这类异构计算场景无法用一个固定的IR层把高层数学运算一路降到低层指令。MLIR提出方言Dialect的概念——每一套IR都是独立方言比如tensor方言描述张量运算affine方言描述循环优化llvm方言对接传统LLVM IR。你可以在各种方言之间自由升降级逐层把模型优化的粒度放宽。现在主流的AI编译器几乎都有MLIR的影子TensorFlow的XLA、PyTorch的Torch-MLIR底层都在用这套框架。如果你关注AI编译器方向MLIR绝对值得花时间深入。它的入门曲线比传统LLVM Pass稍微陡峭一点但掌握了Dialect机制后你能构造的编译系统会超出大多数人的想象力。5.2 compiler-rt被sanitizer带火的运行时库compiler-rt是Clang配套的运行时支持库它干的活包括提供__int128乘法、软浮点实现这类编译器内建函数builtins同时也实现了AddressSanitizerASan、ThreadSanitizerTSan、MemorySanitizerMSan、UndefinedBehaviorSanitizerUBSan这些动态分析工具。ASan在业界几乎成了内存安全检测的标准手段你在开发C/C时加上-fsanitizeaddress编译选项跑起来之后绝大多数越界访问和释放后使用问题都会直接在崩溃现场报出来比开着GDB一步步去猜不知道高到哪里去了。5.3 LLD快得离谱的链接器传统Unix链接器比如GNU ld在处理大型C项目链接时慢得让人想骂人。LLD把链接过程大幅并行化用LLVM自己的IR和对象格式做优化。实测链接一个几十MB的Chromium这么大体量的可执行文件LLD比传统ld大约快3到5倍。现在很多大型软件项目都已经在构建流程里切换到lld了毕竟谁不想每天省出一杯咖啡的时间。5.4 Flang与Polly边缘但不可忽视的战斗力Flang给LLVM带来Fortran前端让老旧的科学计算代码库可以享受LLVM的优化能力HPC社区已经开始大量绑它。Polly则是用多面体模型做循环优化对科学计算中的循环嵌套可以做到极致的变换比如自动改造成适合向量化的内存访问模式。这两个项目虽然不像Clang那样抢眼但它们在专业领域里的地位不可替代。6. 聊点未来LLVM下一步的野心LLVM几乎每6个月发布一个大版本让我挑几个最值得关注的方向聊一聊。首先统一IRUnified IR的工作取得了实质性进展。传统LLVM IR和MLIR的LLVMDialect之间一直存在转换损耗社区正在推动让它们的结构更加一致最终目标是用一套IR同时服务传统优化和新式AI编译器减少维护成本。其次GPU后端在持续强化。NVIDIA的NVPTX、AMD的AMDGPU、Intel的SPIR-V各自的后端这几年迭代非常快。RISC-V的后端也在快速成熟许多国产CPU和GPU芯片厂商已经用LLVM做默认编译工具链。还有安全能力上的演进。Clang 18开始引入了一些强化栈可变大小的内建函数配合-fstack-protector体系做更精细的防护-fcf-protection控制流保护在x86架构上得到更完整的实现。这些不是LLVM首创的思路但把类似能力下沉到默认工具链这件事对整个软件生态的安防能力是有普适提升的。从开发者体验角度看新一代PassManager的API已经稳定clangd的代码补全和静态检查已经成了无数C/C开发者的日常伴侣。今天你安装的VS Code C扩展、CLion、甚至很多国产云IDE底层都在用LLVM系的语言服务协议实现。LLVM不再只是一个编译C语言的程序它已经成了软件工程基础设施中无处不在的底层引擎。回到开头那个问题llvm-project这个仓库它绝不是一堆难以接近的源码堆积而是一张清晰的分工地图、一套可插拔的设计思想以及一个正在改变整个编译与软件开发生态的发动机。从理解它的架构开始你不需要一下子读完所有源码只要握着IR这把钥匙顺着Clang的编译流程走下去你会慢慢发现这个庞然大物其实有理可循、有迹可依并且值得你花时间深耕。如果你正在纠结选型想入门C/C工具链盯住clang opt lld就够了对AI编译感兴趣直接切到mlir目录下下手专门搞嵌入式或者RISC-V那默认支持的后端让你几乎不需要额外配置。不管选哪条路llvm-project这套代码库都足够你遨游好几年。