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):生成从S1到S0的序列(注意S%u会替换为数字,但这里是降序:1, 0)。实际生成两个寄存器:S1和S0(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。
- 第一个 i32 值放入
-
注意:
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 后端中的作用
-
指令调度 :LLVM 的调度器(如
MachineScheduler)会读取这些行程信息,在保持数据依赖的前提下,尽可能避免因功能单元冲突导致的停顿。 -
流水线建模 :对于多周期指令(如
IILoad需要 3 周期),调度器会适当插入空转周期(bubbles)或重排指令来隐藏延迟。 -
代码生成:最终生成的汇编代码在性能上更贴合目标处理器的微架构。
🔧 如何使用
在 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 寄存器号,方便调试器栈回溯。 |
| 寄存器分类 | 通过继承 Cpu0GPRReg 和 Cpu0C0Reg 的不同基类,在后端代码中可以区分通用寄存器和协处理器寄存器,应用不同的处理逻辑。 |
5. 后续使用
在 Cpu0 的后端代码中(例如 Cpu0RegisterInfo.cpp、Cpu0InstrInfo.td、Cpu0ISelLowering.cpp),可以通过这些定义来:
-
引用特定寄存器:如
&Cpu0::ZERO获取零寄存器的TargetRegisterInfo对象。 -
描述指令操作数:在指令的 TableGen 定义中,使用
ZERO作为隐含操作数或显式操作数。 -
生成寄存器分配器所需的寄存器类(Register Classes):例如
GPR32RegClass包含所有Cpu0GPRReg定义的寄存器