news 2026/9/19 15:56:00

llvm-project从入门到实践:构建、源码阅读与自定义Pass开发指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
llvm-project从入门到实践:构建、源码阅读与自定义Pass开发指南

如果你是一个对编译器底层感兴趣的程序员,你大概率已经在无数技术讨论里见过llvm-project这个名字。在我刚开始接触它的时候,心里想的是:这不就是一个编译器吗,把它clone下来编一下应该就能跑吧?结果第一次构建就花了快三个小时,中途还因为内存不足死掉了两次,最后才发现是我对项目结构的理解完全不够。后来花了不少时间把源码、构建系统和核心代码的调用关系理清楚,才真正感受到这套基础设施设计的精妙。如果你也想从“听说过LLVM”到“能上手读源码、能改Pass、能基于它做自己的工具链”,这篇文章就是我走过的路的完整记录。

1. llvm-project不是“一个项目”,而是一整套编译器生态组件

1.1 从两行命令说起:clang与LLVM IR的关系

很多人的第一个疑问是:LLVM和Clang到底是不是同一个东西?我在早期也混淆过,实际上这两个概念分属编译器的不同层次。你可以运行这样两条命令:

clang -S -emit-llvm hello.c -o hello.ll clang -S hello.c -o hello.s

第一条命令会产出LLVM IR(中间表示)文本文件,第二条命令直接产出目标平台的汇编代码。在这个过程里,clang作为C/C++前端,负责把源码解析成抽象语法树,再转换成IR;而IR之后的所有优化和代码生成,才是LLVM核心(LLVM Core)真正发挥作用的地方。llvm-project这个项目之所以叫“项目”而不是“编译器”,就是因为它包含了前端、优化器、后端、链接器、标准库等一整条工具链,而不是单独一个可执行文件。

理解了这一点,再去读官方文档里的“The LLVM Compiler Infrastructure”就好办多了。所谓的Infrastructure,就是要提供一套可复用的编译组件:前端可以换(Clang、Flang、Rustc借用的前端),后端也可以换(X86、ARM、RISC-V、WebAssembly),中间共享的优化层和IR格式是整条链路的粘合剂。

1.2 拆开monorepo:llvm、clang、lld、libc++各自扮演什么角色

llvm-project以monorepo形式管理,根目录下并列着很多子项目。这里我列一下最核心的几块:

目录作用对应可执行文件/库
llvm/核心优化器、IR、目标后端、日常工具opt,llc,lli,llvm-dis
clang/C/C++/Objective-C前端clang,clang++
lld/高性能链接器,支持ELF、Mach-O、COFF等ld.lld,lld-link,ld64.lld
libc++/C++标准库实现无直接可执行文件,编译时通过-stdlib=libc++使用
libc++abi/为libc++提供ABI支持,RTTI、异常处理等
compiler-rt/运行时库:Sanitizer、PGO、内存分析等无,编译时通过clang自动链接
flang/Fortran前端flang-new
mlir/多维中间表示层,专用于机器学习、编译加速mlir-opt,mlir-translate
polly/多面体模型中循环优化opt的扩展

在实际构建时,你可以选择只构建核心和Clang,也可以选择带上lldcompiler-rt。很多发行版里的clang二进制之所以不能和系统链接器很好地配合,就是因为发行版没有把lld一起启用。自己从llvm-project构建时,完全可以把这一套都装上,体验整套工具链顺畅协作的感觉。

1.3 为什么三段式架构能通吃多种语言与硬件

传统编译器(比如GCC)虽然也是“前端-优化-后端”,但LLVM把这三段之间的接口标准化成了一门显式的、有文本表示的语言——IR。这意味着,任何语言只要写出一个能生成IR的前端,就能立刻复用整个优化器、后端和链接器。这带来的直接好处是,如果你想发明一门新语言,不需要为每一个硬件平台从头写代码生成器,只要对接好LLVM后端,就能获得X86、ARM、RISC-V等主流平台的支持。

