后端Pass简介——CallBrPrepare
CallBrPrepare
这个 Pass 在 IR 级别拆分 callbr
的关键边,并在每个间接目标块开头插入 llvm.callbr.landingpad
intrinsic,以便为后端生成时在各条跳转路径上正确地复制并映射返回值。
这个 Pass 不需要调参,但需要 DominatorTreeAnalysis
介入。
这里的“landingpad”有两种常见含义:
- 在异常处理(C++/LLVM EH)里的
landingpad
语句,用于描述 catch/finally 块的入口。 - 在这个
callbr
pass 里,landingpad 指的是用llvm.callbr.landingpad
intrinsic 人工插入的一个 SSA 值定义点,专门用来在每个间接跳转目标块开头产生 callbr 的返回值副本。
例子:
准备前:
entry:
%res = callbr i32 asm(...), label %def, [label %tgt1, label %tgt2]
def:
; ... 默认目标块 ...
tgt1:
; ... 间接跳转块1 ...
use %res
tgt2:
; ... 间接跳转块2 ...
use %res
经过 CallBrPrepare Pass 后,会加入 landingpad 作为缓冲点:
tgt1:
%res.lpad = call i32 @llvm.callbr.landingpad(i32 %res)
use %res.lpad
tgt2:
%res.lpad2 = call i32 @llvm.callbr.landingpad(i32 %res)
use %res.lpad2
那么为什么要这个 landingpad 呢?
- SSA 和寄存器分配的痛点:LLVM IR 是“单一赋值形式”(SSA),意思是每个值有唯一的定义点,并且所有用到它的地方都能追溯到那个定义。==但是实际到后端生成机器码时,需要把每个 SSA 值分配到实际的物理寄存器或者内存,而且寄存器数量有限,分配过程很复杂。==对于大多数情况,LLVM 可以自动安排拷贝/装载,因为控制流和数据流是清晰的。
- callbr 的特殊性:
callbr
是 LLVM 专门为**“带有间接跳转的内联汇编”**设计的。它类似这样:
%x = callbr i32 asm(...), label %normal, [label %tgt1, label %tgt2]
意思是执行完内联汇编后,可能跳到 normal
,也可能跳到 tgt1
或 tgt2
。关键是跳到 tgt1 或 tgt2 的“跳转”本身,是在用户写的汇编里做的,不是LLVM IR里的“br”做的。
这就有个问题:
- 正常的SSA值赋值和寄存器分配是在IR里能显式插入mov/copy的位置做的。
- 但对于 callbr,跳转本身在汇编代码里发生,LLVM 无法在“跳转”之后插入一条寄存器复制指令。
- 比如:你希望
%x
在tgt1
用到时,已经被装进某个寄存器,但实际上你没法保证在tgt1
开头有机会插入那条“把 callbr 返回值拷贝到合适寄存器”的指令,因为跳转发生点在汇编blob里。
所以,landingpad 的作用就是人为在每个目标块头部“重新定义一次 callbr 的返回值”。
你可以理解为: - 每个间接目标块,像“catch”一样,头上插一个“我在这里重新把callbr的返回值变成新的SSA名字”。
- 在 IR 里显式插入
llvm.callbr.landingpad
,这样LLVM的后端知道:“哦,我可以在这个点做寄存器装载/复制!” - 对于每个目标块,所有用到
%x
的地方都指向自己块里的%x.lpad
,而不是原始的%x
。
这样就能“各有独立的 SSA 定义和寄存器映射”:
比如,假设callbr
的返回值%x
,你现在有两个目标块tgt1
和tgt2
。 - 你在
tgt1
插入了%x1 = llvm.callbr.landingpad(%x)
- 在
tgt2
插入了%x2 = llvm.callbr.landingpad(%x)
这样后端在分配寄存器时,可以分别考虑%x1
和%x2
,比如 %x1
可以放到寄存器 eax%x2
可以放到寄存器 ebx
并且在tgt1
和tgt2
的头部,后端都可以安排一条合适的拷贝指令(比如 mov eax, … 或 mov ebx, …),因为 LLVM 现在有了可以插入代码的“入口点”。
如果不这样做,所有 use 都只能指向%x
,而且你没办法在所有路径上插入寄存器赋值的代码点(因为跳转点在用户汇编里,LLVM插不了拷贝指令!)。
一句话: “各有独立的 SSA 定义和寄存器映射”= 让每条跳转路径都可以拥有自己的“callbr返回值”名字,这样在每个目标块头都能安排一条自己的拷贝指令,后端寄存器分配可以独立做选择,从而避免SSA和寄存器分配的尴尬。
你可以想象这样一个对比,有和没有 landingpad:
entry: callbr -> tgt1/tgt2
tgt1: use %x // 你插不了mov/copy了!
tgt2: use %x // 同上
entry: callbr -> tgt1/tgt2
tgt1: %x1 = landingpad(%x) // 这里能插拷贝,后端能自由分配寄存器!
use %x1
tgt2: %x2 = landingpad(%x)
use %x2
希望这样能帮你感受到,landingpad 就像“每条路的第一个台阶”,让后端能自由把callbr的返回值安排进寄存器或内存,也能解决SSA唯一定义点的困境!
核心入口为:
PreservedAnalyses CallBrPreparePass::run(Function &Fn,
FunctionAnalysisManager &FAM) {
bool Changed = false;
SmallVector<CallBrInst *, 2> CBRs = FindCallBrs(Fn);
if (CBRs.empty())
return PreservedAnalyses::all();
auto &DT = FAM.getResult<DominatorTreeAnalysis>(Fn);
Changed |= SplitCriticalEdges(CBRs, DT);
Changed |= InsertIntrinsicCalls(CBRs, DT);
if (!Changed)
return PreservedAnalyses::all();
PreservedAnalyses PA;
PA.preserve<DominatorTreeAnalysis>();
return PA;
}
SplitCriticalEdges
InsertIntrinsicCalls
评论