尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
llvm-project核心解析:掌握IR与Pass,构建自定义编译器生态
干编译器这行llvm-project 大概是绕不开的一个巨型仓库也是很多开发者repo清单里的“既想碰又不敢碰”的存在。我第一次拉取它的时候看到根目录里密密麻麻的工程文件夹脑子里只有一个念头这哪儿是“一个编译器”这分明是一套可以自己造编译器的“工业母机”。如果你是想搞懂现代编译器的整体架构或者想给自己的语言、框架、AI算子直接吃到几十个CPU/GPU后端优化红利这个项目几乎是必经之路。这篇内容我尽量用实操角度讲清楚它到底是什么、怎么把它跑起来、以及里面哪些东西值得你花时间去研究。1. 初识llvm-project先搞清这个工程里到底有什么1.1 它不是“一个编译器”而是“一整套编译器生态”我在早期阶段踩过最大的认知误区是把llvm-project理解成“LLVMClang的代码合集”。实际上这套东西的边界宽得多除了核心的中间表示IR和优化器它还包含了C/C前端Clang、链接器LLD、调试器LLDB、标准库实现libc、运行时库compiler-rt、循环优化工具Polly甚至还有现在AI编译器圈子里非常吃香的MLIR基础设施。它的诞生脉络简单说就是早年LLVM只是一个研究性质的编译器基础设施团队把“前端解析、中间优化、后端生成”三个环节彻底解耦后来苹果看中这个架构拉上Chris Lattner搞出了基于LLVM的Clang编译器慢慢替代掉了GCC在自家生态里的位置。现在这个仓库发展成了一个标准的monorepo也就是把所有相关子项目放在同一个仓库里做版本管理。对新手来说这种大仓库有两个直观感受一是clone体积很大动辄几个GB网络不好真的要等到怀疑人生二是头文件之间的依赖链非常深如果直接跑“apt install llvm”去用其实很难感受到这个项目真正的设计精髓因为预编译包把很多东西都打包封死了。1.2 仓库里的关键模块各自负责什么llvm-project在顶层目录下划分得非常清楚。我最常用的目录大概就是下面这些目录名作用我的使用频率llvm核心基础设施IR定义、优化Pass、目标后端、MC层等极高几乎每天都要碰clangC/C/Objective-C前端负责把源码变成AST再变成IR高日常编译都靠它lld高性能链接器支持ELF、Mach-O、COFF等格式中做工具链会用到lldb调试器和LLVM的底层接口结合紧密中调试复杂crash会用mlir多级IR基础设施适合做AI编译器、硬件加速器工具链高最近研究最重的部分flangFortran前端和某些科学计算场景强相关低用到时再翻polly基于多面体模型的循环优化低但价值很高libcxx/libcxxabi/libunwindC标准库实现含ABI、异常栈展开等中主要看实现细节compiler-rt编译器运行时库比如sanitizer系列都在这高排查内存问题非常依赖bolt二进制级别的布局优化工具出性能问题时有用低这是一把手术刀这里面最值得注意的是mlir。我最早以为它是个“实验性玩具”但后来做AI编译相关的项目才意识到mlir把“高层领域抽象”和“底层硬件优化”结合的思路有多重要。如果没有mlir你从PyTorch导出模型到各种NPU/DSP上基本就是给每个硬件手写一遍做苦力活。1.3 为什么用monorepo做版本管理使用过传统多仓库方案的人心里清楚当一个项目拆成llvm、clang、clang-tools-extra、polly等多个独立仓库时日常提交和版本对齐就是一场灾难。今天clang的某个commit依赖llvm的另一个commit明天你release时就要逐个仓库去查依赖树。monorepo把这个痛点直接干掉了。一次commit把llvm、clang、lld的变动同时放进同一个仓库上游开发者的side-effect保持一致CI也能在同一个事务里跑完各个模块的测试。对使用者来说checkout同一个tag所有模块的代码版本天然对齐不会出现“clang是昨天的llvm是上周的然后IR接口对不上”这种低智错误。这种管理方式看似“只是把多个目录放在一起”实际是整个项目能保持几十个小组并行开发的核心制度保障。2. 学习llvm-project的最好起点IR和Pass优化链路2.1 LLVM IR的三种形态先看可读文本LLVM的核心抽象是IR也就是中间表示。IR不是一种形态而是三种内存表示编译器运行过程中IR以C对象图的形式存在于内存里。bitcode一种紧凑的二进制序列化格式后缀通常是.bc。可读文本也就是以.ll为后缀的汇编风格文本人能直接看懂。很多时候调试问题面对的是.bc这种二进制你会相当痛苦。所以我的习惯是随时用工具把二进制转成可读文本。比如clang -S -emit-llvm hello.c -o hello.ll这样hello.c就会被编译成hello.ll你能直观看到每一个函数是怎么被翻译成IR的。如果你手头已经有.bc文件也可以用llvm-dis把它反向恢复成.ll。IR的设计非常像“带类型的精简汇编”每个寄存器只能是单赋值变量以SSA形式存在显式使用load/store操作内存。这种设计让后续的Pass分析非常舒服——因为没有复杂的“别名”和“生命周期”纠缠每个def-use链都清楚得像是画在纸上的图。2.2 Pass机制从Legacy PM到New PMPass是LLVM优化的灵魂。一个Pass就是对IR做一趟分析和变换的小模块运行完一轮优化可能消除了一些死代码也可能把某段循环向量化了。早期LLVM用所谓Legacy PassManager每个Pass要继承某个Pass基类靠全局注册器去声明自己。这种设计在项目小的时候很灵活但后来Pass一多全局状态碰撞、依赖顺序脆弱的问题越来越明显。现在的新Pass管理器New PM改用PassBuilder和AnalysisManager这套体系每个Pass显式声明自己需要哪些Analysis由管理器统一调度缓存。这不仅仅是工程洁癖而是真实场景下的稳定性要求——编译器一天要跑上百万次优化任何顺序或缓存上的不一致都可能造成难以复现的bug。如果你打算做自定义优化一定要直接学新PM官方已经逐渐淘汰Legacy了没必要在旧体系上浪费时间。2.3 用opt单步观察优化效果想快速验证一个IR经过某个Pass后变成什么样子不需要启动整个工具链直接用opt就行。下面这个命令是查看hello.ll经过mem2reg提升内存操作到寄存器之后的结果opt -passesmem2reg hello.ll -S -o hello.mem2reg.ll-S表示输出可读文本不加就是输出bitcode。我用这个命令最多的场景是看某个Pass是否生效、是否引入了意料之外的指令变动。它比在Clang里加一堆优化flag精细得多。优化前后对比还能用diff工具做文本比较这种“眼见为实”的反馈是我认为新手理解LLVM优化链路最快的方式。3. 构建llvm-project给新手的一份避坑操作手册3.1 构建之前先说清楚环境要求很多人失败的根源不是不懂CMake而是太低估这次构建的资源消耗。我自己第一次构建llvm-project机器只有8GB内存还开了默认的Debug模式加全量targets结果直接把内存吃到swap整个机子卡到鼠标都动不了。所以在敲命令之前请先跟自己的机器对一下配置磁盘源码加构建目录建议预留至少50GB空闲空间目录放在SSD上。内存如果只是本地学习16GB起步之后要做全量测试32GB更安心。构建工具强烈建议用Ninja别用Unix Makefiles。Ninja对并行任务的调度更高效增量构建也快很多。目标架构如果只需要X86就不要贪心编译所有targe。这里有个关键认知Release模式构建产物更省内存和运行更快但如果要调试LLVM自身还是要开Debug加断言这属于“性能换可调试性”的经典取舍。3.2 从clone到构建完成完整跑一遍假设你从零开始在llvm-project根目录下推荐这样操作git clone https://github.com/llvm/llvm-project.git cd llvm-project mkdir build cd build cmake -G Ninja ../llvm \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld;mlir \ -DLLVM_TARGETS_TO_BUILDX86;AArch64;RISCV \ -DLLVM_ENABLE_ASSERTIONSON ninja该项目支持从源码构建出的构建系统放在独立的build目录这样不会污染源码树。上面这个命令中我选择了clang、lld、mlir三个常用的子项目target也控制在了三个避免全量编译地狱。如果只想做一个能跑的clang可以只写-DLLVM_ENABLE_PROJECTSclang。但如果要深入研究建议至少把lld也带进来因为链接环节是理解工具链完整流程的一环。构建时间取决于机器我曾在16核服务器上全量构建大约半小时到一小时笔记本上可能要熬一个晚上。ninja在构建过程中会通过多核并行吃满CPU这时最好不要同时开一堆重型应用。3.3 我踩过的三个构建坑第一没有安装ninja和cmake新版。很多Linux发行版自带的cmake版本太老LLVM的CMakeLists会直接报错。解决办法很简单装好cmake 3.20以上版本ninja用apt或brew都能装Windows下则建议用Visual Studio的组件。第二源码目录路径别带中文或空格。某些工具链在阶段生成文件时会对路径做字符串拼接一旦路径异构各种奇怪错误都会冒出来而且报错信息往往根本看不出和路径有关。第三不要把构建目录放在网络文件系统上。LLVM在构建过程中会创建大量临时文件网络磁盘的I/O延迟会被无限放大构建速度慢到离谱。我亲眼见过某位同事把build目录放在挂载盘上跑了一个下午还没编完。4. 用llvm-project做点实事编译流程、自定义Pass与前端接入4.1 从.c到可执行文件每一步都在发生什么很多人用gcc用了很久却不清楚一个源码文件变成可执行文件时内部到底经过哪些阶段。用LLVM工具链就能非常完整地拆开这个过程。先写一个简单的hello.c#include stdio.h int main() { printf(hello llvm\n); return 0; }CLang的完整编译链路大致是预处理、词法/语法分析、生成AST、生成IR、一系列Pass优化、指令选择、寄存器分配、生成目标汇编、汇编器转成目标文件、链接器链接成可执行文件。我们可以用命令把它们拆开看clang -E hello.c -o hello.i # 只做预处理展开宏和头文件 clang -S -emit-llvm hello.c -o hello.ll # 编译到LLVM IR文本 clang -c hello.c -o hello.o # 编译到目标文件 clang hello.o -o hello # 链接生成可执行文件这里最有意思的是hello.ll。你能看到printf这个名字是如何被保留成外部声明main函数的IR骨架又是如何搭建的。到了链接阶段如果不用clang驱动ldd也可以直接调用ld.lld来手动指定入口点、库路径虽然那种做法在真实项目里很少见但实验性质很强能让你理解“工具链驱动”和“底层工具”之间的边界。4.2 写一个最简单的自定义Pass让它真正跑起来纸上谈兵再多不如动手写一个Pass。我推荐从函数级Pass入手因为FunctionPass能对每个函数做遍历效果直观逻辑也不会太复杂。新建MyPass.cpp#include llvm/IR/Function.h #include llvm/IR/Instructions.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h #include llvm/Support/raw_ostream.h using namespace llvm; namespace { struct MyFunctionPass : public PassInfoMixinMyFunctionPass { PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { errs() Processing function: F.getName() \n; for (auto BB : F) for (auto I : BB) errs() I \n; return PreservedAnalyses::all(); } }; } // namespace extern C ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK llvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, MyFunctionPass, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name my-function-pass) { FPM.addPass(MyFunctionPass()); return true; } return false; }); }}; }这个Pass做得很简单遍历每个函数里的每条指令然后打印出来。它的价值不在于优化而在于让你跑通“插件注册 — opt加载 — 执行打印机”的全流程。构建这个插件的命令clang -shared -fPIC -stdc17 MyPass.cpp \ $(llvm-config --cxxflags --ldflags --libs) \ -o MyPass.so这里需要确保llvm-config是你自己构建出的那个版本而不是系统自带的旧版本。如果PATH不对直接指定路径例如build/bin/llvm-config。然后准备一个测试IR文件define i32 add(i32 %a, i32 %b) { entry: %sum add i32 %a, %b ret i32 %sum }保存为test.ll接着运行opt -load-pass-plugin./MyPass.so -passesmy-function-pass test.ll -S如果成功你会看到终端打印出“Processing function: add”并且逐条输出IR指令。到这里你的自定义Pass已经真实跑起来了。4.3 前端层改造除了Clang还有很多更高层的玩法很多人以为“用llvm-project”就等于“用Clang编译C/C”。实际上前端层能做的事情远超这个范围。你可以给某个语言写一个自己的前端把AST或高层中间表示降级到LLVM IR然后白嫖后面几百个Pass和几十个后端。比如你想发明一种新语言只需要实现前端翻译成IR后面的寄存器分配、指令调度、目标代码生成全都不需要自己操心。这种“只做上游不带下游”的开发模式正是LLVM能吸引那么多语言实现者的原因。MLIR则把这个理念又提升了一层它允许你定义自己的dialect即一套带有语义的IR方言然后通过dialect之间的转换逐层lower到LLVM IR。AI编译器里很常见的做法是框架导出计算图在MLIR里做算子融合、内存规划、循环变换最后降到LLVM的IR再交给普通优化器和后端。上面这一套流程的精髓是把“高层数据流/控制流知识”复用到底层调度中比直接从头做一套专用编译器省太多体力。5. 问题排查与效率工具把这些经验下沉到日常开发5.1 常见错误速查表我平时在群里帮人看LLVM相关报错发现有一批问题出现频率极高。整理成一个速查表方便你直接对照场景典型报错常见原因解决思路CMake配置The source directory does not contain a CMakeLists.txt源码路径写错指向了build目录或错误子目录确保cmake命令里的源码路径是llvm-project下的llvm目录编译阶段内存不足/卡死Debug模式全量target导致内存峰值过高改用Release收窄-DLLVM_TARGETS_TO_BUILD运行optUnable to load pass pluginPass插件ABI版本不匹配确认opt和插件由同一个LLVM版本构建至少API版本一致链接阶段undefined reference to llvm::...CMake里漏了对应LLVM组件库用llvm-config --libs补齐链接库运行时崩溃Assertion failedLLVM_ENABLE_ASSERTIONSON暴露了内部约束问题不要关断言来“碰运气”去查具体Assert位置构建缓慢增量编译像全量重来修改公共头文件导致大规模重编尽量少动公共IR头文件分离实验代码到独立组件命令找不到llvm-config: command not found环境变量PATH没包含build/binexport PATHbuild/bin:$PATH这张表是我在实际使用中最常翻的建议收藏。尤其是ABI版本不匹配真的很烦人每次升级LLVM版本都容易出现只能老老实实把opt和插件放同一套build里跑。5.2 调试LLVM自身的经验当你开始改动LLVM源码或者追查某个优化Pass的输出是否正确时普通print大法会显得很低效。我的做法是先在函数入口加errs()输出确认该Pass是否被执行如果找到了可疑代码区域再用gdb/lldb打断点查看IR对象的结构和值。另外LLVM社区自带一套基于lit的测试框架。你写的Pass如果希望形成长期回归保护可以在test目录里写一个.ll测试文件里面有RUN注释比如; RUN: opt -load-pass-plugin... -passesmy-function-pass %s -S | FileCheck %s ; CHECK: Processing function: add这样跑llvm-lit就能自动化验证Pass行为。这个过程对个人项目来说可能有点重但一旦做了后面回归效率提升是肉眼可见的。我发现很多开发者没有充分利用update_test_checks.py这类工具其实它可以根据当前工具的输出自动生成CHECK行省去手工编写了大把时间。这个习惯值得养成。5.3 我建议你收藏的几条习惯第一遇到“优化结果不对”的问题永远先看IR别急着看汇编。IR是前端和后端之间唯一确定的契约删除中间转换的变量往往能更快定位问题源头。第二搭建一个最小复现用例。虽然LLVM庞大的项目结构让人想一口气跑整套工具链但多数bug只需要一个很小.ll文件加一个opt命令即可复现。把问题缩小到单文件、单个Pass调试体验会大幅提升。第三构建时把LLVM_ENABLE_ASSERTIONS打开。有人觉得断言拖慢性能但在开发调试场景下位置准确的assert比任何日志都好用。它会在IR约束不满足的第一时间爆出来避免错误在后续几步被“掩盖”成更奇怪的现象。6. 最后一点私人心得llvm-project这个项目真正值钱的地方不是某个算法多精妙而是它把“如何组织一个超大规模的基础软件工程”这件事摆在了你面前。我学习它最大的体会是不要一上来就去读那本厚厚的编译器教科书先把工具链跑通再对照一个真实IR的变化去理解每个概念效率会高得多。建议你拿到这个项目后挑一个自己最常用的操作场景比如“把一段C代码编译成可执行文件”然后手动替换其中的一个Pass加一行打印。只要走通这个流程LLVM那些庞大复杂的源码树对你来说就不再是迷宫了它只是一套“你能随时动手改”的普通软件。这个项目值得你花上一整个周末去折腾。
RELATED

