LLVM编译器架构详解:从IR到Pass的二次开发实战

发布时间:2026/9/20 20:11:46

LLVM编译器架构详解:从IR到Pass的二次开发实战 “LLVM项目”这个名字放在今天多少有点历史包袱。2000年Chris Lattner写博士论文时给它起名Low Level Virtual Machine本意是想做一个针对懒惰求值程序的虚拟机运行时。结果二十多年过去这个名字里带着“VM”的项目几乎没人真拿它当虚拟机用。真正的产品是编译器基础设施、是Clang、是LLVM IR、是后来覆盖GPU、机器学习、编译器教育各个角落的庞大生态。我最早接触它是因为iOS开发后来做工具链、做静态分析、甚至帮团队定制代码生成策略兜兜转转全绕不开这一套东西。这篇内容适合想认真了解llvm-project是什么、它内部是怎么协作的人也适合刚准备在工程里接入LLVM做二次开发的读者。它会讲清楚几个核心问题LLVM到底解决了什么IR在其中扮演什么角色前端和后端如何各司其职以及你自己怎么把这套庞大代码库跑起来、写出第一个能在IR上跑的pass。这篇文章不会只停留在概念讲解上还会把我实际构建和调试过程中踩过的坑一并写出来。1. 名字带Virtual Machine却早已不讲虚拟机LLVM到底在解决什么问题1.1 历史与名字的误会先把这个误会彻底解开。LLVM诞生于UIUCChris Lattner最初的博士课题是“面向懒惰编程语言的虚拟机”。按当时的设计高级语言编译成字节码然后在LLVM虚拟机里解释执行这跟Java虚拟机的理念很像。后来课题方向逐渐转向“编译器的编译时、链接时、运行时优化”研究重心放到了“如何用统一的中间表示来贯穿整个编译流程”。到2003年前后LLVM真正变成了一套底层代码生成与优化框架虚拟机这个概念被完全抛在身后但名字已经改不了了。这个历史误会的价值在于它影响了很多人的认知预期。刚接触LLVM的人总会下意识去找“虚拟机”找不到就在论坛里问“LLVM怎么跑起来”。其实正确的理解方式是LLVM是一条生产流水线输入是语言前端吐出来的中间码输出是目标平台的机器码中间流动的就是LLVM IR。1.2 拆掉传统编译器的墙三层架构传统编译器如GCC一直是“前端、优化、后端”三部分绑死在一起。前端把C语言变成GCC自己的GIMPLEGIMPLE的优化逻辑直接面向GCC后端的指令生成需求。这意味着如果你想支持一门新语言几乎要把编译器的后半部分全部重新写一遍如果你想适配一种新CPU架构也要把优化器连带调整一遍。GCC在历史上为了支撑多种语言和多种目标付出了极高的维护成本但整体耦合度依旧没降下来。LLVM从一开始就把“语言相关”和“机器相关”彻底分层前端负责把源码解析成抽象语法树再生成LLVM IR。中端只面对IR做各种优化pass完全不关心这套IR是从C来的还是从Rust来的。后端消费优化后的IR做指令选择、寄存器分配、指令调度最后生成目标平台的汇编或机器码。这个设计的直接收益是只要语言前端能把源码翻译成正确的IR后端和优化器这个庞大工程就能完全复用。Rust的rustc、Swift的编译前端、以及无数DSL工具链实际上都是把语言特有的部分控制在“前端”这一层其余优化和代码生成全部交给了LLVM的后半部。这也就是为什么过去十几年里新增一个CPU架构支持时LLVM团队不需要研究这门语言语言团队也不需要触碰机器码。职责边界这件事LLVM做得非常干净。2. 一切的枢纽LLVM IR和它的SSA设计2.1 为什么是SSA为什么是强类型LLVM IR是整个项目的中枢神经。它是一套强类型的、基于静态单赋值SSA形式的中间表示。SSA形式的基本约束是每个变量只能被赋值一次。如果一个变量的值可能来自多个控制流路径那就需要用Phi节点把多条路径的值汇聚起来。这个设计直接解决了传统编译器数据流分析中的痛点——普通非SSA形式下一个变量被赋值多次分析工具必须处理别名和定值链算法复杂又容易出错。而在SSA形式下每个值的定义点唯一优化pass做const propagation、dead code elimination时不需要跨过重重赋值障碍数据流变成了简单的def-use链。强类型规则也很关键。IR中的每个值都有明确的类型i32就是32位整数ptr就是指针float是单精度浮点[4 x i32]是长度为4的int数组。类型信息不仅让编译器做优化时能精确推断内存布局和运算语义更重要的是让IR本身具备可验证性。一个ir文件读进来llvm-as也好opt也好都会先做类型检查类型对不上直接拒绝处理。这比直接在汇编层做优化要安全太多——汇编里一个字节可以是指令也可以是数据而IR用类型把这个不确定性消除了。2.2 同一份IR的三副面孔新手最容易困惑的是IR到底长什么样其实它有三个存在形态但是表达的是完全等价的内容。内存态编译过程中IR作为C对象存在于内存中各种pass直接操作这些对象。文本态扩展名为.ll的文件人类可读的汇编风格文本便于调试和学习。二进制态扩展名为.bc的bitcode文件用llvm-as编码适合存储和传输。日常开发里我会用到文本态去排查问题。拿一个最简单的整型加法函数举例源码长这样int add(int a, int b) { return a b; }不加优化时编译器生成的IR简化后大致如下define i32 add(i32 %a, i32 %b) { entry: %sum add i32 %a, %b ret i32 %sum }define i32 add(i32 %a, i32 %b)意思是定义一个返回i32的函数函数名是add接受两个i32参数。函数体里%sum是SSA变量add i32 %a, %b执行加法后写入这个变量然后ret返回。信息非常直白甚至比x86汇编更接近代码意图。如果函数逻辑复杂一些出现分支和循环临时变量会变多Phi节点也会出现。刚开始看IR的读者一碰到Phi节点就发晕我的建议是先把Phi理解成“编译器用来表达多路径汇合值的特殊merge函数”就行深入优化再研究它的细节。2.3 看IR比看汇编重要得多很多做底层优化的人习惯直接分析汇编认为IR是中间产物所以不重要。但实践下来IR层面的优化空间和调试便利性远超汇编。因为IR保留了大量高级语义类型信息、函数调用关系、可读的变量名甚至部分源码级符号信息。汇编层面的很多优化在IR层已经完成了比如内联、常量折叠、循环变形。到后端再做的话信息已经损失算法复杂度高得多。举一个实际例子我在分析某段C模板元编程代码时用clang -O2 -S -emit-llvm把源码转成IR明显看到编译器把大量模板实例化产物折叠成了常量。这时去读汇编满眼是CPU分支和地址跳转很难看出“这其实已经是一个编译期常量了”。所以做性能分析、做pass开发、做IR层面的静态检查直接面向IR是最高效的方式。3. 从源码到IR到机器码Clang与后端如何接力3.1 Clang的真正价值不止于能编译提到LLVM的前端大家脑子里第一个蹦出来的是Clang。但Clang覆盖的不只是“把C/C代码变成可执行文件”这一件事它还是一整套C、C、Objective-C的语法和语义分析框架。Clang把源码解析成AST之后供语法检查、代码补全、格式化、静态分析等多个工具复用。我经常用clang-check、clang-tidy这些衍生工具它们本质上都是基于同一份AST工作这也说明了Clang的前端设计并不仅仅服务于编译。从Clang到IR要经历两个阶段AST生成和IRLowering。AST层负责解析语法、处理模板、检查类型。IR Lowering把AST翻译成初始IR这一步还带着不少“笨拙”的结构——比如栈上分配变量、频繁的load/store。真正让初始IR变得高效的是后续的优化pass。3.2 优化pass如何提升IR质量拿上面的add函数来说Clang在-O0下生成的IR含有很多冗余操作。如果带-O2优化pass会把冗余的load/store消除把常量操作折叠结果函数体变得非常紧凑。我调试时最常用的命令组合是clang -O2 -S -emit-llvm add.c -o add.ll这条命令做的事是Clang前端把C源码解析成IR优化器按O2等级跑完所有pass再把最终IR以可读文本形式输出。查看输出的add.ll你会发现和-O0的版本差异巨大。这个过程可以帮助理解每个优化pass存在的意义。3.3 后端从IR到目标指令的博弈IR经过优化后进入后端。后端第一步是指令选择把IR里的每条逻辑运算映射到目标CPU的实际指令比如x86的lea、add、ARM的add.w。LLVM早期采用SelectionDAG来做这件事把IR先转为DAG结构再在DAG里做模式匹配。这套机制很成熟但复杂后来推出了更轻量、更适合快速编译的GlobalISel尤其对GPU和移动端的新架构支持更友好。指令选择之后是寄存器分配、指令调度和目标指令输出。这些环节决定最终生成的代码能跑多快、占用多少内存。LLVM的后端通过TableGen机制描述目标架构开发者只需要用TableGen写清楚指令集、寄存器、调用约定就能自动生成一部分指令选择代码。所以很多新兴CPU设计会优先支持LLVM后端而不是GCC因为LLVM的自动生成工具链让“增加新指令支持”的门槛降低了很多。我在后端层调试时经常用llcllc add.ll -o add.s它会将IR文件直接交给后端生成汇编。如果加了-march选项还能指定目标平台。后端问题排查中先用llc生成汇编再对照IR很快就能定位是指令选择错了还是寄存器分配错了。4. 为什么新语言不约而同选择LLVM生态背后的必然性4.1 站在LLVM肩膀上的选手名单粗略列一下当前生态里已经接入LLVM的语言和项目Rustrustc使用LLVM作为默认后端代码生成性能极其出色。Swift苹果自家的语言整个工具链围绕LLVM体系构建。JuliaJIT模式的科学计算语言核心依赖LLVM做即时编译。Zig直接把LLVM集成进自己的工具链追求对底层控制的极致。CUDA、ROCm、SYCLGPU计算生态的编译器前端很多都构建在LLVM之上。MLIR基于LLVM基础设施做的多层级IR框架正在成为AI编译器的事实标准。这不是偶然。一门新语言要走向工程可用最难的就是写一个稳定、高性能的代码生成后端。自行实现要考虑指令选择、寄存器分配、ABI兼容、调试信息哪怕一个很小众的CPU架构后端工程量都能让整个团队崩溃。而LLVM让“新语言获得所有平台后端”这件事变成了“只要有一个Clang前端把语法翻译成IR就行”。4.2 生态背后商业公司与社区的共同选择除了技术原因生态的形成还有商业推力。苹果需要掌控编译器的全链路来服务iOS和macOS于是把LLVM整合进了Xcode并持续投入研发。Google也很早押注LLVM在Android系统的工具链里用Clang替代了GCC后续又支持了MLIR。这些大型企业的投入让LLVM的工程质量和覆盖范围滚雪球一样膨胀。对个人开发者或小团队来说你几乎找不到第二个基础设施项目能提供如此全面的“免费代码生成器”所以选择LLVM是理性行为。理解这个生态对做技术选型很重要。假如团队要为公司自研领域特定语言最稳妥的路线就是“自制前端LLVM中后端”而不是从零去做全套编译器。我接触过的几个团队研发周期都因此压缩了一个数量级。5. 从零跑起来拉取源码、构建、写第一个自定义Pass5.1 获取代码与构建配置llvm-project是一个单仓库包含LLVM本体、Clang、libc、compiler-rt、lldb、MLIR等子项目。拉取时要注意仓库体积和依赖我建议用浅克隆加分支指定git clone --depth1 --branch llvmorg-18.1.8 https://github.com/llvm/llvm-project.git cd llvm-project构建LLVM的官方推荐工具是CMake加Ninja。构建配置直接决定你后面编译的时间和性能我通常这样设置cmake -G Ninja -S llvm -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_ASSERTIONSON \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_ENABLE_PROJECTSclang几点说明CMAKE_BUILD_TYPE选Release是因为Debug版的LLVM性能下降明显写pass验证时速度很慢。LLVM_ENABLE_ASSERTIONS打开能帮你在调试早期暴露内存和IR一致性问题发布时再关掉。LLVM_TARGETS_TO_BUILD只指定X86能显著缩短构建时间默认会编出一大堆CPU后端。LLVM_ENABLE_PROJECTS告诉CMake除了核心LLVM还要编哪些子项目。日常开发我会把clang一起编上便于用clang生成IR来喂给opt。构建命令ninja -C build首次全量构建X86和Clang在16核机器上大约需要20到40分钟。如果你本机内存小于16G可能会遇到链接器内存不足建议不要并行太多任务用ninja -j8之类限制并发。5.2 添加一个Pass并在IR上运行验证LLVM的pass体系这几年从Legacy PM全面转向了New Pass Manager。新版写法更清晰下面是一个简单的FunctionPass示例它遍历每个函数、统计二元算术指令的数量并打印到stderr#include llvm/IR/Function.h #include llvm/IR/Instructions.h #include llvm/Pass.h #include llvm/IR/PassManager.h #include llvm/Support/raw_ostream.h using namespace llvm; namespace { class CountBinaryOps : public PassInfoMixinCountBinaryOps { public: PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { unsigned Count 0; for (BasicBlock BB : F) { for (Instruction I : BB) { if (BinaryOperator *BO dyn_castBinaryOperator(I)) { (void)BO; Count; } } } errs() Function: F.getName() , binary_ops: Count \n; return PreservedAnalyses::all(); } }; } // namespace在CMakeLists里用add_llvm_pass_plugin把这个pass编成动态库是主流做法add_llvm_pass_plugin(CountBinaryOps mod.cpp)编译完成后先用clang生成一个测试IR文件build/bin/clang -O0 -emit-llvm -S test.c -o test.ll然后用opt加载新passbuild/bin/opt -load-pass-pluginbuild/lib/CountBinaryOps.so \ -passesfunction(CountBinaryOps) test.ll -disable-output输出会显示test.c中每个函数的二元指令数量。-disable-output是防止opt把处理结果又写回文件因为我们这个pass不修改IR所以保留原IR输出没有意义。我建议新手把整个过程跑通后再尝试往pass里加真正的优化逻辑比如把某个模式替换为更简单的指令。理解了“pass加载、pass执行、pass与IR交互”的闭环你才算真正摸到了LLVM二次开发的门。6. 已经在用LLVM当生产工具的人都在踩哪些坑6.1 构建期那些让人崩溃的错误构建LLVM最常见的问题是内存不足。链接LLVM自身时需要的物理内存动辄十几GB如果机器只有8G内存即使单线程构建也可能OOM。我的建议是优先用Ninja而不是MakeNinja并发调度更合理。其次可以给链接器加-flto之外的选项来降低内存峰值但复杂情况还是升级机器或用交换分区最实在。另一个高频坑是子模块或依赖不完整。有些人直接下载release tar包但一些子项目可能依赖版本不一致。尽量用官方GitHub仓库clone并指定release tag。构建前用cmake重新配置一次避免缓存导致项目开关不生效。6.2 写Pass时最容易踩的坑我早期写pass时最常犯的错误是修改IR后返回值写错。New Pass Manager中如果pass真正修改了函数你要返回PreservedAnalyses::none()如果完全没改可以返回all()。如果改了却返回all()后面的优化pass可能基于过期的分析结果继续工作产生的错误非常隐蔽。还有IR的合法性。新手手写IR或修改IR时很容易破坏SSA约束比如给一个已有def的变量重复赋值。LLVM的-verify机制会在pass运行前后检查IR一致性如果看到“Instruction does not dominate all its uses”这类错误基本可以断定是SSA规则被破坏了。我在处理这类报错时会先打开-debug-onlyisel这类模块级调试开关逐步缩小问题范围。但最有效的还是写少量测试用例用FileCheck去验证pass输出而不是每次都用肉眼盯大段IR。6.3 学习路线上的建议如果你准备认真使用llvm-project其实不用急着看全部源码。我的建议顺序是先写一个能跑通的pass知道pass长什么样子然后读官方文档里关于LLVM IR language reference的部分把类型系统、函数、指令记熟悉再回到Clang和opt的命令行参数理解不同优化级别对应哪些pass最后才去读具体优化pass的源码比如InstCombine、GVN。千万不要一上来就看TableGen和SelectionDAG那是深入后端的工程细节没有前置知识很容易劝退。调试过程中-print-after-all、-print-before-all这两个opt参数也很有用。它们会在每个pass执行前后打印IR全貌虽然输出量巨大但在追踪“哪一步把特定指令弄丢了”时它是唯一直观的手段。配合-filter-print-funcs某个函数名可以只看目标函数的pass执行过程大幅减少噪音。6.4 把LLVM用进真实工程时值得慢慢吃透的点最后再说一个生产实践中容易忽略的点bitcode的跨平台和跨版本兼容性。如果你把IR以bitcode形式存档或跨机器传输请留意LLVM版本和target triple的一致性。不同LLVM版本的IR语言版本可能变化bitcode文件可能会被新版工具拒绝。我们团队曾有一次升级LLVM 15到LLVM 18后发现之前存档的.bc文件全部无法加载最后只能重新编译上游代码重新导出IR。从那以后我们会在存档时同时保留.ll文本版本作为无法兼容时的兜底。这个习惯值得推荐给所有做长期工具链维护的人。LLVM这个项目技术细节深得能让人挖一辈子但核心思想并不难理解用一套统一的IR把前端和后端解耦让整个编译器生态共同进步。如果你正准备做工具链、写新语言、或者做静态分析我建议从本文提到的IR和pass入手先跑通一个最小闭环再逐步深入。等到你自己写出第一个pass亲手优化了一小段IR那种“这个庞然大物我居然也撬动了”的感觉会让你对整个项目产生完全不同的理解。
延伸阅读

更多相关文章

2026/9/20 20:11:46

LLVM项目实战指南:从IR到Pass,理解编译器的核心架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 20:06:46

LabVIEW五路同步采集实战:硬件选型、信号调理与实时架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 21:01:49

doocs/source-code-hunter:ArrayList 底层原理源码级剖析与面试指南

文档教程知识库 【免费下载链接】source-code-hunter 😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Red…

2026/9/20 21:01:49

TVBoxOSC 电视盒子播放管理指南:3 步让盒子开始看片

TVBoxOSC 电视盒子播放管理指南:3 步让盒子开始看片 【免费下载链接】TVBoxOSC TVBoxOSC - 一个基于第三方项目的代码库,用于电视盒子的控制和管理。 项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC 如果想在电视上播放收藏的片源&a…

2026/9/20 20:56:48

如何把微信聊天记录导出成文档:WeChatMsg 完整上手指南

如何把微信聊天记录导出成文档:WeChatMsg 完整上手指南 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeCh…

2026/9/20 0:04:49

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/20 0:04:49

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/20 0:04:49

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/20 0:04:49

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/20 4:54:47

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/20 5:01:23

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/20 5:09:33

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码