LLVM Cpu0 调用规则解析

llvm\lib\Target\Cpu0\Cpu0CallingConv.td

// Match if the current subtarget has a feature F.

class CCIfSubtarget<string F, CCAction A>

: CCIf<!strconcat("State.getTarget().getSubtarget<Cpu0Subtarget>().", F), A>;

def CSR_O32 : CalleeSavedRegs<(add LR, FP, (sequence "S%u", 1, 0))>;

def RetCC_Cpu0EABI : CallingConv<[

// i32 are returned in registers V0, V1, A0, A1

CCIfType<[i32], CCAssignToReg<[V0, V1, A0, A1]>>

]>;

def RetCC_Cpu0 : CallingConv<[

CCDelegateTo<RetCC_Cpu0EABI>

]>;

它主要定义了 Cpu0 后端中与调用约定相关的寄存器使用规则。

1. 条件类:CCIfSubtarget

tablegen

复制代码
class CCIfSubtarget<string F, CCAction A>
  : CCIf<!strconcat("State.getTarget().getSubtarget<Cpu0Subtarget>().", F), A>;
  • 作用 :这是一个条件类,用于在调用约定或寄存器分配规则中,判断当前 CPU 子目标(Subtarget)是否启用了某个特性 F

  • 继承关系 :它继承自 LLVM 已有的 CCIf 类。CCIf 接受一个条件字符串和一个动作 A,当条件字符串为真时执行动作 A

  • 实现细节strconcat 将固定的字符串 "State.getTarget().getSubtarget<Cpu0Subtarget>()." 和传入的特性 F 拼接成一个完整的 C++ 条件表达式。例如,如果 F = "hasMips32r2()",最终条件将是 State.getTarget().getSubtarget<Cpu0Subtarget>().hasMips32r2()

  • 用途:用于根据目标特性(如指令集扩展)动态地选择不同的调用约定或参数传递规则。

2. 被调用者保存寄存器:CSR_O32

tablegen

复制代码
def CSR_O32 : CalleeSavedRegs<(add LR, FP, (sequence "S%u", 1, 0))>;
  • 作用 :定义一个名为 CSR_O32 的被调用者保存寄存器(Callee-Saved Registers,CSR)列表,用于 O32 ABI 规范。这些寄存器在被调用函数中必须保存并恢复,调用者可以假设它们的内容在函数调用后保持不变。

  • 内容解析(add LR, FP, (sequence "S%u", 1, 0)) 是一个 TableGen 表达式,表示:

    • LR:返回地址寄存器。

    • FP:帧指针寄存器。

    • (sequence "S%u", 1, 0):生成从 S1S0 的序列(注意 S%u 会替换为数字,但这里是降序:1, 0)。实际生成两个寄存器:S1S0(S 通常表示被调用者保存的寄存器,如 MIPS 中的 S0-S7)。

  • 用途:该列表会用于生成函数序言(prologue)和尾声(epilogue)代码中保存和恢复寄存器的信息。

3. 返回约定 1:RetCC_Cpu0EABI

tablegen

复制代码
def RetCC_Cpu0EABI : CallingConv<[
  CCIfType<[i32], CCAssignToReg<[V0, V1, A0, A1]>>
]>;
  • 作用:定义 Cpu0 在 EABI (Embedded ABI) 下的返回值调用约定。

  • CallingConv:这是 LLVM 中定义调用约定的基类。内部的列表定义了如何将 LLVM IR 类型映射到机器寄存器或栈位置。

  • 规则解释

    • CCIfType<[i32], ...>:如果返回值的类型是 i32(32位整数),则执行后面的动作。

    • CCAssignToReg<[V0, V1, A0, A1]>:将该返回值分配给寄存器列表中的寄存器。规则通常是:

      • 第一个 i32 值放入 V0,第二个放入 V1,第三个放入 A0,第四个放入 A1
    • 注意:V0, V1 通常表示返回值寄存器(如 MIPS 的 $v0, $v1),而 A0, A1 本是参数传递寄存器,但这里复用作为返回值寄存器(在一些嵌入式 ABI 中允许)。

  • 用途:该约定指导 LLVM 如何生成函数返回值的代码。