这个架构还让“一次编写,随处优化”成为可能。因为IR是明确定义的数据结构,你可以在IR层面写跨语言的优化Pass,比如循环展开、内联、常量传播,这些优化对所有接入LLVM的前端语言都生效。在我自己写Pass之后,更能体会到这种模式的价值:我只需要保证对IR的正确变换,不用关心Pass跑在到底是C++还是Rust前端产生的IR上。

2. 动手构建llvm-project:环境评估和CMake配置的实用建议

2.1 源码获取的三种方式与版本选择

要从零开始使用llvm-project,第一步自然是获取源码。官方推荐的方式是:

git clone https://github.com/llvm/llvm-project.git

也可以用--depth做浅克隆来减少首次下载体积:

git clone --depth=1 -b llvmorg-18.1.8 https://github.com/llvm/llvm-project.git

这里有一个版本选择的问题。如果你只是学习,直接跟踪main分支也没有太大问题,但我在生产环境里会锁定一个发布版本,比如llvmorg-17.0.6。原因很简单:main分支每天都有大量提交,很多API接口、Pass注册风格、CMake变量会发生变化,今天能编译通过的代码可能一周后就因为接口调整而报错。而release分支经过了完整的测试,第三方工具如nvptx后端、libc++的兼容性都更稳定。

2.2 构建前的资源评估:内存、磁盘与时间

很多人在第一次构建LLVM时栽在资源预估上。我建议至少准备20GB磁盘空间,如果开启了Debug断言和测试,35GB也不嫌多。内存方面,经典方案是并行-j数量不超过物理内存(GB)的1.5倍。例如一个8核16GB内存的机器,不要直接ninja -j16,比较稳妥的是-j6-j8,不然链接阶段很容易因为内存峰谷触发OOM。时间上,Release构建生成的clangopt在中等配置机器上大约需要30到60分钟;Debug构建由于没有优化且带大量断言,时间可能是Release的三到四倍。

如果想快速验证修改,可以采用“只构建指定工具”的方式,例如:

ninja opt ninja clang ninja llc

这样能大幅缩短迭代周期。我通常先构建optclang,做Pass开发时只需要这两个工具就足够了。

2.3 核心CMake选项逐项解读

构建目录最好和源码目录分开,在源码目录外新建一个build目录,然后执行:

cmake ../llvm-project/llvm \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS="clang;lld;libcxx;libcxxabi" \ -DLLVM_TARGETS_TO_BUILD="X86;AArch64;RISCV" \ -DLLVM_ENABLE_ASSERTIONS=ON \ -GNinja

这些选项各自的含义是:

  • CMAKE_BUILD_TYPE=Release:生成优化过的二进制,适合平时使用和性能测试。如果是为了调试Pass,用Debug会获得更完整的符号和边界检查,但物理内存最好在32GB以上。
  • LLVM_ENABLE_PROJECTS:决定需要一起构建哪些上层项目。注意这个列表以分号分隔,在命令行的引号里要写对。
  • LLVM_TARGETS_TO_BUILD:限定目标后端。如果只写X86,构建时间会显著降低。但如果你要写后端代码或实验交叉编译,就得加上对应目标。可以写host表示只构建当前机器架构的后端,省时省事。
  • LLVM_ENABLE_ASSERTIONS=ON:这个选项强烈建议在开发时开启。它会让LLVM在IR合法性检查、Pass管理器验证等位置主动抛出断言错误,帮助尽早暴露问题。很多崩溃在Release下表现为段错误,而在开启断言的Debug下会变成明确的“IR does not satisfy ... constraint”,省去大量排查时间。

如果CMake配置完成后又新增了子项目,不需要重新创建构建目录,只需要重新运行CMake命令并添加新的-DLLVM_ENABLE_PROJECTS即可。我遇到过直接在命令行把clang追加到已有缓存变量里的情况,这样很容易导致CMake缓存不一致,正确做法是用cmake -DLLVM_ENABLE_PROJECTS="clang;lld" ...重新配置一遍。

2.4 构建失败排查:内存不足、Python版本、链接错误

下面列几个真实容易踩的坑。

