MIT6828 学习笔记 009 (RISC-V ABI::Calling Conventions)

MIT6828 学习笔记 009 (RISC-V ABI::Calling Conventions)

RayAlto OP

我也不知道我写的什么寄罢,等以后明白了再来改

MIT 网站上提供的 RISC-V Calling Convention 已经在最新的 RISC-V 手册 中被移除了,现在在 RISC-V ELF psABI Specification 里,所以下面是新的 Calling Convention 的笔记

缩写全称
ABIApplication Binary Interface 应用程序二进制接口
gABIGeneric system v ABI 通用 System V 应用程序二进制接口
DWARFDebugging With Arbitrary Record Formats 使用任意记录格式进行调试
ELFExecutable and Linking Format 可执行和链接格式
FLEN浮点数寄存器的宽度,单位: bit
GOTGlobal Offset Table 全局偏移量表
ISAInstruction Set Architecture 指令集架构
NTBSNull-Terminated Byte String 以 NULL 结尾的字节串
PCProgram Counter 程序计数器
PLTProgram Linkage Table 程序链接表
psABIProcessor-Specific ABI 处理器特化 ABI
TLSThread-Local Storage 线程本地存储
XLEN整数寄存器的宽度,单位: bit

ABI 名称怎么看:比如 LP64 指的是 long 和指针都是 64 bit 的, int 则是默认的 32 bit , ILP64 就指 int, long 和指针都是 64 bit 的。

1. 寄存器约定

1.1. 整数寄存器约定

寄存器ABI 助记符用途存储是否跨调用
x0zero—(不可变)
x1ra返回地址
x2sp栈指针
x3gp全局指针—(不可分配)
x4tp线程指针—(不可分配)
x5-x7t0-t2临时寄存器
x8-x9s0-s1被调用方保存的寄存器
x10-x17a0-a7参数寄存器
x18-x27s2-s11被调用方保存的寄存器
x28-x31t3-t6临时寄存器

在标准 ABI 下,程序不应该修改 tpgp 寄存器,因为信号处理器可能依赖于这两个寄存器的值。帧指针的存在是可选的,如果决定有帧指针,则必须放在 x8 (s0) 里,且这个寄存器仍然是被调用方保存的。

1.2. 浮点寄存器约定

寄存器ABI 助记符用途存储是否跨调用
f0-f7ft0-ft7临时寄存器
f8-f9fs0-fs1被调用方保存的寄存器是*
f10-f17fa0-fa7参数寄存器
f18-f27fs2-fs11被调用方保存的寄存器是*
f28-f31ft8-ft11临时寄存器

*:被调用方保存的寄存器里的浮点值只有在不大于目标 ABI 中的浮点寄存器的宽度时,才能被跨调用存储,因此,如果针对基本整数调用约定,这些寄存器可以看作是临时的。

1.3. 向量寄存器约定

寄存器ABI 助记符用途存储是否跨调用
v0-v31临时寄存器
vl向量长度
vtype向量数据类型寄存器
vxrm向量定点舍入模式寄存器
vxsat向量定点饱和状态寄存器
  • vl: Vector Length
  • vtype: Vector TYPE
  • vxrm: Vector fiXed-point Rounding Mode register
  • vxsat: Vector fiXed-point SATuration flag register

向量寄存器不是用来传递参数或返回值的,我们打算定义一套新的调用约定实现它以作为未来的软件优化。 vcsr 下的 vxrmvxsat 的存储不跨调用,且它们的值在输入时没有指定。程序可以假定 vstart 在输入时或从程序调用返回时为 0

应用软件通常不应该显式写入 vstart ,任何显式写入 vstart 为非零值的程序必须在返回或调用其他程序之前写入 vstart 为零

2. 程序调用约定

这一章定义标准调用约定、描述如何传递参数及返回值。