4. 返回约定 2:RetCC_Cpu0

tablegen

复制代码
def RetCC_Cpu0 : CallingConv<[
  CCDelegateTo<RetCC_Cpu0EABI>
]>;
  • 作用:定义 Cpu0 的默认返回调用约定。

  • CCDelegateTo :这是一个委托动作,意思是当前的调用约定直接委托给另一个定义好的调用约定(这里是 RetCC_Cpu0EABI)。

  • 为什么要这样分层 :这种设计便于支持多个 ABI。例如,以后如果要增加一个 RetCC_Cpu0O32,只需修改 RetCC_Cpu0 委托的目标即可,或者根据目标特性条件选择。这里只有一种,所以简单地委托给 EABI 版本。

总结:这段代码的整体意图

这段代码共同为 Cpu0 后端搭建了基础的调用约定框架

  • 条件判断工具CCIfSubtarget 允许后续规则根据子目标特性做选择。

  • 寄存器保存规则CSR_O32 告诉编译器哪些寄存器需要在函数调用时由被调用者保护。

  • 返回值规则RetCC_Cpu0(委托给 RetCC_Cpu0EABI)规定了 i32 类型的返回值通过 V0, V1, A0, A1 这组寄存器返回。

这些定义会被 TableGen 生成相应的 C++ 代码(如 Cpu0GenCallingConv.inc),然后在 Cpu0ISelLowering.cpp 等文件中对函数的参数传递和返回值处理进行具体实现。

llvm\lib\Target\Cpu0\Cpu0Schedule.td

Cpu0 后端定义了指令行程(Instruction Itineraries) ,用于描述指令在处理器流水线中各阶段的行为。主要服务于指令调度器代码生成,帮助编译器生成更高效的指令顺序,尤其是在多周期指令或资源冲突的场景下。

🧩 模块拆解

1. 功能单元:FuncUnit

tablegen

复制代码
def ALU     : FuncUnit;
def IMULDIV : FuncUnit;
  • 作用:定义处理器的功能单元(Functional Units)。功能单元是处理器中执行特定类型操作的硬件模块,例如算术逻辑单元(ALU)或乘除法单元(IMULDIV)。

  • 用途:指令行程用来声明每条指令在执行阶段将占用哪些功能单元。如果两个指令需要同一个功能单元且时间重叠,就可能产生冲突,调度器会避免这种冲突。

2. 指令行程类:InstrItinClass

tablegen

复制代码
def IIAlu              : InstrItinClass;
def IICLO              : InstrItinClass;
def IICLZ              : InstrItinClass;
def IILoad             : InstrItinClass;
def IIStore            : InstrItinClass;
def IIBranch           : InstrItinClass;
def IIPseudo           : InstrItinClass;
  • 作用:为每一类指令定义一个"行程标签"。每个标签代表一种指令执行的时间‑资源模式。

  • 命名惯例II 通常代表 "Instruction Itinerary"。例如 IIAlu 对应普通 ALU 运算指令,IILoad 对应加载指令。

  • 用途 :在目标描述文件(.td)中,每条指令可以通过 Itinerary = IIAlu; 这样的字段关联到一个行程类。这样,指令调度器就能知道该类指令需要多少周期、占用哪些资源。

3. 处理器行程表:ProcessorItineraries

tablegen