第一个坑:链接期内存耗尽。LLVM的核心库是几个体积巨大的静态库,链接clangopt时内存消耗特别高。如果Configure成功,但Ninja在链接阶段被系统杀掉,最常见原因就是并行任务太多。解决办法很简单:减少-j参数,或者干脆串行只链接一个目标。另一个技巧是使用lld作为链接器来链接LLVM自身,lld的内存占用比系统默认的GNU ld小很多。启用方法是在CMake时加一条-DLLVM_USE_LINKER=lld,前提是你已经构建出了lld,或者系统里装了ld.lld

第二个坑:Python脚本报错。llvm-project里有些测试和构建步骤依赖Python 3。如果服务器上默认的python指向Python 2,CMake测试阶段就会报错。建议在环境中显式设置PYTHON_EXECUTABLE指向Python 3解释器。

第三个坑:缺少zlib和libxml2。大多数发行版可以通过包管理器安装。比如在Debian/Ubuntu下执行sudo apt install zlib1g-dev libxml2-dev libncurses-dev就能解决大部分可选依赖问题。虽然不装也能构建出核心工具,但有些功能(比如源码级调试信息解析)会受到影响。

构建完成之后,所有生成的可执行文件都在build/bin/目录下。为了使用方便,可以把这一目录加到PATH环境变量里。另外,build/里还会生成build/compile_commands.json,如果你的编辑器支持clangdcompile_commands.json索引,直接用它来导航LLVM源码会非常舒服。

3. 源码导航:如何高效阅读llvm-project而不迷失

3.1 顶层目录与模块依赖关系

拿到底层源码后,最怕的就是像看普通开源项目那样顺着一个目录从头看,结果很快丢失上下文。LLVM的源码量非常大,核心llvm/目录下的.cpp.h加起来超过两万份,没有目标地看会非常低效。我的经验是:先找到“入口”,再沿着一个具体功能走一遍调用链。

先说说llvm/目录的内部结构:

  • llvm/include/llvm/IR/:IR的数据结构定义,包括Module,Function,BasicBlock,Instruction等。
  • llvm/lib/IR/:IR数据结构的实现以及核心Verifier。
  • llvm/lib/Passes/:Pass管理器、标准Pass构建逻辑。
  • llvm/lib/Transforms/:各种优化Pass实现,比如Scalar,IPO,Vectorize
  • llvm/lib/CodeGen/:目标无关的代码生成;每个具体后端又有llvm/lib/Target/<TargetName>/
  • llvm/lib/Support/:基础工具库,字符串、文件系统、命令行选项等。
  • llvm/tools/opt/opt可执行文件的主要入口。

如果你的目标是想理解“Pass是怎么跑起来的”,那最佳的起点就是llvm/tools/opt/opt.cpp。从main函数往下追,看它调用了PassBuilderFunctionAnalysisManagerModuleAnalysisManager,这就是整套优化管线的关键脉络。

3.2 一个优化Pass从注册到执行的调用链

现代LLVM的Pass有两种经典类型:ModulePassFunctionPass(在new PM下,概念变成了对应AnalysisManager中的不同Pass单元,但基本思路一致)。一个Pass在代码里通常这样声明:

class MyPass : public PassInfoMixin<MyPass> { public: PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM); };

要让opt能通过旗标调用它,需要在PassBuilder的registerFunctionAnalysesregisterPipelineParsingCallback里注册。

理解这条调用链,关键在“注册”和“执行”的分离。Registry机制让Pass变成一个可插拔的组件:编译时注册进PassRegistry;运行时opt -passes=my-pass通过字符串名称查找到对应Pass对象。如果我想在opt中加入自己的Pass,就必须修改PassBuilder的注册回调,而不是直接在opt.cpp里加代码。这里面涉及的分析依赖,比如LoopAnalysisDominatorTreeAnalysis,也都是在AnalysisManager中按需创建的。理解了这一整套抽象,你去看llvm/lib/Transforms/里的所有Pass就不会觉得凌乱了。

3.3 用Clang命令行快速观察IR与优化效果

读源码比不过实际跑一遍。我经常用下面的命令来观察IR生成和优化效果。

