CallBrPrepare

这个 Pass 在 IR 级别拆分 callbr 的关键边,并在每个间接目标块开头插入 llvm.callbr.landingpad intrinsic,以便为后端生成时在各条跳转路径上正确地复制并映射返回值。
这个 Pass 不需要调参,但需要 DominatorTreeAnalysis 介入。
这里的“landingpad”有两种常见含义:

  1. 在异常处理(C++/LLVM EH)里的 landingpad 语句,用于描述 catch/finally 块的入口。
  2. 在这个 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 呢?

  1. SSA 和寄存器分配的痛点:LLVM IR 是“单一赋值形式”(SSA),意思是每个值有唯一的定义点,并且所有用到它的地方都能追溯到那个定义。==但是实际到后端生成机器码时,需要把每个 SSA 值分配到实际的物理寄存器或者内存,而且寄存器数量有限,分配过程很复杂。==对于大多数情况,LLVM 可以自动安排拷贝/装载,因为控制流和数据流是清晰的。
  2. callbr 的特殊性:callbr 是 LLVM 专门为**“带有间接跳转的内联汇编”**设计的。它类似这样:
%x = callbr i32 asm(...), label %normal, [label %tgt1, label %tgt2]

意思是执行完内联汇编后,可能跳到 normal,也可能跳到 tgt1tgt2关键是跳到 tgt1 或 tgt2 的“跳转”本身,是在用户写的汇编里做的,不是LLVM IR里的“br”做的。
这就有个问题:

  • 正常的SSA值赋值和寄存器分配是在IR里能显式插入mov/copy的位置做的。
  • 但对于 callbr,跳转本身在汇编代码里发生,LLVM 无法在“跳转”之后插入一条寄存器复制指令
  • 比如:你希望 %xtgt1 用到时,已经被装进某个寄存器,但实际上你没法保证在 tgt1 开头有机会插入那条“把 callbr 返回值拷贝到合适寄存器”的指令,因为跳转发生点在汇编blob里。
    所以,landingpad 的作用就是人为在每个目标块头部“重新定义一次 callbr 的返回值”
    你可以理解为:
  • 每个间接目标块,像“catch”一样,头上插一个“我在这里重新把callbr的返回值变成新的SSA名字”
  • 在 IR 里显式插入 llvm.callbr.landingpad,这样LLVM的后端知道:“哦,我可以在这个点做寄存器装载/复制!”
  • 对于每个目标块,所有用到 %x 的地方都指向自己块里的 %x.lpad,而不是原始的 %x
    这样就能“各有独立的 SSA 定义和寄存器映射”:
    比如,假设 callbr 的返回值 %x,你现在有两个目标块 tgt1tgt2
  • 你在 tgt1 插入了 %x1 = llvm.callbr.landingpad(%x)
  • tgt2 插入了 %x2 = llvm.callbr.landingpad(%x)
    这样后端在分配寄存器时,可以分别考虑 %x1%x2,比如
  • %x1 可以放到寄存器 eax
  • %x2 可以放到寄存器 ebx
    并且tgt1tgt2 的头部,后端都可以安排一条合适的拷贝指令(比如 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