复制代码
def Cpu0GenericItineraries : ProcessorItineraries<[ALU, IMULDIV], [], [
  InstrItinData<IIAlu               ,  [InstrStage<1,   [ALU]>]>,
  InstrItinData<IICLO               ,  [InstrStage<1,   [ALU]>]>,
  InstrItinData<IICLZ               ,  [InstrStage<1,   [ALU]>]>,
  InstrItinData<IILoad              ,  [InstrStage<3,   [ALU]>]>,
  InstrItinData<IIStore             ,  [InstrStage<1,   [ALU]>]>,
  InstrItinData<IIBranch            ,  [InstrStage<1,   [ALU]>]>
]>;
  • 参数解析

    • [ALU, IMULDIV]:列出该处理器模型中可用的所有功能单元。

    • []:一个空的行程列表(可能是用于传递额外的行程属性,此处为空)。

    • 最后的大列表:用 InstrItinData 将行程类(如 IIAlu)映射到具体的行程数据。

  • InstrStage:描述一个执行阶段。

    • 第一个参数是周期数 (Cycles)。例如 InstrStage<3, [ALU]> 表示该指令在 ALU 单元上执行需要 3 个周期

    • 第二个参数是此阶段使用的功能单元列表 。例如 [ALU] 表示使用 ALU 单元。

    • 可以链式定义多个阶段(例如加载指令可能先计算地址,再访问内存),但这里每个行程只有单个阶段。

⏱️ 各指令的行程含义

行程类 周期数 功能单元 含义
IIAlu 1 ALU 普通算术/逻辑运算(如 ADD, SUB)单周期完成。
IICLO 1 ALU CLO(计算前导零)指令单周期。
IICLZ 1 ALU CLZ(计算前导零)指令单周期。
IILoad 3 ALU 加载指令(如 LW)需要 3 个周期,推测是地址计算(1 周期)+ 访存(2 周期),但建模中全部计入 ALU 单元。
IIStore 1 ALU 存储指令单周期(实际可能也需要地址计算+访存,但这里简化为 1 周期)。
IIBranch 1 ALU 分支指令单周期。
IIPseudo 未定义 伪指令(如 IIPseudo)通常没有对应的硬件行程,会被展开为真实指令序列,调度器可能忽略它。

🎯 在 LLVM 后端中的作用

  1. 指令调度 :LLVM 的调度器(如 MachineScheduler)会读取这些行程信息,在保持数据依赖的前提下,尽可能避免因功能单元冲突导致的停顿。

  2. 流水线建模 :对于多周期指令(如 IILoad 需要 3 周期),调度器会适当插入空转周期(bubbles)或重排指令来隐藏延迟。

  3. 代码生成:最终生成的汇编代码在性能上更贴合目标处理器的微架构。

🔧 如何使用

在 Cpu0 的指令定义中(例如 Cpu0InstrInfo.td),可以为每条指令添加 Itinerary 字段:

tablegen

复制代码
def ADD : ArithLogicInst<...> {
  let Itinerary = IIAlu;
}
def LW : LoadInst<...> {
  let Itinerary = IILoad;
}

然后 TableGen 会生成 Cpu0GenInstrInfo.inc 等文件,其中包含指令行程信息的 C++ 数据结构,供调度器使用。

💎 总结

这段代码构建了一个简单但完整的指令行程模型,为 Cpu0 处理器定义了:

  • 功能单元(ALU、IMULDIV)。

  • 指令类别(ALU运算、加载、存储、分支等)。

  • 每个类别所需的周期数和资源占用。

let Namespace = "Cpu0" in {

//@ General Purpose Registers

def ZERO : Cpu0GPRReg<0, "zero">, DwarfRegNum<[0]>;

def AT : Cpu0GPRReg<1, "1">, DwarfRegNum<[1]>;

def V0 : Cpu0GPRReg<2, "2">, DwarfRegNum<[2]>;

def V1 : Cpu0GPRReg<3, "3">, DwarfRegNum<[3]>;

def A0 : Cpu0GPRReg<4, "4">, DwarfRegNum<[4]>;

def A1 : Cpu0GPRReg<5, "5">, DwarfRegNum<[5]>;

def T9 : Cpu0GPRReg<6, "6">, DwarfRegNum<[6]>;

def T0 : Cpu0GPRReg<7, "7">, DwarfRegNum<[7]>;

def T1 : Cpu0GPRReg<8, "8">, DwarfRegNum<[8]>;

def S0 : Cpu0GPRReg<9, "9">, DwarfRegNum<[9]>;

def S1 : Cpu0GPRReg<10, "10">, DwarfRegNum<[10]>;

def GP : Cpu0GPRReg<11, "11">, DwarfRegNum<[11]>;

def FP : Cpu0GPRReg<12, "12">, DwarfRegNum<[12]>;

def SP : Cpu0GPRReg<13, "13">, DwarfRegNum<[13]>;

def LR : Cpu0GPRReg<14, "14">, DwarfRegNum<[14]>;

def SW : Cpu0GPRReg<15, "15">, DwarfRegNum<[15]>;

def PC : Cpu0C0Reg<0, "pc">, DwarfRegNum<[20]>;

def EPC : Cpu0C0Reg<1, "epc">, DwarfRegNum<[21]>;

}

