llvm ir到机器码是七步流水线:1. ir优化;2. 指令选择(ir→目标指令,基于selectiondag);3. 生成machine ir(含虚拟寄存器);4. 机器指令优化与调度;5. 寄存器分配(虚拟→物理寄存器);6. 后ra处理;7. mc指令发射→二进制。

LLVM IR 到机器码的核心流程是七步流水线
不是“一步到位”,也不是“黑盒转换”。从 define i32 @foo(i32 %x) 这样的 IR 函数开始,到最终生成 x86-64 的二进制机器码,LLVM 后端走的是明确分层的七阶段流水线。每一步都产出更贴近硬件的中间表示,且任意阶段都可能因目标架构或优化策略而插入定制逻辑。
这七个阶段不是理论模型,而是真实存在于 llvm/lib/CodeGen 中的 Pass 执行顺序,对应源码里 addPass 调用链。你调试一个后端崩溃(比如 llvm::SelectionDAG::Select 段错误),基本就卡在这条流水线上某一点。
指令选择(Instruction Selection)把 IR 映射成目标指令
这一步不生成汇编,也不分配寄存器,只做“语义对齐”:把 add i32 %a, %b 这类平台无关的 IR 指令,替换成目标架构能理解的等价操作,比如 x86 上的 ADD32rr、ARM 上的 ADDWrr、R600 GPU 上的 V_ADD_I32。
- 核心数据结构是
SelectionDAG,节点是SDNode,边表达数据依赖和控制依赖 - 实际映射靠
TargetLowering和ISelDAGToDAG完成,前者定义目标平台的合法类型和操作,后者做模式匹配(类似正则匹配 DAG 子图) - 常见坑:IR 中未 legal 的操作(如 128 位整数除法在 x86 上不原生支持)会先被
Legalize拆解,再进入选择;否则直接 crash 或 fallback 到软实现 - 如果你看到调用栈里出现
visitSDIV或LowerFormalArguments,说明正卡在这步的 visit 或 lowering 阶段
寄存器分配(Register Allocation)决定变量放哪颗物理寄存器
前面生成的指令仍用虚拟寄存器(%vreg17 这类名字),而 CPU 只认 %rax、%r15 这种物理寄存器。这一步就是把几百个虚拟寄存器“挤”进几十个物理寄存器里,冲突时还得往栈上 spill。
- LLVM 默认用
RegAllocGreedy,基于图着色思想但做了大量启发式剪枝,速度比传统 Chaitin 快得多 - 关键输入是
MachineFunction和其中的MachineInstr序列,输出是每个MachineOperand的PhysReg字段被填满 - 容易踩的坑:
spill过多会导致性能骤降,而原因常是 LiveRange 分析不准(比如没正确处理PHI节点的跨块活跃性)或目标描述里getReservedRegs()漏了某些 ABI 保留寄存器 - 崩溃如
llvm::RegAllocGreedy::allocateRegisters,大概率是 LiveInterval 构建异常或寄存器类(TargetRegisterClass)配置错
代码发射(Code Emission)把 MachineInstr 翻译成二进制
这是最后一道关卡:把已经调度好、寄存器已分配完毕的 MachineInstr(例如 ADD32rr %rax, %rbx, %rcx),转成真正的字节序列(如 0x48 0x01 0xd8),再封装进 ELF 或 Mach-O。
- 核心是
MCInst—— 一种轻量、目标无关的机器指令抽象,它不含寄存器名,只有操作码编号和操作数类型(MCOperand::createReg(Reg)) - 翻译由
MCCodeEmitter完成,每个后端必须提供;它读MCInst,查Info->getEncodingValue(),拼出字节 - 真正写入文件前,还要过
MCAsmStreamer(生成 .s)或MCELFObjectTargetWriter(生成 .o),这两者对重定位、符号表、section 属性的处理差异极大 - 如果生成的二进制跑起来 segfault,但
llc -march=x86-64 -filetype=asm看汇编没问题,问题几乎一定出在MCCodeEmitter或重定位补丁逻辑里
整个流水线里最易被忽略的,是各阶段之间“表示切换”的隐式成本:从 IR 到 SelectionDAG 是树→图,DAG 到 MachineInstr 是图→线性指令流,MachineInstr 到 MCInst 是语义→编码。每次切换都丢失一部分信息,也引入一层新的约束。调试时若只盯着某一层看,很容易误判问题归属。











