LLVM后端通过TargetLowering和CCState机制实现参数传递,核心是SelectionDAG构建阶段的LowerFormalArguments:先用ComputeValueVTs展开类型,再调用目标重写的getRegisterTypeForCallingConv等方法决定寄存器分配或栈传参,最终生成SDValue节点接入DAG。

LLVM后端不“实现”函数参数传递逻辑,而是通过 TargetLowering 和 CCState 机制,将 IR 层的参数语义映射到目标平台的调用约定(calling convention)上。这个过程发生在 SelectionDAG 构建阶段,核心是把 %arg 这类 IR 值,转换为寄存器分配或栈偏移的 SDValue 节点。
参数降级(LowerFormalArguments)是关键入口
当 SelectionDAGISel::LowerArguments 被调用时,它会:
- 对每个 IR 参数调用
ComputeValueVTs,把结构体/数组展开为基本类型(如{i32, i64}→[i32, i64]),生成一组EVT类型 - 对每个
EVT,调用目标子类重写的getRegisterTypeForCallingConv和getNumRegistersForCallingConv,决定该值占几个寄存器、用什么寄存器类(如GR32或FPR64) - 构造
ISD::InputArg列表,传给TargetLowering::LowerFormalArguments
这个函数才是你真正要定制的地方——比如 RISC-V 后端在 LowerFormalArguments 中用 CCState 尝试 AllocateReg,失败则走 AllocateStack,并返回对应位置的 SDValue(可能是 CopyFromReg 或 load)。
寄存器 vs 栈:依赖 calling convention 和 CCState 状态
CCState 维护当前已分配的整数/浮点寄存器计数(NumIntRegs/NumFloatRegs),以及栈偏移游标(NextStackOffset)。是否走寄存器,取决于:
- 当前调用约定(如
CallingConv::C、CallingConv::Fast)是否允许该类型使用寄存器 -
CCState::CanAssignArg返回 true,且AllocateReg成功(例如 x86-64 的前 6 个整数参数用%rdi–%r9) - 否则 fallback 到
AllocateStack,生成带FrameIndex的load节点
注意:即使 IR 参数是 i32,若调用约定禁用整数寄存器(如某些嵌入式 ABI),也会强制栈传参;反之,小结构体在 x86-64 System V 下可能被拆成两个寄存器传,而非压栈。
参数节点最终怎么接入 DAG?
LowerFormalArguments 返回的 SDValue 列表,会被 SelectionDAGISel 按原始 IR 参数顺序重新组装:
- 如果原始参数是聚合类型(如 struct),需用
ISD::BUILD_PAIR或ISD::MERGE_VALUES合并多个寄存器/栈 load 结果 - 这些节点成为 DAG 的 root inputs,后续指令(如
add、call)通过SDValue引用它们,形成数据依赖边 - 不会生成显式的 “move %rdi → %rax” 指令;寄存器分配阶段才决定物理寄存器,而此处只是标记“这个 SDValue 来自第 0 个整数参数寄存器”
常见错误:重写 LowerFormalArguments 时忘记调用 CCState::AnalyzeFormalArguments,导致 CCState 内部计数未更新,后续参数全被压栈;或误将 SDValue 类型设为 MVT::Other,引发 legalization 失败。
调试参数传递问题最直接的方法
编译时加 -mllvm -debug-only=isel 可输出 SelectionDAG 构建日志,重点关注:
- “LowerFormalArguments for function …” 段落,确认每个参数的
EVT和分配结果(reg X或stack slot Y) - DAG dump 中
EntryToken后紧跟的CopyFromReg或load节点,检查其Chain和Index是否合理 - 若看到
UNDEF或空SDValue,大概率是LowerFormalArguments返回了非法值或漏处理某个InputArg
参数传递不是靠硬编码 mov 序列完成的,而是靠 CCState 驱动的契约式分配 + SDValue 数据流建模。一旦 LowerFormalArguments 返回的节点语义和目标 ABI 不一致,后续所有指令选择都会错位——这点比寄存器分配或指令调度更容易被忽略。

