我们被问到:"解释",用户提供了一段TableGen代码,定义了一些寄存器。需要解释这段代码的意思和作用。这是LLVM后端描述Cpu0架构寄存器定义的一部分。需要详细解释Namespace、GPR、C0寄存器等。

这段代码是 LLVM 后端中用于描述 Cpu0 处理器寄存器的 TableGen 定义。它明确列出了 Cpu0 架构中的所有通用寄存器(GPR)和协处理器 0(C0)寄存器,并给每个寄存器分配了编号、名称和 DWARF 调试编号。

以下是对代码的逐部分详细解释。

1. let Namespace = "Cpu0" in { ... }

  • 作用 :设定接下来的寄存器定义所在的 C++ 命名空间(Namespace)。这意味着在生成的 C++ 代码中,这些寄存器相关的枚举值、访问函数等会被包裹在 namespace Cpu0 { ... } 内,防止与其他目标(如 X86、ARM)的寄存器名称冲突。

  • 语法let Namespace = "Cpu0" in { ... } 是一个 TableGen 的 let 语句,它让花括号内的所有定义都继承 Namespace 这个属性的值为 "Cpu0"

2. 通用寄存器定义(Cpu0GPRReg

tablegen

复制代码
def ZERO : Cpu0GPRReg<0,   "zero">, DwarfRegNum<[0]>;
def AT   : Cpu0GPRReg<1,   "1">,    DwarfRegNum<[1]>;
def V0   : Cpu0GPRReg<2,   "2">,    DwarfRegNum<[2]>;
def V1   : Cpu0GPRReg<3,   "3">,    DwarfRegNum<[3]>;
...
def SW   : Cpu0GPRReg<15,  "15">,   DwarfRegNum<[15]>;
  • Cpu0GPRReg :这是一个由 Cpu0 后端自己定义的寄存器类(通常在 Cpu0RegisterInfo.td 中定义),它继承自 LLVM 的 Register 类。它的参数依次是:

    • 第一个参数:硬件编码Encoding),即指令中用于表示该寄存器的二进制数值。例如 ZERO 的编码为 0。

    • 第二个参数:汇编名称AsmName),即编写汇编代码时使用的名字。例如 ZERO 的名字是 "zero",而 AT 的名字是 "1"(可能是因为 Cpu0 的汇编器允许使用数字编号作为寄存器名)。

  • DwarfRegNum<[num]> :为该寄存器指定 DWARF 调试编号。调试器(如 GDB、LLDB)使用这个编号在 DWARF 调试信息中识别寄存器。这里为 ZERO 分配编号 0,AT 编号 1,依次类推直到 SW 编号 15。

寄存器名称及用途约定(基于通常的 MIPS 风格命名,Cpu0 很可能沿用了类似约定):

寄存器 硬件编码 汇编名 典型用途
ZERO 0 zero 始终读为 0,写入被忽略
AT 1 1 汇编器临时寄存器(Assembler Temporary)
V0, V1 2, 3 2, 3 函数返回值(Value registers)
A0, A1 4, 5 4, 5 函数参数(Argument registers)
T9 6 6 临时寄存器,也可用于调用(Temporary)
T0, T1 7, 8 7, 8 临时寄存器(Temporaries)
S0, S1 9, 10 9, 10 被调用者保存寄存器(Saved registers)
GP 11 11 全局指针(Global Pointer)
FP 12 12 帧指针(Frame Pointer)
SP 13 13 栈指针(Stack Pointer)
LR 14 14 链接寄存器(Link Register),保存返回地址
SW 15 15 另一个临时/保留寄存器?名称"SW"可能代表"Software"

