news 2026/9/20 9:30:29

深入理解LLVM:从中间表示到编译器工具链的完整解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解LLVM:从中间表示到编译器工具链的完整解析

1. LLVM项目到底是个什么

先聊点实际的。很多人第一次接触LLVM,是在学编译原理的时候,或者是在用Xcode写iOS程序时,偶尔看到编译日志里蹦出来的Clang字样。但真正要弄懂llvm-project这个仓库的价值,需要换一个视角——它不是一个“编译器”,而是一整套“造编译器的工具”。

LLVM的全称是Low Level Virtual Machine,但今天这个名字已经名不副实了。它早已不是一台虚拟机,而是从底层指令表示、优化框架、代码生成,到上方各种语言前端、工具链、运行时库的完整生态。GitHub上那个llvm-project仓库,就是这一切的源代码集合,涵盖了Clang、LLDB、libc++、compiler-rt、MLIR等十几个子项目,数以百万行的C++代码,维护者遍布全球各大科技公司。

为什么说它是“造编译器的工具”而不是编译器本身?因为LLVM的核心设计理念是把编译器拆成三段:前端负责把源代码变成中间表示,中端在这个中间表示上做各种优化,后端再把中间表示翻译成目标平台的机器码。这个中间表示就是LLVM IR,而围绕IR做优化的那一整套pass框架,才是LLVM真正值钱的部分。Clang只是套在前端位置上的一个具体实现,你完全可以写一个自己的前端,把这个IR喂给LLVM的优化器和后端,产出一个完整可用的编译器。

这套架构的价值,类比过来就是:别人卖你一台组装好的电脑,LLVM卖你的是主板、电源标准、接口规范,外加一堆现成的配件。你不用从零开始设计电路,只需要做好自己的那块扩展卡,插上去就能工作。

这也是为什么现在几乎每一门新语言都在考虑基于LLVM——Rust用了它,Swift用了它,Julia的JIT也跟它有千丝万缕的关系。理解了llvm-project的定位,后面所有细节都好聊了。

2. 三段式架构与LLVM IR的设计智慧

2.1 为什么非要中间表示

早年的编译器都是“一条龙”结构,比如GCC,前端解析完语法、做完语义分析,生成一个树状结构,然后直接在这个树上做优化,最后生成汇编。这个树状结构跟语言强相关,跟目标机器也强相关,导致整个编译器非常难拆分。你想给这门语言加一个新目标平台的支持,几乎等于把优化器重写一遍。

LLVM的思路完全不同。它规定了一个统一的中间表示层,前端把源代码编译成IR,后端只认IR。这个IR既是前端输出的目标,也是后端读取的输入,前后端之间不需要知道对方的存在。于是语言开发者只需要关心怎么把语法翻译成IR,芯片厂商只需要关心怎么把IR映射到自己的指令集,两边都可以独立演进。

2.2 静态单赋值与无限寄存器

LLVM IR有一个核心性质叫静态单赋值形式,英文是Static Single Assignment,简称SSA。这个性质要求每个变量只能被赋值一次,代码里如果需要给一个变量赋多个值,就必须用新名字来表示。

看起来这是个很别扭的约束,但它给优化器带来了极大的便利。传统编译器里,一个变量可能在多个地方被修改,数据流分析需要花大力气追踪变量的所有读写点。而在SSA形式下,每个值的定义点是唯一的,使用点清清楚楚,很多优化算法可以直接在IR上做,复杂度大幅降低。

另一个隐藏设计是“无限寄存器”。LLVM IR里的变量是无限多的,用%1%2这样的编号区分。真正的物理寄存器是在后端做寄存器分配时,由算法决定哪些虚拟寄存器可以共用同一个物理寄存器、哪些时候需要溢出到内存。这个设计让中端优化完全不用考虑目标机器的寄存器数量限制,大大简化了优化器的编写。

2.3 三种内存里的存在形态

LLVM IR在编译过程中的不同阶段有不同形态。源码层面是人类可读的文本形式,后缀是.ll;经过汇编器处理后是二进制位码形式,后缀是.bc;而在编译器的内存中,它是一系列C++对象,模块里放着函数,函数里放着基本块,基本块里放着指令。

这三种形态各有用途。文本形式适合调试,你能直接打开看一个源文件被前端翻译成了什么样子;位码形式适合存储和传递,编译器在做链接时处理的大多是这种格式;内存对象形式则是优化器实际操作的对象,每一条指令对应一个Instruction类的实例。

