llvm-project源码获取、构建与二次开发实战指南

发布时间:2026/9/19 2:50:57
llvm-project源码获取、构建与二次开发实战指南 有段时间没折腾编译器相关的东西了最近因为一个代码分析的需求又跟 llvm-project 打起了交道。说句实话每次重新拉这个仓库、配置构建环境的时候我都觉得这玩意儿又爱又恨——爱的是它作为编译器基础设施的完备程度几乎无可挑剔恨的是如果你不了解它的内部结构和构建逻辑光是 cmake 那几百个配置项就能让你转晕。如果你对 LLVM、Clang 这些名字还比较陌生可以先把它理解成一套“编译器工具箱”。它不是只给你一个命令行工具而是给了一套完整的库和工具链你可以用它来写编译器、写静态分析器、做代码优化、跑自动化重构甚至用它生成给 GPU 用的代码。llvm-project 就是这套工具箱的源码仓库总称网上常说的 LLVM、Clang、lld、libc、compiler-rt 这些项目其实都统一放在这一个仓库里维护。这篇文章我想从一个实际动手的角度把 llvm-project 这个仓库的获取、结构、构建、使用过程中最容易被忽略的细节掰开说清楚也把我踩过的一些坑同步给你希望能帮你少走点弯路。老实说这篇文章不适合只想“用一下 Clang 编译个 C 文件”的人那种场景直接装发行版自带的 clang 包就够了不需要碰源码。但如果你是想基于 LLVM 做二次开发、研究编译原理、给某个芯片写后端或者想搞懂这套现代编译器基础设施的工程组织方式那这份实操笔记你应该用得上。1. llvm-project 到底是什么仓库里都有哪些东西1.1 不只是“一个编译器”而是一整套工具链生态很多人第一次见到 llvm-project 这个词会以为它只是一个跟 GCC 类似的编译器项目。但严格来说LLVM 的核心设计理念跟传统编译器有很大不同。传统编译器通常是“前端 中端 后端”三段式写在一个整体程序里而 LLVM 从一开始就把这三层拆成独立模块中间用一套被称为“LLVM IR”Intermediate Representation中间表示的抽象语言来衔接。这样做带来的直接好处是前端只要负责把各种语言翻译成统一的 IR后端只需要负责把 IR 翻译成目标机器指令两者完全解耦。所以你可以看到在 llvm-project 仓库里既有 C/C 语言的 Clang 前端也有 Rust、Swift 等外部项目把各自的 AST 降低到 LLVM IR。这一套设计理念几乎影响了后来所有现代编译器项目的架构选择。整个 llvm-project 仓库并不是一个单一的代码库它内部按功能拆成了几十个子项目目录。你需要搞清楚它们各自负责什么因为很多人第一次 git clone 完这个仓库后会被庞大的目录结构吓到其实核心的也就那几个。1.2 子项目地图快速搞清 llvm、clang、lld、libc 这些目录的关系在 llvm-project 顶层目录下你会看到 llvm、clang、lld、lldb、libc、libcabi、compiler-rt、polly、flang、mlir、bolt、clang-tools-extra 等等一大堆目录。我给你按实际用途分一下类这样你在定位代码时会更清楚。第一类是核心基础设施就是 llvm 目录本身它包含了 LLVM IR 的定义、优化 Pass 框架、目标后端X86、ARM、RISCV 等以及 CodeGen 代码生成逻辑。也就是说所有语言的公共优化和代码生成逻辑都在这个目录里。第二类是语言前端主力是 clang负责解析 C、C 和 Objective-C 源码并生成 LLVM IR。如果你用过 clang-tidy、clang-format这两个工具其实也在这个体系里其中 clang 的源码位于 tools 目录下而 clang-tools-extra 子目录主要负责维护 clang-tidy 等额外工具。第三类是工具链集成组件比如 lld 是链接器lldb 是调试器这两个分别对应传统工具链里的 ld 和 gdb。还有 compiler-rt它提供编译器运行时库像 ASan、UBSan 这些你的代码在运行时需要的开关库都集成在这个模块里。第四类是标准库和扩展项目其中 libc 和 libcabi 对应 C 标准库及其 ABI 层的实现flang 是 Fortran 编译器前端mlir 是用于构建编译基础设施的多级中间表示框架近两年很多 AI 芯片编译器都用它来做底层表示层的工作。别急着把每个目录都背下来。真正需要你深入源码去读的时候往往是针对某一个具体问题这时候知道“问题应该去哪个仓库里找”就已经赢了一半。比如你在做链接期优化时遇到了奇怪的问题那大概率要去 lld 和 llvm/lib/Passes 下翻代码而不是对着 clang 的前端语法树发愁。2. 源码获取与构建环境准备仓库很大别硬来2.1 拉取仓库的正确姿势不要傻乎乎地 git clone 全量历史我第一次碰 llvm-project 时按照很多开源项目的习惯直接 git clone结果仓库体积和克隆耗时都让人相当难受。llvm-project 是个历史非常长的项目Git 历史里积攒了大量变更记录全量克隆大概会拉到几个 GB 的数据这对网络条件和磁盘空间都不太友好。这里我建议用带有 --depth 参数的浅克隆方式只需要拿到最新的代码切片不关心历史提交记录。具体做法是先拉取默认分支的最新代码这样至少要等一段时间因为仓库单层工作区文件本身也是很大的但是比全量克隆省很多流量和时间。如果你对版本稳定性有要求建议直接切到某个 release 分支。LLVM 官方每半年发一个大版本版本号规则像 18.x、19.x 这样递增发布。选择 LLVM 版本有个经验可以参考不要选太旧的比如 10 以下的因为社区对这些老版本的维护基本停滞新硬件和后端特性的支持会跟不上也不要选太新的小版本如果有问题社区还来不及反馈修复。对一个可靠的选择来说在当前最新版往回退一个大版本通常是很稳的比如最新是 20那选 19 就比较稳妥。2.2 构建前的系统依赖和硬件要求内存是最大的瓶颈llvm-project 的构建是出了名的吃资源尤其是编译 Clang 和 LLVM 核心库时需要大量并行编译任务和内存空间。如果你用 Ninja 配合默认的并行度在 8G 内存的机器上有很大概率直接 OOM 崩溃。官方文档建议内存越大越好但按我的实际体验一台 16G 内存的机器配置合适的构建参数用合适的并行数基本可以在可接受的时长内完成 Release 版构建。磁盘方面源码加构建产物至少准备 50G 空间如果开 Debug 模式那数据量会更大预留空间最好按 80G 来感觉比较踏实。系统依赖这一块Linux 上最核心的就是 cmake、ninja 和 gcc 的版本llvm-project 对某些依赖也有版本下限要求。值得注意的是cmake/ninja/gcc 这些工具的版本都不能太老否则在配置阶段就可能报错退出。2.3 为什么不建议你现在就从零构建整个项目聊到这一步我特别想跟入门的朋友说句掏心窝的话如果你的目标只是试用 Clang 的新功能或者写个小插件分析代码完全没必要从源码构建整个 llvm-project。发行版自带的 clang 包或者官方发布的预编译二进制对于绝大多数场景都够用了。那到底什么情况下才值得自己从源码构建据我经验通常有三类需求。第一类是你要改 Clang 或 LLVM 核心源码并验证效果第二类是你需要为特定 target 交叉编译或者要开启发行版没开的某些特性第三类是你的项目要在 LLVM 的某个具体 Commit 上做开发环境对齐。只有这些时候从源码构建才是绕不开的路径。而且即便真要构建也推荐你只构建需要的子项目而不是一下子把整个项目的全量组件都编出来下面我会具体讲这套“最小化构建”的配置技巧。3. 完整构建流程详解从 cmake 配置到 Ninja 编译3.1 一份可以直接参考的 CMake 配置模板构建 llvm-project 时配置和编译两个步骤分别由 cmake 和 ninja 负责。cmake 阶段你需要指定源码路径和构建目录路径构建目录建议单独建一个目录不要放在源码目录内部这样源码树更干净出问题时也可以直接删掉构建目录重来。下面这份配置是我平时用得比较多的一套参数组合你可以直接抄来用。cmake -G Ninja \ -S llvm-project/llvm \ -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld;clang-tools-extra \ -DLLVM_TARGETS_TO_BUILDX86;AArch64;RISCV \ -DLLVM_ENABLE_ASSERTIONSON \ -DCMAKE_C_COMPILERgcc \ -DCMAKE_CXX_COMPILERg \ -DCMAKE_INSTALL_PREFIX/opt/llvm-project-dev配置完成后直接执行编译命令建议控制并行度以控制内存上限ninja -C build -j8下面我来逐条解释这几个参数是什么意思为什么要这么设。第一个是 -DCMAKE_BUILD_TYPE这个参数决定编译出的二进制是 Release 还是 Debug。Release 模式下优化开得足、运行时速度快但调试信息少Debug 模式下携带完整调试信息方便用 gdb 追踪源码但编译出来的二进制体积和运行速度都不占优势。做编译器开发的人经常会在 Release 模式下打开断言开关这样可以兼顾大多数场景。第二个是 -DLLVM_ENABLE_PROJECTS这个参数是在告诉构建系统除了 LLVM 核心之外还要一并构建哪些子项目。注意这里的写法是用分号把项目名拼成一个字符串。如果你把一堆子项目全写上比如把 flang、mlir、lldb 全都启用那构建时间会成倍上涨。前面我建议构建什么就启什么原因就在这里。第三个是 -DLLVM_TARGETS_TO_BUILD用于选择需要生成代码的目标架构。如果不设置默认会构建大量架构的后端支持比如 MIPS、PowerPC 等既慢又没必要。按需指定自己真正用得到的架构可以明显减少编译时间和产物体积。第四个是 -DCMAKE_C_COMPILER 和 -DCMAKE_CXX_COMPILER。LLVM 本身需要用一个已有的 C/C 编译器来编译这里我习惯用系统的 GCC。如果你想做一个比较冷门的实验比如想知道新版本 LLVM 用老版本自身能否成功引导编译那就涉及到一个术语叫做“自举构建”这个玩法更高级等你有一定经验后再尝试也不迟。最后一个 -DLLVM_ENABLE_ASSERTIONSON我自己特别看重这个选项。它会在 LLVM 内部和外部接口中启用断言检查对发现调用错误很有帮助。官方 Release 版通常把这个关掉以追求性能但你在开发调试时打开它还是很有必要的很多难以察觉的问题能靠断言直接定位出来。3.2 只构建需要的组件怎么缩短编译时间前面提到编译整个工程会很久这里我介绍几个能让构建过程省时省力的核心思路。第一个思路是用 ccache 做缓存它的原理是把每次编译的中间结果缓存起来如果源码和编译参数没变就直接从缓存拿结果而不重新编译。这个工具对重复构建的场景提升非常明显特别是你改了某个很小的头文件触发大面积重新编译时ccache 的效果立竿见影。配置方式是在 cmake 时加一个参数指定 ccache 路径-DLLVM_CCACHE_BUILDON。第二个思路是控制并行任务数。ninja 默认会根据 CPU 核数把所有核都占满但这并不总是最优解。当你的内存有限时并行任务太多会导致内存耗尽直接崩溃。把并行数手动设置为一个跟内存匹配的值常常是更稳的选择。经验值我后面讲排查时会具体说。第三个思路是增量构建。llvm-project 支持按 Target 维度做增量编译。比如你只是改了 X86 相关代码那可以只用 ninja 编译指定组件而不是重新编译全部内容。通过给 ninja 传具体的构建目标名比如某个特定优化 Pass 对应的目标就能把改动的影响范围压缩到最小大幅缩短编译等待时间。3.3 构建完成后如何验证工具链是否可用构建完成后你会得到一整套工具链二进制例如 clang、clang、ld.lld、llvm-ar、llvm-nm 等。我自己有个习惯看到二进制生成后并不急着做大型验证先跑一个最简单的 C 文件确认工具链基本可用再继续深入测试。有个特别重要的点想提醒你如果你打算拿这份自建工具链去编译一个大型 C 项目务必优先确认 C 标准库的头文件和库路径是否找得到。这不是随便就能说清楚的问题它涉及到你用哪个标准库实现、标准库头文件在哪里、以及链接时能否把 libc 或 libstdc 正确关联起来。很多新手在这里栽跟头明明编译出了 clang结果处理 C 项目时报出一堆找不到头文件的错误。4. 基于 llvm-project 做二次开发调试、测试、定位问题的实战姿势4.1 写一个简单的 LLVM Pass 并跑起来很多朋友接触 llvm-project 不只是为了用工具链而是想在框架上做二次开发。我最常遇到的就是想写 LLVM Pass对 IR 做自定义分析或优化的人。这里我就不讲完整的 Pass 写法了重点讲一下“跑起来”的流程和当前版本框架的差异。LLVM 框架经过多次迭代新版本已经推荐用 New Pass Manager写 Pass 的风格和老版本有本质区别。建议你直接照官方最新文档或示例代码开始不推荐去找很老的教程硬套。拿我自己的项目举例我需要在某个函数调用点插桩当时的做法是写一个 FunctionPass在 runOnFunction 回调里遍历指令找到 CallInst 后做数据插桩然后用 opt 工具加载 .so 文件并跑在指定的 IR 上。这个流程的关键在于你的 LLVM Pass 源码版本必须跟 clang 和 opt 工具的版本严格一致否则加载时很容易报符号不匹配或 ABI 冲突。4.2 用 lit 和 FileCheck 跑测试用例而不是全靠手工验证LLVM 这个项目非常重视测试它的测试基础设施主要由 lit 和 FileCheck 构成。你可以在每个子项目的 test 目录下看到一堆 .ll、.c、.cpp 文件它们其实就是测试用例里面用 RUN 指令声明了要执行的命令再用 CHECK 指令声明了对输出内容的匹配规则。作为开发者在你改完代码后跑一遍相关目录的测试对比改动前后测试结果是否一致可以帮助你快速判断改动是否破坏既有行为。跑测试也有技巧直接全量跑整个项目的测试相当费时推荐的做法是先跑到你改动的模块对应的测试子目录比如 llvm/test/Transforms 下针对某个 Pass 的测试。4.3 利用 llvm-lit 做自动化回归先搞懂 RUN 和 CHECK 的基本含义我举一个具体的例子来说Test 文件开头通常有一行; RUN: opt -passesmy-custom-pass -S %s | FileCheck %s这行的意思是用 opt 工具加 my-custom-pass 去处理当前 IR 文件把输出交给 FileCheck 做断言。然后你在文件中写; CHECK: call void my_inserted_function()这样只要转换后的输出里找不到这一行测试就会失败。这种“文本匹配”方式非常巧妙它不依赖精确的数值比对而是关注输出的关键特征是否存在所以在编译器测试领域被广泛采用。再往下挖一点FileCheck 还支持 CHECK-NEXT、CHECK-DAG、CHECK-LABEL 这类更精细的匹配规则分别代表“紧跟上一行匹配”、“在某个区间内无序匹配”和“标记一个逻辑段起点”。这几种规则是我调试时使用频率最高的建议提前熟悉一下后续写大型校验逻辑会很省事。跑测试时进入构建目录执行特定命令即可ninja check-llvm-unit ninja check-clang如果想只跑某个具体测试文件可以这样llvm-lit path/to/test/file.ll掌握这种方式后你的调试效率会提升一个台阶因为做编译器开发时“手工看 IR 对不对”很容易遗漏边界情况自动化断言能帮你把验证粒度做细。5. 常见问题与排查技巧实录那些年我踩过的坑5.1 内存不足导致编译器崩溃应该怎么调参这是我在构建 llvm-project 过程中遇到频率最高的问题。报错信息通常是 c: internal compiler error: Killed或者 ninja 直接 reporting failed。这类问题的根源往往很简单就是并行编译任务数超出了内存承受能力。你需要做的事情也很直接把并行数降下来。如果是 8G 内存j2 比较稳妥16G 内存可以尝试 j4若仍有内存压力就降到 j232G 及以上再考虑 j8 以上。当然这只是经验参考实际还受源码改动范围影响。不想丢失并行性能的话也可以优先考虑加 swap 分区做缓冲但低频编译场景下还是直接调低并行数性价比最高。5.2 链接阶段报符号找不到或 undefined reference编译到链接阶段偶尔会遇到 undefined reference 这类问题尤其是在修改了 LLVM 库接口之后没有进行增量重建时最容易出现。通常做法是先清理旧的 build 目录然后重新 cmake 再构建这一步能解决很多诡异的链接问题。还有一个隐藏点如果你用了 LTOLink Time Optimization特性链接失败的原因可能来自 llvm 后端本身的代码生成 bug而不是代码错误。这时候需要多收集详细报错信息最好把报错内容原样保留去 GitHub 的 llvm-project issue 区搜索对比看看是否是已知问题。5.3 使用 clang 时头文件找不到该怎么设置 sysroot自己从源码构建出的 clang 跟发行版预编译的 clang 相比有个明显区别它默认不知道目标平台的标准头文件安装在哪个路径下。如果你直接拿它编译个 hello.c可能没问题但系统工程就很容易报无法找到 stdio.h 这类错误。这里我推荐你用两种方式解决。第一是让 clang 自动探测 GCC 工具链目录配置时用 -DGCC_INSTALL_PREFIX 指向你系统 GCC 的安装位置llvm 构建时可以通过指定每个工具链的路径参数校正。第二是同系统包管理器保持同步检查并安装与 clang 版本匹配的 libstdc-dev 包并确认头文件路径在 clang 默认搜索路径内。如果你的交叉编译需求更复杂就得配合 --sysroot 参数手动指定目标根文件系统路径了。这个问题是所有自建 clang 开发者都绕不开的一步耐心配置一次后面就顺畅了。5.4 如果想给上游提 Patch代码风格和提交通常怎么弄最后一个偏工程实践的话题你改完代码后如果想给上游 llvm-project 提交补丁务必严格遵守它的代码风格要求。LLVM 对代码格式有强制规范比如 80 列宽度限制、指针靠左声明、注释风格统一等。项目直接用 clang-format 作为格式化工具并提供了 .clang-format 配置文件你只要在提交前对改动文件执行相应格式规整即可。提交流程不是直接往 GitHub 发 PR 那么简单LLVM 用的是 GitHub Pull Request 的流程但在提交信息中需要添加 Differential Revision 链接等额外要求。第一次提 Patch 前建议花时间读完官方贡献指南提前了解测试包含性、文档更新这类要求能减少不少来回沟通的成本。这个流程确实比普通开源项目繁琐但这也是 llvm-project 能长期保持高质量基线的原因。6. 写在后头的一些经验每一次重新接触 llvm-project我都会有新的收获。它内部的结构逻辑非常清晰工程化程度做得极高但门槛也实打实摆在那里。如果你刚接触这个项目我的建议是先从小的切入点着手比如先学会用现有 clang 工具链做一些分析再慢慢进入 LLVM IR 的世界最后再碰 Pass 开发和后端相关代码。一上来就啃全部源码很容易因为信息过载而失去方向。构建这块别追求一步到位把整个项目都编出来够用就好。等你对编译器的内部工作流程有了直观感知再逐步按需扩展会顺畅很多。这个项目的世界很庞大但只要找对入口后面会有很多好玩的报偿。