函数必须遵守调用约定中定义的寄存器约定:任何寄存器的内容,如果没有在调用约定中规定它作为参数寄存器,则在输入时都是未指定的;任何寄存器的内容,如果没有在调用约定中规定它作为返回值或被调用方保存的寄存器,则退出时都是未指定的;所有被调用方保存的寄存器的内容必须被恢复为进入时的值;所有类似 gptp 的固定寄存器的内容永远不变。

Big Endian vs Little Endian

数据在计算机内存里本质上是一段 byte ,计算机内存的每个 byte 都可以用一个地址访问,一般规定数据在内存中的最小地址为一段数据的地址。比如一段数据 0x11, 0x45, 0x14 ,它的地址是 0x0000 ,在 little endian 下它的存储方式是:

1
2
3
0x0002: 0x14
0x0001: 0x45
0x0000: 0x11

在 big endian 下是:

1
2
3
0x0002: 0x11
0x0001: 0x45
0x0000: 0x14

要注意的是 uint32_t foo = 0x11223344 的数据实际上是 0x44, 0x33, 0x22, 0x11 ,因为就数字来说最右边是较低位;而 char* foo = "11223344" 的数据实际上是 '1', '1', '2', '2', '3', '3', '4', '4' ,因为就字符串来说最左边的是较低位。

2.1. 整数调用约定

基本整数调用约定提供了八个参数寄存器 a0a7a0a1 同时也被用作返回值。

最宽 XLEN 个 bit 的标量通过单个参数寄存器传递,或者没有可用的参数寄存器时以值的形式在栈上传递。整数标量传递时根据其类型的符号可最大扩展到 32 个 bit ,然后符号扩展到 XLEN ;浮点标量传递时扩展到 XLEN ,上面的几个 bit 留空。