比如一个简单的C文件:

// test.c int foo(int x) { return x * 8; }

先生成未优化的IR:

clang -S -emit-llvm -O0 test.c -o test-O0.ll

再生成优化后的IR:

clang -S -emit-llvm -O2 test.c -o test-O2.ll

对比两份文件,会看到O2版本直接将mul指令变成了shl,这就是编译器前端和优化器协作的直观体现。如果想进一步看到特定Pass执行后的IR快照,可以用opt

opt -S -passes=mem2reg test-O0.ll -o test-mem2reg.ll

这种工具组合是理解LLVM IR的最佳入口。你会发现Clang前端生成的IR带有大量allocaload/store指令,这是为了保留程序语义;而mem2reg这样的Pass会把这些局部变量提升为SSA值,让后续的分析和优化更容易进行。写字Pass时,我通常也是先生成一个小的IR文本文件,再使用opt反复调试,确认修改符合预期后再合并到工程里。

4. 基于llvm-project做自定义工具:写一个真实可跑的Pass

4.1 工程结构与新增Pass的代码轮廓

打开llvm-project根目录,大多数人会拿llvm/lib/Transforms/InstCombine/InstCombine.cpp当作范例。但如果只是想新增一个实验性的独立Pass,更合适的方式是放在一个独立的目录里,比如llvm/lib/Transforms/HelloWorld/。一个最小可用的Pass长这样:

#include "llvm/IR/Function.h" #include "llvm/IR/PassManager.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Support/raw_ostream.h" using namespace llvm; namespace { class HelloWorldPass : public PassInfoMixin<HelloWorldPass> { public: PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { errs() << "Hello from: " << F.getName() << "\n"; return PreservedAnalyses::all(); } }; } // namespace llvm::PassPluginLibraryInfo getHelloWorldPluginInfo() { return {LLVM_PLUGIN_API_VERSION, "HelloWorld", LLVM_VERSION_STRING, [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "hello-world") { FPM.addPass(HelloWorldPass()); return true; } return false; }); }}; } extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return getHelloWorldPluginInfo(); }

注意,这里使用了“插件Pass”的方式,它最大的好处是不需要重新编译整个LLVM。只要构建出来的opt开启了插件支持(默认就是开启的),就可以用-load-pass-plugin来加载。这对于前期学习来说是最高效的路径。

4.2 在LLVM的CMake体系中注册新Pass

如果你想把这个Pass作为内置Pass放进LLVM源码体系(和那些官方Pass并列),需要在llvm/lib/Transforms/CMakeLists.txt里新增一个目录:

add_subdirectory(HelloWorld)

并在HelloWorld/CMakeLists.txt中写:

add_llvm_component_library(LLVMHelloWorld HelloWorld.cpp LINK_COMPONENTS Core Support )

这种方式的优点是Pass能直接通过opt -passes=hello-world调用,不需要每次-load-pass-plugin,但缺点是你必须重新编译LLVM。如果你的机器性能一般,建议先写插件Pass,成熟后再考虑融入源码树。

4.3 用opt加载Pass并编写lit测试

自定义Pass跑通的最快路径如下:

  1. 写一个测试用IR文件test.ll
define i32 @main() { %a = add i32 1, 2 ret i32 %a }
  1. 编译插件:
clang++ -fPIC -shared -I<path-to-llvm-project>/llvm/include HelloWorld.cpp -o libHelloWorld.so

如果你的LLVM不是通过标准安装而是从源码构建的,还需要加上-I<build>/include,因为一些由llvm-tblgen生成的*.inc头文件都在构建目录里。

  1. 运行:
opt -load-pass-plugin=./libHelloWorld.so -passes=hello-world test.ll -disable-output

如果看到Hello from: main,说明整个编译-注册-执行链路是通的。

之后可以给Pass写官方风格的lit测试。在llvm/test/Transforms/HelloWorld/下新建一个hello-world.ll

; RUN: opt -load-pass-plugin=<path>/libHelloWorld.so -passes=hello-world -disable-output %s | FileCheck %s ; CHECK: Hello from: main define i32 @main() { ret i32 0 }

