本章介绍了RV32和RV64程序的C编译器标准以及两种调用约定:一种是针对基础ISA加上标准通用扩展(RV32G/RV64G)的调用约定,另一种是针对缺少浮点单元的实现(例如RV32I/RV64I)的软浮点调用约定。具有ISA扩展的实现可能需要扩展的调用约定。
重要要点
- C数据类型和对齐(18.1):
int在RV32和RV64下都是32位宽。long和指针在RV32下是32位,在RV64下是64位。long long是64位,float是32位,double是64位,long double是128位。- 所有数据类型在内存中保持自然对齐。
- RVG调用约定(18.2):
- 使用寄存器
a0-a7传递最多8个整数参数,fa0-fa7传递最多8个浮点参数。 - 较小的参数放在参数寄存器的低位,并在栈上是字节对齐的。
- 大于指针宽度两倍的参数按引用传递。
- 返回值通过
a0/hp0、a1/hp1返回,浮点值用fa0/fa1。 - 栈向下增长,
sp总是保持16字节对齐。
- 使用寄存器
- 软浮点调用约定(18.3):
- 用于无浮点硬件的实现,浮点参数和返回值通过整数寄存器传递。
- 在RV32下,
double参数传递在a2-a3中,long double通过引用传递。
关键概念和名词
- RVG调用约定:用于标准ISA扩展(RV32G/RV64G)的参数和返回值传递规则。
- 软浮点调用约定:用于无浮点硬件实现的调用规则,避免使用浮点指令。
- 参数传递规则:参数通过寄存器(整数和浮点)或栈传递,取决于参数数目和类型。
- 返回值传递:简单类型在寄存器返回,复杂结构返回指针。
- 寄存器角色:指明哪些寄存器由调用者保存,哪些由被调用者保存。
- ISA:(Instruction Set Architecture,指令集架构)是指计算机硬件与软件之间的接口规范,定义了处理器能够理解和执行的指令集。它主要包括以下几个关键方面:
- 指令集:定义了处理器能够执行的所有指令,包括操作码(指示操作类型)和操作数(操作对象)。
- 寻址模式:定义了如何访问操作数,例如立即寻址、寄存器寻址、直接寻址等。
- 数据类型:定义了处理器支持的数据类型,如整数、浮点数等。
- 寄存器:定义了处理器的寄存器数量、类型和用途。
- 中断和异常处理:定义了处理器如何处理中断和异常。
- 特权指令:定义了只有操作系统才能执行的指令,用于保护系统资源。
需要了解的背景知识
- 寄存器和内存布局:理解整数和浮点寄存器的分配和使用。
- 调用堆栈机制:清楚栈增长方向和栈帧结构。
- RISC-V架构基础:了解RISC-V基本指令集和ISA扩展。
- C语言数据类型:熟悉C中数据类型(如
int,long等)的大小和对齐。
学习目标
- 理解参数和返回值传递规则:掌握参数如何在寄存器或栈上传递,以及返回值的传递方式。
- 掌握寄存器使用和保存规则:明确哪个寄存器是调用者保存,哪个是被调用者保存。
- 熟悉栈的使用:了解栈如何存储未在寄存器传递的参数和局部变量。
- 比较RVG和软浮点调用约定:知道两种调用约定的不同适用场景和差异。
重要代码和函数
- 参数传递逻辑:在编译器或汇编代码中,函数调用时参数的入栈或寄存器分配部分。
- 函数返回值处理:如何从寄存器或内存读取函数返回值的代码部分。
- 栈帧管理:函数调用时,栈指针调整、局部变量分配等代码片段。
学习建议
- 阅读《RISC-V ISA手册》:特别是有关用户级ISA的部分,理解寄存器和指令。
- 研究编译器生成的汇编代码:查看C代码编译后如何应用调用约定。
- 动手实验:在支持RISC-V的环境中编写代码,观察参数传递和返回值的处理。
- 强化基础:充分理解寄存器、栈、C数据类型等基础知识,以便更好地掌握调用约定。
18.1 C数据类型和对齐
数据类型
- 整数类型:
int:32位宽。long和 指针:- RV32:32位宽(ILP32模型)。
- RV64:64位宽(LP64模型)。
long long:64位宽。char和unsigned char:8位宽,存储时零扩展。unsigned short:16位宽,存储时零扩展。signed char和short:分别8位和16位宽,存储时符号扩展。
- 浮点类型:
float:32位IEEE 754-2008浮点数。double:64位IEEE 754-2008浮点数。long double:128位IEEE浮点数。
- 特殊规则:
- 在RV64中,32位类型(如
int)存储在寄存器中时,会以符号扩展形式存储其32位值(即使是无符号类型)。
- 在RV64中,32位类型(如
数据对齐
- RV32和RV64的C编译器以及兼容软件在将数据类型存储到内存时会保持自然对齐。这意味着数据类型会按照其大小对齐到相应的内存地址,以提高访问效率。
- *数据对齐会实现访问效率的提升,这是我们在page table一节也提到过的内容。不太正确地来将,我们所使用指针加减动作,其实每次都是实现的一个某一个量上的变化,是固定的,比如int a,则 a + 1,实际上是移动了4个字节,因为a的类型是4个字节,这是一种统一性

18.2 RVG调用约定
在这里,我们先要思考要给问题,calling convention为什么被设计出来。在汇编语言中,如果一个函数需要调用另一个函数,是需要保留上下文和现场的,方便后面的现场恢复。这也就引生出来了保存寄存器(save register)和一般寄存器。我们一般认为保存寄存器中的内容,是绝对可信的,因为在calle或者caller,二者必有一人会保存信息在其中,并且在某个阶段恢复它们(根据协议来看)。但是一般寄存器是绝对不能相信的。
所以我们可以得出结论:
- calling convention是通过使用寄存器实现数据传递的。
- 协议定义的是栈的使用和寄存器的保存规则
1. 参数传递规则
-
寄存器传递:
- 最多使用8个整数寄存器(
a0–a7)和8个浮点寄存器(fa0–fa7)来传递参数。 - 参数被视为结构体字段,前八个指针大小的字段通过寄存器传递。
- 浮点参数通过浮点寄存器传递,除非它们是联合体或结构体数组字段的一部分,此时通过整数寄存器传递。
- 可变参数函数中的浮点参数也通过整数寄存器传递。
- 最多使用8个整数寄存器(
-
小端字节序和对齐:
- 小于指针大小的参数通过寄存器的最低有效位传递。
- 子指针大小的参数通过栈传递时,存储在指针大小字段的低地址部分。
- 参数大小为指针大小的两倍时:
- 通过栈传递时自然对齐。
- 通过寄存器传递时,存储在对齐的偶-奇寄存器对中,偶数寄存器存储最低有效位(例如,
long long在RV32中通过a2和a3传递)。
-
大参数传递:
- 大于指针大小两倍的参数通过引用传递。
- 结构体中未通过寄存器传递的部分通过栈传递,栈指针
sp指向第一个未通过寄存器传递的参数。
2. 返回值传递规则
- 返回值通过以下寄存器传递:
- 整数返回值:
a0和a1。 - 浮点返回值:
- 如果是原始类型或仅包含一到两个浮点值的结构体成员,通过
fa0和fa1返回。 - 其他能够放入两个指针大小的返回值通过
a0和a1返回。
- 如果是原始类型或仅包含一到两个浮点值的结构体成员,通过
- 更大的返回值通过内存传递,调用者分配内存区域,并将其指针作为隐式第一个参数传递给被调用者。
- 整数返回值:
3. 栈和寄存器的使用规则
- 栈:
- 栈向下增长,栈指针
sp始终保持16字节对齐。
- 栈向下增长,栈指针
- 寄存器分类:
- 参数寄存器:
- 整数寄存器:
a0–a7。 - 浮点寄存器:
fa0–fa7。
- 整数寄存器:
- 临时寄存器(易失性):
- 整数寄存器:
t0–t6。 - 浮点寄存器:
ft0–ft11。 - 如果后续需要使用,必须由调用者保存。
- 整数寄存器:
- 保留寄存器(非易失性):
- 整数寄存器:
s0–s11。 - 浮点寄存器:
fs0–fs11。 - 如果使用了这些寄存器,必须由被调用者保存。
- 整数寄存器:
- 参数寄存器:
4. 总结
RVG调用约定通过寄存器高效地传递参数和返回值,同时定义了栈的使用和寄存器的保存规则。这种调用约定旨在优化函数调用的性能,同时保持代码的可移植性和一致性。

18.3 软浮点调用约定
核心内容总结
软浮点调用约定是针对缺乏浮点硬件的RISC-V实现(如RV32I或RV64I)设计的调用约定。它完全避免使用浮点寄存器和浮点指令,而是通过整数寄存器来传递和处理浮点参数。以下是关键要点的总结:
1. 浮点参数的处理
- 浮点参数传递:
- 浮点参数通过整数寄存器传递,遵循与相同大小的整数参数相同的规则。
- RV32:
- 单精度浮点(
float):通过单个整数寄存器传递。 - 双精度浮点(
double):通过两个连续的整数寄存器传递(例如,a2和a3)。 - 扩展精度浮点(
long double):通过引用传递(存储在内存中,传递指针)。
- 单精度浮点(
- RV64:
- 单精度浮点(
float)和双精度浮点(double):通过单个整数寄存器传递。 - 扩展精度浮点(
long double):通过引用传递。
- 单精度浮点(
- 返回值:
- 浮点返回值通过整数寄存器返回。
- RV32:
- 单精度浮点:通过
a0返回。 - 双精度浮点:通过
a0和a1返回。
- 单精度浮点:通过
- RV64:
- 单精度浮点和双精度浮点:通过
a0返回。
- 单精度浮点和双精度浮点:通过
- 示例:
- 在RV32中,函数
double foo(int, double, long double):- 第一个参数(
int)通过a0传递。 - 第二个参数(
double)通过a2和a3传递。 - 第三个参数(
long double)通过a4以引用方式传递。 - 返回值(
double)通过a0和a1返回。
- 第一个参数(
- 在RV64中,参数通过
a0、a1和a2-a3对传递,返回值通过a0返回。
- 在RV32中,函数
2. 动态舍入模式和异常处理
- 动态舍入模式和累积异常标志可以通过C99标准头文件
<fenv.h>提供的例程访问。 - 这些功能在软浮点实现中通过软件模拟完成,因为硬件不支持浮点操作。
3. 总结
软浮点调用约定是为缺乏浮点硬件的RISC-V实现设计的,通过整数寄存器传递和处理浮点参数。这种调用约定确保了即使在没有浮点单元的情况下,浮点运算也能通过软件模拟完成,同时保持与RVG调用约定的兼容性。