尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

LLVM项目深度解析:从架构到源码构建与Pass开发实战

发布时间:2026/9/19 4:49:30

资讯中心
01
ARTICLE

LLVM项目深度解析:从架构到源码构建与Pass开发实战

LLVM项目深度解析:从架构到源码构建与Pass开发实战
一段时间里我每隔一阵就会在技术交流群里看到有人甩出一个链接llvm-project。有人问这东西跟编译器到底什么关系有人问为什么叫“项目”而不是“编译器”还有人说下载下来编译了半天没编过。这些反馈其实都很真实因为 llvm-project 这个仓库确实不像普通开源软件那样“拿下来就能用”它更像一座巨大的组件工厂门类齐全但也因此劝退了不少刚接触的人。这篇文章想解决的就是三个层面的问题第一llvm-project 到底是什么为什么它值得被当作一个独立的技术方向去理解第二它的核心组成和设计逻辑是怎样的各组件之间如何配合第三如果你真的想把它跑起来、甚至在里面写点东西实操路径是什么有哪些坑我替你踩过了。不管你是想入门编译器开发、研究代码优化还是打算给自己的编程语言做一个后端或者单纯好奇为什么这么多大厂都在往这个仓库里提交代码这篇文章都适合你。我会从整体架构讲到源码构建再讲到写第一个 Pass最后把我在实际使用中遇到的坑一并列出来。1. 它不是一个编译器而是一条完整的工具链流水线LLVM 这个名字最早是 Low Level Virtual Machine 的缩写但项目发展到今天已经跟“虚拟机”没太大关系了。你更应当把它理解成一套围绕中间表示IR展开的编译器基础设施。所谓基础设施意思是它不直接帮你完成“从源代码到可执行文件”的全部工作但你把源语言前端、优化器、目标后端三板斧搭起来就能拼装出一套完整的编译器工具链。这个设计理念跟传统编译器有很大区别。拿 GCC 来对比GCC 虽然也有一套内部表示但它的整体架构基本还是围绕具体语言和目标平台耦合设计的。LLVM 从第一天起就想着解耦前端负责把语言翻译成统一的 IR中端只针对 IR 做优化后端再把优化后的 IR 翻译成目标机器码。三层各管各的前端作者不需要关心 x86 和 ARM 的差异后端作者也不需要关心语言是 C 还是 Rust。这种设计的价值我打个比方传统编译器像一家餐厅从买菜、洗菜、炒菜到上桌都是同一个后厨团队换菜单等于换后厨而 LLVM 像中央厨房加连锁店的模式中央厨房只负责把食材加工成标准化的半成品每家分店根据自己的灶台设备决定最后怎么烹饪。食材供应链前端、半成品加工中端、门店烹饪后端可以分别优化也能各自替换。从这个意义上讲llvm-project 这个仓库本身就是这套理念的实体化。你 clone 下来看到的不是一个文件树里只有一个巨型程序而是一组既可独立使用、又能互相协作的子项目集合。有人可能会问那我只想要一个能编译 C 的编译器直接装发行版里的 clang 不就行了吗确实可以但 clang 只是这个仓库里众多组件之一而且是“前端”角色。真正让你能对优化、代码生成、链接、运行时行为做精细控制的能力全部藏在整个项目分层协作的架构里。我见过不少初学者直接 clone 仓库然后轻描淡写地执行cmake加make希望在几分钟内得到一个编译器。这个想法本身没有错但如果不理解上述分层关系后面的构建参数会看得你一头雾水。为什么要这么多样化的目标和选项为什么要区分LLVM_TARGETS_TO_BUILD和LLVM_ENABLE_PROJECTS这些细节背后的逻辑都跟“项目 vs 编译器”的区别直接相关。顺着这个思路往下走我们就能理解llvm-project 真正强悍的地方不在于它某个单独组件能干什么而在于这些组件被设计成可以自由拼装。你既可以拿它构建出一个完整的 C/C 工具链也可以只抽其中一部分用来做静态分析甚至可以完全绕过所有的前端和后端只在 IR 层面上写优化逻辑。2. 仓库里到底装了什么核心组件与它们的分工llvm-project 仓库的一级目录非常多第一次接触的人很容易迷失。我们先把它拆成几类每类说明它的职责和典型使用场景。下面这张表格是我个人整理的组件分工视图不是官方目录的照搬但按这个框架去理解会清晰很多。组件角色典型用途llvm核心基础设施包含 IR、优化器、后端代码生成、MC 层编写 Pass、定制后端、生成目标代码clangC/C/Objective-C 前端编译 C/C、静态分析、IDE 索引clang-tools-extraclang 周边工具clang-tidy、clangd、include-what-you-uselld链接器替代系统默认 ld速度优势明显compiler-rt运行时库Sanitizer、覆盖率支持、builtinslibc / libcabiC 标准库实现及 ABI 层配合 clang 使用的新版 C 运行时lldb调试器基于 LLVM 架构的调试工具mlir多级中间表示框架AI 编译器、硬件抽象、DSL 编译flangFortran 前端科学计算领域的老牌语言接入 LLVMpolly循环优化与多面体模型高性能计算优化openmpOpenMP 运行时与编译支持并行计算2.1 llvm 核心IR、优化器与后端llvm 目录是整个仓库的心脏。这里最核心的产物包括LLVM IR一种静态单赋值SSA形式的中间语言既是文本形式.ll也是二进制位码形式.bc。所有前端最终都把源代码翻译成 IR所有优化都发生在 IR 上。优化器一个以 Pass 为基本单位的流水线。每个 Pass 对 IR 做一次变换比如死代码消除、循环不变量外提、函数内联等。后端包含指令选择、寄存器分配、指令调度、目标代码发射等子系统。针对 X86、ARM、RISC-V、PowerPC 等架构有各自的实现。理解 IR 的价值是理解这个项目的钥匙。IR 介于“人类可读的高级语言”和“机器可读的汇编”之间它足够低层能表达机器相关的细节又足够抽象能承载各种高级语言的语义。我在实际工作中最直观的感受是通过在 IR 层写分析和优化逻辑一份代码就能同时作用于所有接入 LLVM 的前端语言这比针对具体高级语言做处理通用得多也稳定得多。2.2 clang最常用的前端但不只是前端clang 是 llvm-project 里用户接触最多的组件。它的核心任务是把 C、C、Objective-C 代码翻译成 LLVM IR。但要提醒一句clang 对很多人来说已经“不只是编译器”了它同时提供了一套完整的库接口可以用于语法树分析、代码重构、静态检查、代码补全。典型例子是 clangd它给编辑器提供了语言服务器能力。我以前用 VS Code 写 C 时后台进程就是 clangd负责跳转定义、查找引用、实时诊断。这些能力并不需要你真的编译一个程序而是 clang 库在解析层面的工作。clang-tidy 是另一个常用工具它基于 clang 的 AST 做静态检查能帮你发现潜在 bug、风格问题、性能问题。有些团队直接把它作为 Code Review 的前置门禁配合 .clang-tidy 配置文件统一代码规范。2.3 lld 与 compiler-rt链接和运行时层面的“隐形功臣”很多人写 C/C 只关心编译不关心链接。但链接恰恰是构建过程中的瓶颈之一尤其是大型 C 项目。lld 的目标就是用更现代的实现替代老牌链接器实测对大项目的链接速度快了不是一点半点。我在构建 Chromium 级别的巨型工程时从 ld 切换为 lld链接时间能减少一半以上。这不是堆硬件能解决的而是算法和数据结构层面的差异。compiler-rt 看起来不起眼但它提供了底层运行时支持。最出名的包括 AddressSanitizerASan、UndefinedBehaviorSanitizerUBSan、ThreadSanitizerTSan。这些工具几乎是 C/C 程序员的救命稻草。ASan 能帮你捕获内存越界、释放后使用、堆溢出等问题配合测试用例跑一遍比自己盯着代码硬找效率高得多。2.4 mlirLLVM 生态里最有想象力的新方向MLIR 不是传统意义上的编译器组件它是一套“用于构建编译器的框架”。它支持多级中间表示也就是说你可以在高层定义接近应用语义的 IR再逐步下降到 LLVM IR。为什么需要多级 IR举个例子你写一个深度学习编译器输入是 PyTorch 的计算图如果直接把计算图下降到 LLVM IR中间会丢失很多高级信息比如张量形状、算子融合机会、内存布局约束。MLIR 让你先建一个 Tensor 级别的 IR 做算子融合和布局优化然后一步步下降到循环级别、向量级别最终到 LLVM IR。这个思路让 AI 芯片厂商做工具链时省了很多重复劳动。2.5 这些组件是怎么被组织到一起的llvm-project 在 2019 年底前后完成了 monorepo单仓库化改造。在此之前clang、lld、compiler-rt 等各自有独立的仓库版本必须手动对齐维护成本很高。统一成一个仓库之后主干开发和版本发布变得简单很多你 checkout 同一个 commit就能获得所有组件在时间上完全同步的源码状态。这看起来是工程组织层面的事但对使用者来说意义重大。官方发布的 LLVM 版本是一个整体你不需要自己在不同组件之间做兼容性测试。比如我升级到 LLVM 17 时clang、lld、compiler-rt、libc 全部从同一个 tag 下源码构建不存在“clang 17 配 lld 16”这种组合问题。社区提供的 release tarball 也是同样逻辑。3. 源码构建为什么建议你亲手编一次 llvm-project我知道很多人用的是发行版自带的 clang或者从官网下载预编译包很少自己从源码构建 llvm-project。我的建议是如果你只是打算日常使用预编译包完全够但如果你想深入了解这个项目甚至后续在里面改代码那么自己从源码构建一次几乎是必经之路。这个过程能帮你理清依赖关系、构建系统、组件开关而这些知识在你看 Pass、写后端的阶段都会用到。3.1 依赖与前置环境准备llvm-project 的构建依赖并不多基础的 GCC 或者 clang 编译器、CMake、Ninja、Python 就够了。我建议使用较新的系统环境因为老旧的编译器版本可能导致新版本 LLVM 无法编译。这里的“新”没有一个绝对标准通常看你想构建的 LLVM 版本要求的最低编译器版本。源码包里的llvm/CMakeLists.txt或文档会写明这一点。我自己的习惯是在最小化环境里验证依赖减少干扰。以 Ubuntu 22.04 为例只需要执行sudo apt update sudo apt install build-essential cmake ninja-build python3其中build-essential提供 GCC用来引导编译 LLVM 自身cmake负责生成构建文件ninja比 make 快增量构建体验更好。Python 在构建过程中主要参与一些代码生成脚本的调用。3.2 一次最简构建的完整命令与参数解读下面是我推荐的最简构建流程。先 clone 仓库注意加--depth 1和--branch可以显著减少下载量git clone --depth 1 --branch llvmorg-17.0.6 https://github.com/llvm/llvm-project.git cd llvm-project cmake -S llvm -B build -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_ENABLE_ASSERTIONSON cmake --build build我对这个构建配置做一个逐项拆解方便你按需调整-S llvm -B build告诉 CMake 源码根目录是llvm构建目录是build。这里的llvm是仓库里的一级目录不是仓库根目录。刚开始很容易搞错在仓库根目录直接执行 cmake 会报找不到 CMakeLists.txt。-G Ninja指定生成器为 Ninja。相比默认的 Unix MakefilesNinja 在并行度和增量构建上强很多。-DCMAKE_BUILD_TYPERelease构建 Release 版本。如果你要调试 LLVM 自身可以改成 Debug但编译时间会大幅上升磁盘占用也会多很多。-DLLVM_ENABLE_PROJECTSclang;lld指定除了 llvm 核心之外还要构建哪些子项目。用分号分隔。我这里选了 clang 和 lld是日常使用频率最高的两个。-DLLVM_TARGETS_TO_BUILDX86只生成 X86 后端的代码。如果不指定默认会构建所有支持的后端包括 ARM、AArch64、RISC-V、PowerPC 等编译时间要翻好几倍。如果只是为了学习和日常编译只保留自己所在架构即可。-DLLVM_ENABLE_ASSERTIONSON开启断言。对开发 LLVM 本身的人很重要能从运行期错误中早一点发现 IR 层面的异常。代价是编译出的优化器性能略降。我开发 Pass 时一直开着它效果远大于副作用。构建完成后二进制文件会出现在build/bin目录下里面包含clang、llc、opt、llvm-dis等一系列工具。你可以把build/bin加入PATH方便后续使用。3.3 构建时间的几个现实问题与对策LLVM 的构建时间是个绕不开的话题。全量 Release 构建在 8 核 16G 内存的机器上可能需要 20 到 40 分钟具体取决于配置。如果稍微贪心一点把默认 Target 都打开轻松接近一个小时。这不算异常不需要怀疑自己操作出了问题。如果嫌构建慢有几种优化思路只构建你需要的 Target。像我在 x86 机器上只保留 X86 后端省掉一大半编译量。使用 ccache。第一次全量构建仍然要时间但改代码后二次构建会快很多。配置方式是在 cmake 时加-DLLVM_CCACHE_BUILDON。提高并行度。Ninja 默认根据 CPU 核数分配任务也可以用-j指定例如cmake --build build -j 16。但内存不足时并行度过高容易把机器跑死。我印象很深的一次经历是在只有 8G 内存的云主机上构建 LLVM默认并行度直接 OOM内存耗尽了。解决方案是调低 Ninja 并行任务数或者选择构建 Release 版并关闭 Debug 信息来减少内存压力。如果你在内存受限环境里操作建议把-DLLVM_ENABLE_ASSERTIONSOFF也考虑进去能显著降低内存峰值。4. 上手实战写一个你的第一个 LLVM Pass在 llvm-project 里写代码最典型的切入点就是写一个 LLVM Pass。Pass 是对 IR 做分析和变换的基本单位。学习写 Pass 能让你同时理解 IR 的结构、优化器的运行机制、以及如何注册和测试一段自定义逻辑。4.1 新版 Pass 的编写骨架LLVM 从 14 版本起默认使用 New Pass Manager。你在网上会看到很多旧版的 Legacy Pass 教程里面大量使用initializeXXXPass之类的宏那些在新框架下已经不推荐了。我建议直接上手新写法避免一开始就混淆。下面是一个最简的 Function Pass它会遍历每个函数统计基本块数量并打印输出。这虽然没有什么实际优化效果但足以演示整个开发流程。#include llvm/IR/Function.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 BlockCounterPass : public PassInfoMixinBlockCounterPass { public: PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { outs() Function: F.getName() \n; outs() BasicBlock count: F.size() \n; return PreservedAnalyses::all(); } }; } // namespace llvm::PassPluginLibraryInfo getPassPluginInfo() { const auto callback [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name block-counter) { FPM.addPass(BlockCounterPass()); return true; } return false; }); }; return {LLVM_PLUGIN_API_VERSION, BlockCounter, LLVM_VERSION_STRING, callback}; } extern C LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return getPassPluginInfo(); }这段代码里最有意思的是PreservedAnalyses::all()。它的含义是我这个 Pass 对 IR 没有任何修改所有分析结果都保持有效。如果你改了 IR就要返回相应被破坏的分析信息否则后续 Pass 可能使用了过期的分析结果产生很难排查的错误。这个设计是 New Pass Manager 对旧版框架的最大改进之一把“谁影响了谁”的依赖关系显式化。4.2 编译与注册 Pass 的两种方式编译这段代码有两种方式第一种是把源码放进 LLVM 树内作为内部 Pass 编译第二种是编译成动态库用opt在运行时加载。我强烈推荐第二种它不需要改动 LLVM 本身的构建配置开发效率高很多。以动态库方式为例写一个简单的 CMakeLists.txtcmake_minimum_required(VERSION 3.20) project(BlockCounter) find_package(LLVM REQUIRED CONFIG) add_library(BlockCounter MODULE BlockCounter.cpp) target_include_directories(BlockCounter PRIVATE ${LLVM_INCLUDE_DIRS}) target_compile_definitions(BlockCounter PRIVATE ${LLVM_DEFINITIONS}) target_link_libraries(BlockCounter PRIVATE LLVM)注意find_package(LLVM REQUIRED CONFIG)这一行它要求你在配置时用-DLLVM_DIR指定 LLVM 的 cmake 配置文件路径。如果你按照上一节的流程构建这个路径就是build/lib/cmake/llvm。配置和编译命令如下cmake -S . -B build -DLLVM_DIR$(pwd)/../llvm-project/build/lib/cmake/llvm cmake --build build编译完会得到libBlockCounter.so。接下来准备一个简单的 C 源码test.cint add(int a, int b) { return a b; } int main() { int x add(1, 2); return x; }先用 clang 生成 IR再用 opt 加载我们的 Passclang -S -emit-llvm test.c -o test.ll opt -load-pass-plugin./build/libBlockCounter.so -passesblock-counter test.ll执行后你会在终端看到每个函数的基本块数量。看到自己的 Pass 跑起来的那一刻基本就算迈进 LLVM 开发的门了。4.3 构建 Pass 时容易被卡住的三个点第一个坑找不到PassPluginLibraryInfo。这个符号在 LLVM 源码里定义一般不需要你额外链接库。如果编译报链接错误检查一下你的 CMake 是否真的依赖了 LLVM 库以及LLVM_DIR指向的版本是否和你源码一致。第二个坑版本不匹配。LLVM 的 Pass API 在版本间变化不小如果你看的是老教程代码很可能编译不过。应对方式是查看你本地安装的 LLVM 头文件里的示例代码llvm/examples/Bye目录就是一个非常好的参考实现。第三个坑-passesblock-counter在 opt 里提示找不到 Pass。这通常说明你的 Pass 没有通过-load-pass-plugin正确加载或者注册回调里Name与命令行参数不一致。可以先在getPassPluginInfo里加一个outs()输出确认插件被加载了。5. 我踩过的构建和使用坑附带排查链路这里分享几个我实际遇到的典型问题。这些问题查官方文档未必有现成答案但按排查链路走下来通常都能解决。5.1 内存不足导致 Ninja 进程被杀现象是构建过程中终端突然报Killed没有明确 error 信息dmesg 里能看到 OOM 记录。排查思路是先确认不是 Swap 不足的问题再逐步调低并行度。我最终的处理方式是限制并发任务数cmake --build build -j 4同时在 CMake 配置时降低 Debug 信息级别比如-DCMAKE_BUILD_TYPERelease配合-DLLVM_ENABLE_ASSERTIONSOFF。内存从 16G 降到 8G 的机器也能顺利编完只是时间稍长。5.2 编译报“undefined reference tollvm::...”这类问题大多出在链接环节。常见原因是你写的 Pass 或工具使用了 LLVM 的某些库但没有在 CMake 里链接。排查链路是先看报错符号属于哪个组件。比如llvm::createX86ISelDAG这种明显是后端符号就要确保你构建 LLVM 时包含了 X86 后端并且在链接时链接了LLVMX86CodeGen这类目标。一个比较隐蔽的问题是LLVM 17 之后对 C 标准版本要求提高如果你用 GCC 编译自己的 Pass但 GCC 版本过老默认的 C 标准达不到 LLVM 头文件要求也会出现奇怪的编译错误。这时可以手动加-stdc17或者升级编译器版本。5.3 clang 编译时默认找不到标准库头文件如果你只构建了 clang 而没安装配套的 libc直接编译 C 程序可能报找不到iostream之类的头文件。原因是 clang 默认会去系统路径找 libstdc但某些发行版没装 GCC 的开发包。最简单的解决方式是安装系统完整的 build-essential让 clang 能复用系统的 libstdc。或者你用 llvm-project 构建时也加上libcxx;libcxxabi然后在编译命令里显式指定clang -stdliblibc test.cpp但要注意这个命令能否成功还取决于你的 clang 是否能找到 libc 的头文件路径。如果是从源码构建通常要把build/include/c/v1加入头文件搜索路径或者安装到系统路径。5.4 升级 LLVM 大版本后 Pass 源码编译失败这是最常遇到的维护型问题。LLVM 主版本演进时API 兼容性并不保证。比如从 LLVM 16 升级到 LLVM 17很多 Pass 的构造函数签名、分析管理器接口可能都变了。我的做法是建立一个对照测试矩阵本地同时保留旧版本和新版本的源码目录编译自己的 Pass 时先用新版头文件过一遍。如果报错到新版本源码的include/llvm头文件里搜索对应的类看接口变化再对照llvm/examples或 LLVM 单元测试里的写法修改。这个排查链路听起来笨但非常有效。LLVM 自身单元测试里对每个 Pass 的调用方式基本就是最佳范例比你在网上搜索的第三方博客可靠得多。6. 后续可以怎么继续深入走到这一步你基本已经理解了 llvm-project 的架构和基本开发方式。接下来想深入的话我个人推荐三条路径你可以按兴趣选择。第一条路径是 IR 层面的深入。把LLVM Language Reference文档通读一遍理解每一条 IR 指令的语义再用opt的手工输入 IR 去验证 Pass 对指令的变换效果。我自己学 IR 的方式是拿 C 写好例子用clang -S -emit-llvm生成 IR然后逐段分析对应关系。这个过程能帮你建立起“高级语言到中间表示”的直觉。第二条路径是后端方向。从llvm/include/llvm/Target里的 TargetMachine、TargetLowering、TargetRegisterInfo 接口开始配合一个简单指令集比如 RISC-V的 TableGen 文件去理解指令选择过程。这条路径门槛偏高但如果你对芯片或编译器后端有长远兴趣它的回报也是最大的。第三条路径是 MLIR 方向。如果你做 AI 编译器或者对异构计算感兴趣MLIR 是一个绕不开的框架。可以从mlir/examples下的 Toy 教程入手逐步理解方言Dialect、操作Operation、类型Type这些核心抽象再到如何把一个自定义 DSL 编译到 LLVM IR。我的建议是不要贪多。llvm-project 的知识体系是高度分层的每一层都需要投入时间去消化。哪怕一开始只理解 IR 和 Pass再往后看任何组件都会轻松很多。你在动手写代码和踩坑过程中积累的那些经验会比读十篇教程都管用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。