这个设计给工具链带来了一个非常实用的特性:你可以把C语言代码翻译成IR文件,保存下来,然后在另一个时间点对它做优化、做分析,甚至把它链接到其他语言的IR上。跨语言优化在传统编译器里几乎不可能实现,在LLVM这里就是文件合并而已。

3. 工具链拆解:Clang、LLDB与libc++的协同

3.1 Clang的模块化设计

Clang是llvm-project里最广为人知的子项目,负责C、C++、Objective-C等语言的前端工作。它跟老牌编译器GCC的C++前端相比,最大的优势是模块化程度高,有清晰的AST(抽象语法树)接口,所以诞生了一大批基于Clang的工具。比如代码格式化工具clang-format,静态分析工具clang-tidy,重构工具clangd,它们的底层都是Clang的库。

Clang在编译速度上通常也优于GCC,原因是它把语法分析的粒度控制得当,并且很多处理是增量的。做iOS开发的同学应该感受过,同样的项目用Clang编译,比某些旧工具链要快不少。这里需要说明,不同项目、不同机器上数据差异很大,不能简单说Clang一定碾压GCC,但它的架构给工具生态带来的价值是无可争议的。

3.2 LLDB的调试体验

LLDB是LLVM项目的调试器,跟GDB相比,它的架构更干净,模块间的接口更明确,而且原生支持LLVM的调试信息格式。在macOS和iOS开发中,Xcode默认使用的就是LLDB,你在Xcode里断点、看变量、优雅地表达表达式求值,都是LLDB在做幕后工作。

LLDB的一大亮点是通过Python脚本扩展,你可以编写插件来自定义调试流程,比如格式化输出复杂对象、自动定位崩溃现场等等。对写底层代码的人来说,LLDB的表达式求值能力很强,在调试时直接调用函数、修改变量,体验比老牌调试器流畅不少。

3.3 libc++与compiler-rt

libc++是LLVM项目的C++标准库实现。这个库的设计目标是干净、性能好、严格的标准化,而且license友好。很多项目选择libc++而不是libstdc++,是因为它在一些边界行为上更符合标准,也更容易定制。在内嵌场景和跨平台编译里,libc++的使用非常普遍。

compiler-rt则提供了一些底层运行时支持。比如__asan相关的函数是AddressSanitizer(地址消毒器)运行时的一部分,这个工具能检测内存越界、使用已释放内存等内存错误,对C/C++开发者的帮助是巨大的。平时说的ASan就是compiler-rt里实现的众多sanitizer之一。

3.4 MLIR:LLVM延伸出的编译基础设施

MLIR(Multi-Level Intermediate Representation,多级中间表示)是LLVM项目近几年最重要的衍生子项目之一。如果说LLVM IR是面向“代码优化和机器码生成”的通用表示,那么MLIR更关注的是“在更高层次上表达计算图、算子、数据流”,尤其适合机器学习和AI加速器场景。

它的核心思想是多级IR,不同抽象级别的表示可以共存于同一个编译流程中,逐级lowering(降级),直到最终变成LLVM IR。像TensorFlow、PyTorch的高性能编译器,都会在中间阶段用到MLIR。这意味着llvm-project不只是服务传统编译器,它已经延伸到了深度学习编译和GPU编程领域。

4. 用LLVM实际能做什么

4.1 写一个领域专用语言的前端

如果你要设计一门DSL(领域专用语言),比如一门用于配置路由的脚本语言,或者一门用于描述数据处理管道的语言,你不需要从零实现代码生成。只需要写一个词法分析器和语法分析器,把源代码翻译成LLVM IR,接下来的事情都交给LLVM:优化、寄存器分配、指令选择、生成目标平台代码。

实际动手时,大多数DSL不会做成独立可执行文件,而是嵌入宿主语言中,通过JIT(即时编译)的方式在运行时执行。LLVM提供了一套ORC JIT框架,可以在程序运行过程中把IR编译成机器码,然后直接调用生成的函数指针。这意味着你的DSL代码可以享受接近原生代码的执行性能。

4.2 做自定义编译优化pass

LLVM的优化器采用pass机制,每个pass是一个独立的C++类,对IR进行一种特定变换或分析。内置的pass非常多,比如死代码消除、循环展开、函数内联、向量化等等。它们以流水线的方式依次执行。