2 倍 XLEN 宽的标量通过一对参数寄存器传递,较低位数据保存在较低数字的寄存器里,或者没有可用的参数寄存器时以值得形式在栈上传递(如果恰好只剩一个参数寄存器可以用,则较低的 XLEN 个 bit 会通过这个寄存器传递,较高位的 XLEN 个 bit 会通过栈传递

超过 2 倍 XLEN 宽的标量会通过引用传递,在参数列表里以地址形式存放

不超过 XLEN 宽的聚合类型数据通过单个寄存器传递,字段的分布和在内存里时一样;如果没有可用寄存器则通过栈传递;不超过 2 倍 XLEN 宽的聚合类型数据通过一对寄存器传递(如果恰好只剩一个寄存器可以用,则开头的 XLEN 个 bit 通过寄存器传递,剩下的通过栈传递);如果没有可用的寄存器,则聚合类型数据通过栈传递。没有使用的 bit 会被填充,大小不能被 XLEN 整除的聚合类型数据后面的空闲的 bit 是没有定义的。

通过栈传递的标量或聚合类型与更大的类型以及 XLEN 对齐,单不会超过栈对齐。

超过 2 被 XLEN 的聚合类型数据会通过引用传递,在参数列表里以地址形式存放,具有可平凡复制构造函数及析构函数或 vtables 的 C++ 聚合类型也一样。

C 编译器会忽略空的结构体/联合体参数或返回值,因为 C 编译支持它们作为非标准扩展,但 C++ 不是这样,因为 C++ 要求它们的大小是确定的。

位域以 little endian 保存,并以最小的成员大小对齐,比如:

1
2
3
4
struct {
uint32_t x : 10;
uint32_t y : 12;
};

则这个结构体占 32 个 bit () ,前 10 个 bit 是 x ,接着的 12 个 bit 是 y 的,剩下的 10 个是未定义的,因为所以不需要对齐,比如这个结构体的地址是 0x00

1
2
3
4
5
6
7
8
0x03: 0b00000000

0x02: 0b00000000
yyyyyy
0x01: 0b00000000
yyyyyyxx
0x00: 0b00000000
xxxxxxxx

再比如:

1
2
3
4
struct {
uint16_t x : 10;
uint16_t y : 12;
}

占 32 个 bit () ,且需要对齐,前 10 个 bit 是 x ,接着 6 个 bit 用于对齐(未定义的),再接着 12 个 bit 是 y ,剩下的 4 个是未定义的,比如这个结构体的地址是 0x00

1
2
3
4
5
6
7
8
0x03: 0b00000000
yyyy
0x02: 0b00000000
yyyyyyyy
0x01: 0b00000000
xx
0x00: 0b00000000
xxxxxxxx

浮点实数的传递方式与聚合类型数据相同,浮点叙述的传递方式与包含两个浮点实数的结构体相同。在基本整数调用约定下,可变参数的传递方式和命名参数的相同。

栈向下增长(低地址方向),栈指针应该在程序入口上面与 128-bit 边界对齐。栈上传递的第一个参数在函数入口上的栈指针的偏移量为零处,接下来的参数被相应地保存在更高的地址。

在标准 ABI 下,栈指针必须在程序执行过程中始终保持对齐,非标准 ABI 代码必须使栈指针对齐后才可以调用标准 ABI 程序。操作系统不许确保栈指针对齐才可以调用信号处理器,因此 POSIX 信号处理器不需要重新对齐栈指针,在使用被中断者的栈实现中断服务的系统下,如果中断服务流程链接了使用非标准栈对齐规则的代码,则必须重新对齐栈指针,但如果所有代码都遵循标准 ABI 的话则不需要重新对齐栈指针。程序不可以以来于地址低于栈指针的栈分配的数据的持续性。

s0-s11 寄存器需要被跨程序调用保存,如果存在浮点寄存器,则不用跨调用保存

2.2. 硬件浮点调用准则

硬件浮点调用准则添加了 8 个浮点参数寄存器 fa0-fa7 ,前两个也被用作返回值,无论整数寄存器是否已经用尽,值都会尽可能地通过浮点寄存器传递。

C 结构体都会被“展开”,比如下面两个结构体的处理方式一样:

1
2
3
4
5
6
7
8
9
10
struct {
struct {
float f[1];
} g[2];
};

struct {
float f;
float g;
};

展开过程中包含空结构体/联合体的字段都会被忽略(甚至 C++ 也是一样,除非他们没有可平凡复制构造函数及析构函数),位域中宽度为零的字段也会被忽略,且 aligned, packed 之类的属性也不会影响这个过程,比如下面两个结构体的处理方式也是一样的:

1
2
3
4
5
6
7
8
struct {
int i;
double d;
};
struct __attribute__((__packed__)) {
int i;
double d;
};

这两个也是一样的:

1
2
3
4
5
6
7
8
struct {
float f;
float g;
};
struct {
float f;
float g __attribute__((aligned(8)));
};

3. C/C++ 类型详情

3.1. C/C++ 类型大小及对齐

对于 C/C++ 的类型大小和对齐有两种约定, ILP32, ILP32F, ILP32D 和 ILP32E 遵循下面类型大小及对齐(基于 ILP32 约定):

类型大小 (bytes)对齐 (bytes)
bool/_Bool11
char11
short22
int44
long44
long long88
void*44
_Float1622
double88
long double1616
float _Complex84
double _Complex168
long double _Complex3216

LP64, LP64F, LP64D 和 LP64Q 遵循下面的类型大小及对齐(基于 LP64 约定):

类型大小 (bytes)对齐 (bytes)
bool/_Bool11
char11
short22
int44
long88
long long88
__int1281616
void*88
_Float1622
float44
double88
long double1616
float _Complex84
double _Complex168
long double _Complex3216

3.2. C/C++ 类型表现

char 是无符号的;布尔值 (bool/_Bool) 在内存中和作为标量参数传递时非 0 (false) 即 1 (true)

3.3. va_list, va_startva_arg

va_list 的类型是 void* ,含有可变参数的被调用函数需要复制传递可变参数的寄存器的值到 vararg 保存区域(需要接着在栈上传递的参数), va_start 宏初始化它的 va_list 参数使其指向 vararg 保存区域的起点。 va_arg 宏会根据给定类型的大小扩展它的 va_list 参数。