MIT6828 学习笔记 009 (RISC-V ABI::Calling Conventions)
我也不知道我写的什么寄罢,等以后明白了再来改
MIT 网站上提供的 RISC-V Calling Convention 已经在最新的 RISC-V 手册中被移除了,现在在 RISC-V ELF psABI Specification 里,所以下面是新的 Calling Convention 的笔记
| 缩写 | 全称 |
|---|---|
ABI | Application Binary Interface 应用程序二进制接口 |
gABI | Generic system v ABI 通用 System V 应用程序二进制接口 |
DWARF | Debugging With Arbitrary Record Formats 使用任意记录格式进行调试 |
ELF | Executable and Linking Format 可执行和链接格式 |
FLEN | 浮点数寄存器的宽度,单位: bit |
GOT | Global Offset Table 全局偏移量表 |
ISA | Instruction Set Architecture 指令集架构 |
NTBS | Null-Terminated Byte String 以 NULL 结尾的字节串 |
PC | Program Counter 程序计数器 |
PLT | Program Linkage Table 程序链接表 |
psABI | Processor-Specific ABI 处理器特化 ABI |
TLS | Thread-Local Storage 线程本地存储 |
XLEN | 整数寄存器的宽度,单位: bit |
ABI 名称怎么看:比如 LP64 指的是
long和指针都是 64 bit 的,int则是默认的 32 bit , ILP64 就指int,long和指针都是 64 bit 的。
1. 寄存器约定
1.1. 整数寄存器约定
| 寄存器 | ABI 助记符 | 用途 | 存储是否跨调用 |
|---|---|---|---|
x0 | zero | 零 | —(不可变) |
x1 | ra | 返回地址 | 否 |
x2 | sp | 栈指针 | 是 |
x3 | gp | 全局指针 | —(不可分配) |
x4 | tp | 线程指针 | —(不可分配) |
x5-x7 | t0-t2 | 临时寄存器 | 否 |
x8-x9 | s0-s1 | 被调用方保存的寄存器 | 是 |
x10-x17 | a0-a7 | 参数寄存器 | 否 |
x18-x27 | s2-s11 | 被调用方保存的寄存器 | 是 |
x28-x31 | t3-t6 | 临时寄存器 | 否 |
在标准 ABI 下,程序不应该修改 tp 和 gp 寄存器,因为信号处理器可能依赖于这两个寄存器的值。帧指针的存在是可选的,如果决定有帧指针,则必须放在 x8 (s0) 里,且这个寄存器仍然是被调用方保存的。
1.2. 浮点寄存器约定
| 寄存器 | ABI 助记符 | 用途 | 存储是否跨调用 |
|---|---|---|---|
f0-f7 | ft0-ft7 | 临时寄存器 | 否 |
f8-f9 | fs0-fs1 | 被调用方保存的寄存器 | 是* |
f10-f17 | fa0-fa7 | 参数寄存器 | 否 |
f18-f27 | fs2-fs11 | 被调用方保存的寄存器 | 是* |
f28-f31 | ft8-ft11 | 临时寄存器 | 否 |
*:被调用方保存的寄存器里的浮点值只有在不大于目标 ABI 中的浮点寄存器的宽度时,才能被跨调用存储,因此,如果针对基本整数调用约定,这些寄存器可以看作是临时的。
1.3. 向量寄存器约定
| 寄存器 | ABI 助记符 | 用途 | 存储是否跨调用 |
|---|---|---|---|
v0-v31 | 临时寄存器 | 否 | |
vl | 向量长度 | 否 | |
vtype | 向量数据类型寄存器 | 否 | |
vxrm | 向量定点舍入模式寄存器 | 否 | |
vxsat | 向量定点饱和状态寄存器 | 否 |
vl: Vector Lengthvtype: Vector TYPEvxrm: Vector fiXed-point Rounding Mode registervxsat: Vector fiXed-point SATuration flag register
向量寄存器不是用来传递参数或返回值的,我们打算定义一套新的调用约定实现它以作为未来的软件优化。 vcsr 下的 vxrm 和 vxsat 的存储不跨调用,且它们的值在输入时没有指定。程序可以假定 vstart 在输入时或从程序调用返回时为 0
应用软件通常不应该显式写入
vstart,任何显式写入vstart为非零值的程序必须在返回或调用其他程序之前写入vstart为零
2. 程序调用约定
这一章定义标准调用约定、描述如何传递参数及返回值。
函数必须遵守调用约定中定义的寄存器约定:任何寄存器的内容,如果没有在调用约定中规定它作为参数寄存器,则在输入时都是未指定的;任何寄存器的内容,如果没有在调用约定中规定它作为返回值或被调用方保存的寄存器,则退出时都是未指定的;所有被调用方保存的寄存器的内容必须被恢复为进入时的值;所有类似 gp 和 tp 的固定寄存器的内容永远不变。
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. 整数调用约定
基本整数调用约定提供了八个参数寄存器 a0 到 a7 , a0 和 a1 同时也被用作返回值。
最宽 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 | struct { |
则这个结构体占 32 个 bit (x ,接着的 12 个 bit 是 y 的,剩下的 10 个是未定义的,因为0x00:
1 | 0x03: 0b00000000 |
再比如:
1 | struct { |
占 32 个 bit (x ,接着 6 个 bit 用于对齐(未定义的),再接着 12 个 bit 是 y ,剩下的 4 个是未定义的,比如这个结构体的地址是 0x00:
1 | 0x03: 0b00000000 |
浮点实数的传递方式与聚合类型数据相同,浮点叙述的传递方式与包含两个浮点实数的结构体相同。在基本整数调用约定下,可变参数的传递方式和命名参数的相同。
栈向下增长(低地址方向),栈指针应该在程序入口上面与 128-bit 边界对齐。栈上传递的第一个参数在函数入口上的栈指针的偏移量为零处,接下来的参数被相应地保存在更高的地址。
在标准 ABI 下,栈指针必须在程序执行过程中始终保持对齐,非标准 ABI 代码必须使栈指针对齐后才可以调用标准 ABI 程序。操作系统不许确保栈指针对齐才可以调用信号处理器,因此 POSIX 信号处理器不需要重新对齐栈指针,在使用被中断者的栈实现中断服务的系统下,如果中断服务流程链接了使用非标准栈对齐规则的代码,则必须重新对齐栈指针,但如果所有代码都遵循标准 ABI 的话则不需要重新对齐栈指针。程序不可以以来于地址低于栈指针的栈分配的数据的持续性。
s0-s11 寄存器需要被跨程序调用保存,如果存在浮点寄存器,则不用跨调用保存
2.2. 硬件浮点调用准则
硬件浮点调用准则添加了 8 个浮点参数寄存器 fa0-fa7 ,前两个也被用作返回值,无论整数寄存器是否已经用尽,值都会尽可能地通过浮点寄存器传递。
C 结构体都会被“展开”,比如下面两个结构体的处理方式一样:
1 | struct { |
展开过程中包含空结构体/联合体的字段都会被忽略(甚至 C++ 也是一样,除非他们没有可平凡复制构造函数及析构函数),位域中宽度为零的字段也会被忽略,且 aligned, packed 之类的属性也不会影响这个过程,比如下面两个结构体的处理方式也是一样的:
1 | struct { |
这两个也是一样的:
1 | struct { |
3. C/C++ 类型详情
3.1. C/C++ 类型大小及对齐
对于 C/C++ 的类型大小和对齐有两种约定, ILP32, ILP32F, ILP32D 和 ILP32E 遵循下面类型大小及对齐(基于 ILP32 约定):
| 类型 | 大小 (bytes) | 对齐 (bytes) |
|---|---|---|
bool/_Bool | 1 | 1 |
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 4 |
long long | 8 | 8 |
void* | 4 | 4 |
_Float16 | 2 | 2 |
double | 8 | 8 |
long double | 16 | 16 |
float _Complex | 8 | 4 |
double _Complex | 16 | 8 |
long double _Complex | 32 | 16 |
LP64, LP64F, LP64D 和 LP64Q 遵循下面的类型大小及对齐(基于 LP64 约定):
| 类型 | 大小 (bytes) | 对齐 (bytes) |
|---|---|---|
bool/_Bool | 1 | 1 |
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 8 | 8 |
long long | 8 | 8 |
__int128 | 16 | 16 |
void* | 8 | 8 |
_Float16 | 2 | 2 |
float | 4 | 4 |
double | 8 | 8 |
long double | 16 | 16 |
float _Complex | 8 | 4 |
double _Complex | 16 | 8 |
long double _Complex | 32 | 16 |
3.2. C/C++ 类型表现
char 是无符号的;布尔值 (bool/_Bool) 在内存中和作为标量参数传递时非 0 (false) 即 1 (true)
3.3. va_list, va_start 和 va_arg
va_list 的类型是 void* ,含有可变参数的被调用函数需要复制传递可变参数的寄存器的值到 vararg 保存区域(需要接着在栈上传递的参数), va_start 宏初始化它的 va_list 参数使其指向 vararg 保存区域的起点。 va_arg 宏会根据给定类型的大小扩展它的 va_list 参数。