如果你在开发某个领域应用,发现生成的代码存在特定的模式可以优化,可以写一个自定义pass。比如你的代码里大量出现对某个数学函数的调用,参数是常量,你就可以写一个pass在编译期把这些调用计算掉。再把pass加载到编译流程中,用专用的工具opt命令跑一下,就能把优化后的IR输出出来。

4.3 做代码静态分析工具

传统的静态分析工具大多基于语法树,分析的是代码的表层模式。基于LLVM做的分析工具可以直接在IR上运行,获得的信息更接近程序的真实语义。你可以写一个遍历IR的pass,统计每个函数的指令数、识别未使用的全局变量,或者实现一个自定义的警告规则。

这也解释了为什么很多代码检查工具的底层都依赖Clang。基于Clang做分析,可以拿到完整的语法树信息和控制流图,再结合一些数据流分析框架,能做出来的检查项非常丰富。llvm-project在这方面的积累是其他工具链很难超越的。

4.4 反编译与二进制分析

LLVM的后端可以把IR翻译成各平台的机器码,反过来,也可以加载二进制代码,经过反汇编、指令识别后提升为IR。这一点是很多二进制分析工具的基础。比如你用LLVM的MC框架写一个工具,可以把一段机器码反汇编成汇编,也可以尝试把汇编提升为IR,再通过优化器做一些简化,方便理解程序的逻辑。

这个领域的门槛比前面的高,但如果你想做一些程序分析、兼容层或者安全研究,LLVM的这套基础设施提供了相当强大的起点。

5. 源码结构与构建实践

5.1 仓库目录到底有什么

先看llvm-project仓库的根目录,第一层子目录包括clanglldlldbmlirflangcompiler-rtlibcxxlibcxxabilibunwind等等。有一个容易让人疑惑的地方是,LLVM自身的核心代码不在根目录,而是在llvm这个子目录里。这个llvm目录里才是优化器、IR定义、后端代码、各种库的源码。

如果你用的是GitHub镜像,还需要注意分支切换。主分支是main,但同时维护着多个发布分支,比如release/17.x等。想稳定构建,建议切到发布分支,而不是使用最新主分支,因为主分支每天都在变,构建出问题的概率更高。

5.2 硬件和依赖准备

构建LLVM是一个资源密集型任务。我建议至少准备16GB内存、8个以上的逻辑核心、50GB以上的磁盘空间。磁盘空间主要被中间文件和编译缓存占用,如果开启ccache可以减小重复构建的负担。

依赖方面,需要CMake、Ninja、Python,以及宿主系统的C++编译器。还有一个可选依赖是zlib,主要用于压缩位码文件。zstd也建议装一下,有些发行版对压缩支持有偏好,装了能让llvm避免一些cmake配置时的告警。

cmake和ninja的版本要够新,太老的版本可能不支持LLVM用到的一些特性。在x86 Linux上,用系统自带apt源通常足够。

5.3 配置与编译命令详解

以下是我常用的配置方式,在llvm-project源代码根目录下操作:

cmake -G Ninja -S llvm -B build -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TARGETS_TO_BUILD="X86;AArch64" -DLLVM_ENABLE_ASSERTIONS=ON

逐个解释参数的含义:

  • -G Ninja:指定使用Ninja作为构建工具。Ninja比make的并行调度更快,增量构建也更准确。
  • -S llvm:指定LLVM核心源码目录为源码根目录。这个参数必须指向llvm子目录,而不是仓库根目录。
  • -B build:指定构建输出目录为build。所有生成的中间文件都会放在这里。
  • -DCMAKE_BUILD_TYPE=Release:构建release版本,优化等级高,适合实际使用。
  • -DLLVM_ENABLE_PROJECTS="clang;lld":指定要构建的额外子项目。编译器工具链通常需要clang和lld。
  • -DLLVM_TARGETS_TO_BUILD="X86;AArch64":只生成x86和ARM架构的后端。如果不指定,默认会生成所有支持的后端,构建时间会多好几倍。
  • -DLLVM_ENABLE_ASSERTIONS=ON:启用断言。虽然断言会轻微影响性能,但在学习、调试工具链时非常有用,能帮你提前发现问题。

配置完成后,直接执行:

cmake --build build

如果只想构建一个特定的目标,比如只想编出clang,可以:

ninja -C build clang

5.4 验证构建产物

构建完成后,产物在build/bin目录下。验证方式很简单:

echo 'int main(){return 0;}' | build/bin/clang -x c - && ./a.out && echo "build ok"