然后运行ninja check-llvm,或者用llvm-lit单独测试。lit测试框架会自动拼接环境变量,但是插件路径通常需要写成绝对路径,否则不同环境上会找不到.so文件。

4.4 进阶:从Pass拿到更细粒度的程序信息

一个只打印函数名的Pass演示意义大于实际价值。实际开发中,如果你要在Pass里做分析,最常用的API是:

auto &DT = AM.getResult<DominatorTreeAnalysis>(F); auto &LI = AM.getAnalysis<LoopAnalysis>(F);

通过依赖分析结果,可以判断一个循环是否可向量化,一条指令的操作数是否来自外部内存,等等。这些都是优化Pass的核心内容。我建议在读过LoopInfoScalarEvolution的源码后再开始写复杂的Pass,否则很容易做出“一看很合理,一跑就崩”的Pass。

写Pass时的最大心得:先想清楚你要在IR层做变换还是仅做分析。如果做变换,一定要处理好PreservedAnalyses的返回值。如果你复用了某些分析结果但没有在返回值里声明保留,Pass管理器会认为所有分析均已失效,导致后续Pass重新分析,性能大幅下降。反过来,如果你声称保存了实际被改变的分析,结果并不会自动更新,就可能触发断言错误。

5. 版本演进与社区协作:跟进llvm-project的日常实践

5.1 main分支为何值得跟,但生产环境必须锁release

LLVM的迭代节奏非常快,社区几乎每天都有几十个提交。如果你想使用最新指令集特性、新Pass策略,main分支毫无疑问是信息源。但对于稳定产出来说,main分支的风险也很明确:可能引入未完全修复的回归,可能改变LLVM IR的某些版本兼容性,也可能让第三方工具(比如你的自定义Pass)突然无法编译。我在经历过一次由于FunctionAnalysisManager接口改动导致整个Pass模块重写之后,就形成了“开发环境用main跟踪、生产环境锁定期release”的模式。

llvm-project的发布策略里,通常每年发布两次major release,例如18.x和19.x。release分支的版本号在CMake配置时体现为LLVM_VERSION_MAJOR。锁定时建议连patch版本一起记录,因为你构建出来的二进制取决于具体版本,不能只用“llvm 18”来归档。

5.2 Monorepo带来的git操作变化

llvm-project迁移到monorepo之前,Clang和LLVM属于不同仓库,跨仓库同步提交是一件痛苦的事情。现在所有子项目放在一个仓库里,日常操作简化了很多。比如git log --oneline -10能看到涉及Clang和LLD的提交交错在一起,而查找某个特定组件的变更可以用:

git log --oneline -- clang/lib/CodeGen lld/ELF

另外,monorepo也让我们能用单个git clone --filter=blob:none来减少下载量,这在网络环境一般时特别有用。需要注意的是,由于仓库体积很大,一次git pull --rebase可能会拉取大量对象,如果长时间没更新,建议用git fetch --shallow-since=等浅历史方式来控制数据量。

5.3 如何提交Patch与应对代码评审

如果你打算向社区贡献代码,需要了解LLVM的代码评审工作流。它使用Phabricator(目前正逐步迁移到GitHub Pull Requests)。典型的流程是:

git checkout -b my-patch # 修改代码... clang-format --style=LLVM <changed-files> git commit git diff --stat # 检查修改范围

提交信息需要遵循规范,一般包含[Clang][llvm]等前缀。社区非常看重测试覆盖,所以新增Pass时必须附带lit测试。提交到Phabricator后,会有机器人运行预提交测试,开发者们会在Review里逐行给意见。初看起来这套流程有点重,但对于基础设施项目而言,这是保障几十个后端、数百个贡献者稳定协作的必要方式。

在贡献过程中我个人的经验是:看代码风格时不要猜,直接用git clang-format;提交前务必跑相关测试,比如修改的是llvm/lib/Transforms/,至少要把check-llvm-transforms跑一遍,而不是只跑自己新增的测试。一次测试没覆盖全的Patch被打回重审是常见的事。