相关推荐

IsaacLab 安装总卡在 rsl-rl?3 种做法一步修好

IsaacLab 安装总卡在 rsl-rl?3 种做法一步修好

IsaacLab 安装总卡在 rsl-rl?3 种做法一步修好 【免费下载链接】IsaacLab Unified framework for robot learning with multi-physics/renderer support 项目地址: https://gitcode.com/GitHub_Trending/is/IsaacLab 装机器人强化学习框架 IsaacLab 时&#…

📅 2026/9/20 11:54:47
Obsidian 多端同步方案对比:从 WebDAV 到 Syncthing 的选型指南

Obsidian 多端同步方案对比:从 WebDAV 到 Syncthing 的选型指南

用 Obsidian 做笔记,最让人头大的从来不是双链怎么建、插件怎么配,而是怎么让手机、平板、公司电脑和家里的电脑上的笔记库保持一致。我一开始天真地以为把 Vault 扔进某个云盘就行,结果真正多端同步起来才发现,这里面全是门道。为…

📅 2026/9/20 11:54:47
Artificial Analysis 价目表:GLM 5.3 Flash 用 TaoToken 同一把 Key 核对

Artificial Analysis 价目表:GLM 5.3 Flash 用 TaoToken 同一把 Key 核对

/* 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 11:54:47
MORE NEWS

更多资讯

📰

BrewUI:给Homebrew装上可视化仪表盘,包管理不再依赖命令行

1. 这个项目到底解决了什么问题1.1 命令行很强大,但不是每个人都在享受它先聊个真实场景。用 macOS 做开发的朋友,几乎绕不开 Homebrew。装个 nginx 要brew install nginx,升级所有包要brew upgrade,想看看哪个软件占了多少磁盘空…

📰

Grafana Tempo 升级实战指南:从 2.x 迁移到 3.0 / 3.1 的破坏性变更与配置迁移

后端可观测性链路追踪 【免费下载链接】tempo Grafana Tempo is a high volume, minimal dependency distributed tracing backend. 项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo 点击查看 免费下载 本文面向自托管(self-managed&#…

📰

3DES加解密源码解析:密钥处理、ECB/CBC模式与PKCS7填充实战

简介:这份资源提供了一套完整的3DES加密解密源代码,包含C工程配置与可执行程序,面向信息安全初学者、密码学爱好者以及有对称加密开发需求的程序员,适合课程设计、毕业设计或日常自学。资源共11个文件,压缩包仅84KB&am…

📰

数据采集选型实战:API与全托管平台如何权衡?

做数据采集这件事,我从给客户写定制脚本一直做到带团队搭采集平台,已经好几年了。每年年初都会有人问同样的问题:到底是用现成的数据采集 API,还是买个全托管平台?2026年这个问题变得尤其难回答,因为 API 服…

📰

Windows 上安装 Claude Code 实战:环境配置与高频报错排查

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

📰

从HDFS迁移到对象存储:计算存储分离架构实践

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

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