你应该能看到build ok的输出。这说明clang能把C代码编译成可执行文件,并且执行成功。如果想验证C++也没有问题,把命令中的-x c改成-x c++即可。

6. 进阶实操:用LLVM API遍历IR

这个练手环节值得认真走一遍,它能让你真正体会到LLVM IR的结构。

6.1 准备输入IR

先准备一个简单的C代码文件,比如叫demo.c

int add(int a, int b) { return a + b; } int main() { return add(2, 3); }

使用clang把它翻译成文本形式的IR:

build/bin/clang -S -emit-llvm demo.c -o demo.ll

打开demo.ll,你能看到类似这样的内容:

define i32 @add(i32 %a, i32 %b) { entry: %add = add nsw i32 %a, %b ret i32 %add }

这里可以看到IR和汇编的相似之处,但它更抽象。i32是32位整数类型,%a%b是虚拟寄存器,add nsw是带无符号溢出语义的加法指令。

6.2 编写遍历IR的程序

下面这个示例程序的功能:读取一个LLVM位码文件(.bc),遍历其中每个函数的每个基本块,并打印所有指令的操作码。它演示的是LLVM C++ API的基本用法,即llvm::parseIRFile函数、ModuleFunctionBasicBlockInstruction这几种核心数据结构之间的组织关系。

#include "llvm/IR/Module.h" #include "llvm/IR/Function.h" #include "llvm/IR/BasicBlock.h" #include "llvm/IR/Instruction.h" #include "llvm/IR/LLVMContext.h" #include "llvm/IRReader/IRReader.h" #include "llvm/Support/SourceMgr.h" #include "llvm/Support/raw_ostream.h" #include <memory> using namespace llvm; int main(int argc, char **argv) { if (argc < 2) { errs() << "usage: " << argv[0] << " file.ll\n"; return 1; } LLVMContext Context; SMDiagnostic Err; std::unique_ptr<Module> M = parseIRFile(argv[1], Err, Context); if (!M) { Err.print(argv[0], errs()); return 1; } for (Function &F : *M) { outs() << "Function: " << F.getName() << "\n"; for (BasicBlock &BB : F) { outs() << " Block: " << BB.getName() << "\n"; for (Instruction &I : BB) { outs() << " " << I.getOpcodeName() << "\n"; } } } return 0; }

代码逻辑很直白,如果你是第一次接触LLVM API,可以把Module理解成一个源代码文件,Function是文件里的函数,BasicBlock是函数内部的基本块,Instruction是基本块里的每一条指令。嵌套的for循环就是逐层向下遍历。

要注意,parseIRFile既能读.ll文本文件,也能读.bc位码文件,它会自动识别格式。

6.3 编译并运行这个分析工具

把上面的代码保存为irdump.cpp,然后编译它,这一步需要链接LLVM的核心库:

build/bin/clang++ irdump.cpp -o irdump -std=c++17 -I build/include $(build/bin/llvm-config --libs core irreader support --system-libs 2>/dev/null)

这里使用了llvm-config工具来获取库的链接参数,这是LLVM开发中最常用的方式。它输出的--libs core irreader support参数会展开成一系列-lLLVMCore -lLLVMIRReader -lLLVMSupport等链接选项,--system-libs会把需要用到的系统库(比如curses、zlib等)也加上。

如果编译时提示找不到llvm-config命令,确认一下它的完整路径:build/bin/llvm-config

运行这个工具:

./irdump demo.ll

输出应该类似:

Function: add Block: entry add ret Function: main Block: entry call ret

这就对了。你的代码已经能够解析IR文件,并遍历其中的每一条指令了。基于这个骨架,你可以扩展出很多实用工具,比如统计指令比例、分析函数调用关系、检测潜在问题模式等。

7. 避坑指南与经验心得

7.1 构建阶段的坑

坑一:根目录和llvm子目录混淆。很多人第一次克隆完仓库直接在根目录执行cmake,结果飘红。必须明确,cmake的-S参数指向的是llvm子目录。我在配置时踩过这个坑,当时浪费了半小时排查,最后发现是路径问题。

坑二:内存不足导致链接失败。LLVM的很多库文件很大,链接器在链接阶段可能吃掉大量内存。建议给构建过程配置swap空间,或减少并行度。

ninja -C build -j 4 clang

如果内存比较紧张,可以把并行度调低,虽然慢一些,但至少能跑完。