还有一点值得分享:正因为是monorepo,提交时极容易混入无关修改。我在一次提交里因为改了clang/lib/Driver的同时顺手调整了lld/ELF/CMakeLists.txt的换行,结果评审者要求拆成两个独立提交。所以保持每个Patch聚焦单一问题,是参与这个社区的基本礼仪。

写在最后的一点经验

在我自己使用llvm-project从入门到能动手做Pass的这段时间里,最大的体会是:不要惧怕项目体量,但要学会“贴着具体场景读源码”。只看目录结构容易迷失,只跑工具不看代码又得不到深入理解。最好的方式是从一条简单的IR变换开始,比如写一个把mul替换成shl的Pass,走一遍注册、编译、执行、测试的完整链路,然后再逐渐扩展到循环分析和代码生成。这比从头到尾读一遍LLVM Programmer’s Manual要有效得多。如果你也正好卡在第一次构建或第一次写Pass的过程中,希望这篇文章能帮你少浪费几次OOM时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 15:55:26

通达信分时资金进出副图指标公式详解:量价净额法实战

简介&#xff1a;适用于通达信软件用户的分时图资金流向分析指标源码&#xff0c;专门用于在分时图上观察机构、主力、散户等不同资金级别的实时买卖动态&#xff0c;便于短线与盯盘投资者快速判断资金异动。压缩包内仅含1个doc格式文档&#xff0c;体积168KB&#xff0c;文档提…

作者头像 李华
网站建设 2026/9/19 15:55:13

SPAD值到叶绿素含量的前馈神经网络建模

简介&#xff1a;本资源是一篇发表于《光谱学与光谱分析》&#xff08;2015年9月&#xff0c;第35卷第9期&#xff09;的核心学术论文&#xff0c;面向农业信息化、植物生理监测、智能传感与深度学习交叉领域的科研人员、研究生及农业技术开发者。针对传统SPAD法因叶片厚度、水…

作者头像 李华
网站建设 2026/9/19 15:54:45

基于Hadoop的智慧社区内网平台:从数据采集到可视化大屏

简介&#xff1a;这份资源是一篇基于Hadoop、Python与数据可视化技术构建智慧社区内网平台的毕业论文&#xff0c;面向计算机相关专业学生及需要完成类似课题的开发人员&#xff0c;帮助解决社区管理信息化与智能化设计中的选题、架构与实现难题。压缩包内仅含1个docx文档&…

作者头像 李华
网站建设 2026/9/19 15:52:55

从零构建智能体:LangChain与LangGraph实战入门指南

最近好几个做后端和算法的朋友都在问我 Agent 开发到底怎么入门&#xff0c;说 LangChain 文档翻了好几遍、示例代码也能跑&#xff0c;但一动手做自己的智能体还是发懵。这篇笔记是我自己从零啃 Agent 的踩坑记录&#xff0c;核心目标就一个&#xff1a;把“Agent 到底是什么、…

作者头像 李华
网站建设 2026/9/19 15:52:21

基于CNN的语义SLAM实现:动态点剔除与八叉树地图构建

简介&#xff1a;这份PDF文档包含发表于《科学技术与工程》2019年第19卷第9期的学术论文《基于卷积神经网络的语义同时定位以及地图构建方法》。面向智能车辆、自动驾驶与视觉SLAM方向的研究者&#xff0c;论文提出一种融合CNN语义分割的四线程双目SLAM方案&#xff0c;用以解决…

作者头像 李华
网站建设 2026/9/19 15:51:24

N_m3u8DL-RE 流媒体下载实战:5 个任务把 m3u8 流变成本地 mp4

N_m3u8DL-RE 流媒体下载实战&#xff1a;5 个任务把 m3u8 流变成本地 mp4 【免费下载链接】N_m3u8DL-RE Cross-Platform, modern and powerful stream downloader for MPD/M3U8/ISM. English/简体中文/繁體中文. 项目地址: https://gitcode.com/GitHub_Trending/nm3/N_m3u8D…

作者头像 李华