3. 协处理器 0 寄存器定义(Cpu0C0Reg

tablegen

复制代码
def PC   : Cpu0C0Reg<0, "pc">,    DwarfRegNum<[20]>;
def EPC  : Cpu0C0Reg<1, "epc">,   DwarfRegNum<[21]>;
  • Cpu0C0Reg :这是 Cpu0 后端为 协处理器 0(System Control Coprocessor) 的寄存器定义的类。协处理器 0 通常用于系统控制、异常处理、内存管理等功能。

    • PC:程序计数器(Program Counter)。虽然 PC 通常不是程序员可见的通用寄存器,但在某些架构中可以通过特殊指令读取。这里将其建模为 C0 寄存器。

    • EPC:异常程序计数器(Exception Program Counter),用于保存发生异常时的返回地址。

  • DWARF 编号:为了在调试信息中区分,给 PC 分配了编号 20,EPC 编号 21。这些编号不与通用寄存器的编号冲突(通用寄存器用了 0~15)。

4. 整体作用总结

这段代码是 Cpu0 后端寄存器描述文件 的核心部分,它为 LLVM 提供了以下信息:

信息类型 说明
寄存器枚举值 TableGen 会生成 Cpu0GenRegisterInfo.inc,其中包含 Cpu0::ZERO, Cpu0::AT, ..., Cpu0::PC 等枚举常量。
硬件编码映射 指令选择(ISel)和代码生成时,知道每个寄存器的二进制编码(如 ZERO=0),以便生成正确的机器码。
汇编打印与解析 打印汇编时,将寄存器枚举转换为对应的字符串(如 "zero");解析汇编时反向映射。
DWARF 调试信息 生成 .debug_frame.eh_frame 时,知道每个 LLVM 寄存器对应哪个 DWARF 寄存器号,方便调试器栈回溯。
寄存器分类 通过继承 Cpu0GPRRegCpu0C0Reg 的不同基类,在后端代码中可以区分通用寄存器和协处理器寄存器,应用不同的处理逻辑。

5. 后续使用

在 Cpu0 的后端代码中(例如 Cpu0RegisterInfo.cppCpu0InstrInfo.tdCpu0ISelLowering.cpp),可以通过这些定义来:

  • 引用特定寄存器:如 &Cpu0::ZERO 获取零寄存器的 TargetRegisterInfo 对象。

  • 描述指令操作数:在指令的 TableGen 定义中,使用 ZERO 作为隐含操作数或显式操作数。

  • 生成寄存器分配器所需的寄存器类(Register Classes):例如 GPR32RegClass 包含所有 Cpu0GPRReg 定义的寄存器

相关推荐
qq_2518364571 小时前
基于java 汽车检修管理系统设计与实现 论文
java·开发语言·汽车
基德爆肝c语言1 小时前
Qt系统相关
开发语言·qt
Jetev1 小时前
Django怎么优雅发送邮件_Python配置SMTP后端实现异步通知
jvm·数据库·python
redaijufeng1 小时前
C/C++程序从编译到链接的过程
c语言·开发语言·c++
woxihuan1234561 小时前
golang如何读写YAML配置文件_golang YAML配置文件读写解析
jvm·数据库·python
彳亍1011 小时前
mysql如何实现数据库按月分表_利用分区表优化查询性能
jvm·数据库·python
Captain_Data1 小时前
Python机器学习实战:用Scikit-learn从0构建信用风险评分模型(含WOE编码+AUC/KS/PSI评估+评分卡转换)
python·机器学习·数据分析·scikit-learn·风控建模
木子墨5161 小时前
系统设计面试 | 实现一个限流器:滑动窗口 → 令牌桶 → 漏桶
java·开发语言·数据结构·数据库·面试·职场和发展
m0_463672201 小时前
Golang怎么获取当前工作目录_Golang如何用os.Getwd获取程序运行路径【基础】
jvm·数据库·python