坑三:断言不启用的隐性风险。我在写自定义pass时曾关闭断言调试,结果运行时出现了奇怪的段错误,追踪了一天也没有眉目。后来重新打开断言,编译一跑就弹出了详细的失败位置和调用栈。

7.2 开发阶段的坑

坑一:不要直接改LLVM核心库的API。LLVM的API在版本间变动很大,如果你的代码基于某个版本开发,升级仓库后大概率要调整。建议对外层代码做好封装,减少对具体API版本的依赖。

坑二:优先使用IRBuilder。写IR的构建逻辑时,手动创建指令对象太繁琐,而且容易出错。IRBuilder提供了一套高级接口,可以帮你自动处理很多指令创建的细节。比如创建加法指令的IR,用Builder.CreateAdd一行就完成,非常清晰。

坑三:irreader库里的parseIRFileparseIR不同。前者是顶级函数,支持文件路径,适合独立工具使用;后者是ParseIR函数,需要依赖于LLVMContext,更适合嵌入到编译器的模块里使用。

7.3 全链路验证技巧

拿到一个修改过的LLVM版本,想验证它是否真的改善了生成的代码质量,建议做三件事:先用opt跑一遍IR优化,观察优化前后IR的变化;再对比生成的目标机器代码,看指令数量是否减少或关键循环是否被向量化;最后一定要跑基准测试,不能只看指令数量,因为有时候为了省几条指令反而会造成流水线停顿。

这个流程做下来,你对LLVM的理解会深入很多,不再只是表面上的工具用户。

8. 给新手的实操路线图

如果你是一个从未接触过LLVM源码的开发者,想系统地入门llvm-project,我的建议分四步走,每一步都不长,但能积少成多。

第一步:用Clang实践前端功能,学会clang -S -emit-llvmclang -O2 -S等常用命令,能看懂IR文件的常见指令。这一步的目标是建立对IR的直觉。

第二步:学会用opt跑各种内置pass,观察不同优化选项对IR的影响。比如把-mem2reg跑在一个简单函数上,观察它是怎么把堆栈变量的存取转换成SSA寄存器的。

第三步:用LLVM API写一个简单的分析工具,比如上面那个遍历指令的工具。这一步的目标是理解IR在内存中的表示形式,体会到用C++操作IR的流程。

第四步:尝试给LLVM添加一个新的优化pass,哪怕它只是把某条恒定表达式折叠掉。你会经历pass注册、构建、加载、调试的完整流程,之后再看LLVM的方法论就会觉得豁然开朗。

我在学习Rust编译器、Swift编译器的实现原理时,同样使用了这四步走的方法。LLVM就像一个编译器领域的“游戏引擎”,学会操作它之后,进入任何语言实现的大门都会顺畅得多。记住,最好的学习材料永远是源码本身,动手改一行代码的价值远大于读三篇博客。

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

AgentWriter 拆 plan/write 长文管道,Base URL 填 TaoToken

/* 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 9:29:10

ComfyUI-Workflows-ZHO:16 个即插即用的 AI 绘图工作流合集

ComfyUI-Workflows-ZHO&#xff1a;16 个即插即用的 AI 绘图工作流合集 【免费下载链接】ComfyUI-Workflows-ZHO 我的 ComfyUI 工作流合集 | My ComfyUI workflows collection 项目地址: https://gitcode.com/GitHub_Trending/co/ComfyUI-Workflows-ZHO ComfyUI-Workflo…

作者头像 李华
网站建设 2026/9/20 9:28:22

羽毛球目标检测数据集实战:从YOLO训练到三格式转换

1. 一个人工智能项目&#xff0c;为什么非要从“数据集”讲起拿到这个标题的时候&#xff0c;我第一个反应是&#xff1a;这不就是又一份“标注好的目标检测数据集”吗&#xff1f;但仔细一看&#xff0c;2879张图、识别率84.4%、三格式全支持&#xff08;yolo、coco json、voc…

作者头像 李华
网站建设 2026/9/20 9:24:24

Claude Code官方安装脚本全解析:从零安装到权限配置

最近把主力终端工作流换成了 Claude Code&#xff0c;从安装到日常使用折腾了差不多一个礼拜。网上关于 Claude Code 的讨论很多&#xff0c;但大多停留在“一句话装完”的层面&#xff0c;真正把官方安装脚本、环境依赖、登录授权、权限设置、升级卸载这些环节讲透的内容不多。…

作者头像 李华