3. LLVM 核心架构
LLVM 的架构,说白了就是「三明治」——前端、中间层、后端。我刚开始接触 LLVM 时,觉得这分层有点多余。后来做跨平台编译,才明白这设计有多巧妙。
3.1 三层架构:各司其职
LLVM 把编译过程拆成三个独立阶段:
- 前端(Frontend):负责解析源代码,生成中间表示。比如 clang 就是 C/C++ 的前端。
- 中间表示(IR):LLVM 的核心语言,独立于源语言和目标机器。
- 后端(Backend):把 IR 翻译成目标机器码。
为什么要这么分?你想想看,如果我想支持一种新语言,只需要写个前端,把代码转成 IR 就行。后端完全不用动。反过来,要支持新硬件,也只需要写个后端。
核心思想:前端 + 后端 = N × M 种组合。没有 IR,就得写 N × M 个编译器。
我在项目中遇到过这种情况:团队要同时支持 ARM 和 x86 平台。我们只写了一个前端,IR 层做优化,后端分别生成两种架构的代码。开发量直接减半。
3.2 LLVM IR:编译器的「通用语言」
IR 是 LLVM 的灵魂。它有三种形式:
- 内存表示:编译过程中在内存里操作的数据结构
- 比特码(.bc):二进制格式,适合存储和传输
- 可读文本(.ll):人类可读的汇编风格代码
我个人习惯用 .ll 文件调试,因为能直接看到 IR 指令。举个例子:
; 一个简单的加法函数
define i32 @add(i32 %a, i32 %b) {
%result = add i32 %a, %b
ret i32 %result
}
这段 IR 定义了一个函数,接受两个 32 位整数,返回它们的和。你看,它不依赖任何高级语言特性,也不依赖任何 CPU 架构。
小技巧:用 clang -emit-llvm -S test.c 就能把 C 代码转成 .ll 文件。我经常用这招分析编译器做了什么优化。
3.3 LLVM Pass 机制:优化的「插件系统」
Pass 是 LLVM 里最让我着迷的设计。说白了,Pass 就是一个「优化插件」。每个 Pass 只做一件事,比如:
- 死代码消除:删掉永远不会执行的代码
- 常量传播:把常量表达式直接算好
- 循环展开:减少循环开销
Pass 分两种:
| 类型 | 特点 | 例子 |
|---|---|---|
| FunctionPass | 每次处理一个函数 | 内联优化 |
| ModulePass | 处理整个模块 | 全局变量优化 |
我曾经踩过一个坑:写了一个 FunctionPass,却想访问其他函数的变量。结果跑起来全是段错误。后来才明白,FunctionPass 只能看到当前函数。要跨函数操作,得用 ModulePass。
避坑指南:Pass 的执行顺序很重要。我曾经把「死代码消除」放在「常量传播」前面,结果很多死代码没被识别出来。正确的顺序是先做常量传播,再做死代码消除。
3.4 LLVM 工具链:三剑客
LLVM 提供了三个核心工具,我几乎每天都在用:
- clang:前端编译器。把 C/C++ 代码转成 IR 或目标文件。
- opt:IR 优化器。运行 Pass 对 IR 做优化。
- llc:后端编译器。把 IR 转成汇编或机器码。
它们的协作流程是这样的:
# 1. 用 clang 生成 IR
clang -emit-llvm -S test.c -o test.ll
# 2. 用 opt 做优化
opt -passes=mem2reg,instcombine test.ll -o test_opt.ll
# 3. 用 llc 生成汇编
llc test_opt.ll -o test.s
我个人习惯把这三步分开做。为什么?因为可以随时查看中间结果。如果最终代码有问题,我能快速定位是前端、优化还是后端出的问题。
经验之谈:调试时多用 opt -print-after-all。这个选项会打印每个 Pass 执行后的 IR。我曾经靠它找到了一个 Pass 把变量优化没了的问题。
3.5 知识体系总览
下面这张图展示了 LLVM 核心架构的全貌。我建议你把它存下来,以后看代码时对照着看。
嗯,这张图把三层架构和工具链的关系都串起来了。你写代码时,可以想想当前工作在哪个层,该用哪个工具。
个人建议:初学者先别急着写 Pass。先用 clang 和 llc 把 C 代码编译成不同平台的汇编,感受一下 IR 的「跨平台」特性。等熟悉了 IR 的语法,再动手写 Pass。
LLVM 的三层架构,说白了就是「一次编写,到处运行」的编译器实现。前端只管生成 IR,后端只管翻译 IR,中间层做优化。这种解耦设计,让 LLVM 成了编译器领域的「Linux」——几乎所有人都用它做基础平台。
可可精品资料库