Calling Convention


本章介绍了RV32和RV64程序的C编译器标准以及两种调用约定:一种是针对基础ISA加上标准通用扩展(RV32G/RV64G)的调用约定,另一种是针对缺少浮点单元的实现(例如RV32I/RV64I)的软浮点调用约定。具有ISA扩展的实现可能需要扩展的调用约定。

重要要点

  1. C数据类型和对齐(18.1)
    • int 在RV32和RV64下都是32位宽。
    • long 和指针在RV32下是32位,在RV64下是64位。
    • long long 是64位,float 是32位,double是64位,long double是128位。
    • 所有数据类型在内存中保持自然对齐。
  2. RVG调用约定(18.2)
    • 使用寄存器a0-a7传递最多8个整数参数,fa0-fa7传递最多8个浮点参数。
    • 较小的参数放在参数寄存器的低位,并在栈上是字节对齐的。
    • 大于指针宽度两倍的参数按引用传递。
    • 返回值通过a0/hp0a1/hp1返回,浮点值用fa0/fa1
    • 栈向下增长,sp总是保持16字节对齐。
  3. 软浮点调用约定(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位宽。
    • charunsigned char:8位宽,存储时零扩展。
    • unsigned short:16位宽,存储时零扩展。
    • signed charshort:分别8位和16位宽,存储时符号扩展。
  • 浮点类型
    • float:32位IEEE 754-2008浮点数。
    • double:64位IEEE 754-2008浮点数。
    • long double:128位IEEE浮点数。
  • 特殊规则
    • 在RV64中,32位类型(如int)存储在寄存器中时,会以符号扩展形式存储其32位值(即使是无符号类型)。
数据对齐
  • RV32和RV64的C编译器以及兼容软件在将数据类型存储到内存时会保持自然对齐。这意味着数据类型会按照其大小对齐到相应的内存地址,以提高访问效率。
    • *数据对齐会实现访问效率的提升,这是我们在page table一节也提到过的内容。不太正确地来将,我们所使用指针加减动作,其实每次都是实现的一个某一个量上的变化,是固定的,比如int a,则 a + 1,实际上是移动了4个字节,因为a的类型是4个字节,这是一种统一性

RISC-V C程序所支持的本地数据类型

18.2 RVG调用约定

在这里,我们先要思考要给问题,calling convention为什么被设计出来。在汇编语言中,如果一个函数需要调用另一个函数,是需要保留上下文和现场的,方便后面的现场恢复。这也就引生出来了保存寄存器(save register)和一般寄存器。我们一般认为保存寄存器中的内容,是绝对可信的,因为在calle或者caller,二者必有一人会保存信息在其中,并且在某个阶段恢复它们(根据协议来看)。但是一般寄存器是绝对不能相信的。

所以我们可以得出结论:

  • calling convention是通过使用寄存器实现数据传递的。
  • 协议定义的是栈的使用和寄存器的保存规则

1. 参数传递规则

  1. 寄存器传递

    • 最多使用8个整数寄存器(a0–a7)和8个浮点寄存器(fa0–fa7)来传递参数。
    • 参数被视为结构体字段,前八个指针大小的字段通过寄存器传递。
    • 浮点参数通过浮点寄存器传递,除非它们是联合体或结构体数组字段的一部分,此时通过整数寄存器传递。
    • 可变参数函数中的浮点参数也通过整数寄存器传递。
  2. 小端字节序和对齐

    • 小于指针大小的参数通过寄存器的最低有效位传递。
    • 子指针大小的参数通过栈传递时,存储在指针大小字段的低地址部分。
    • 参数大小为指针大小的两倍时:
      • 通过栈传递时自然对齐。
      • 通过寄存器传递时,存储在对齐的偶-奇寄存器对中,偶数寄存器存储最低有效位(例如,long long在RV32中通过a2a3传递)。
  3. 大参数传递

    • 大于指针大小两倍的参数通过引用传递。
    • 结构体中未通过寄存器传递的部分通过栈传递,栈指针sp指向第一个未通过寄存器传递的参数。

2. 返回值传递规则

  1. 返回值通过以下寄存器传递:
    • 整数返回值:a0a1
    • 浮点返回值:
      • 如果是原始类型或仅包含一到两个浮点值的结构体成员,通过fa0fa1返回。
      • 其他能够放入两个指针大小的返回值通过a0a1返回。
    • 更大的返回值通过内存传递,调用者分配内存区域,并将其指针作为隐式第一个参数传递给被调用者。

3. 栈和寄存器的使用规则

    • 栈向下增长,栈指针sp始终保持16字节对齐。
  1. 寄存器分类
    • 参数寄存器
      • 整数寄存器:a0–a7
      • 浮点寄存器:fa0–fa7
    • 临时寄存器(易失性)
      • 整数寄存器:t0–t6
      • 浮点寄存器:ft0–ft11
      • 如果后续需要使用,必须由调用者保存。
    • 保留寄存器(非易失性)
      • 整数寄存器:s0–s11
      • 浮点寄存器:fs0–fs11
      • 如果使用了这些寄存器,必须由被调用者保存。

4. 总结

RVG调用约定通过寄存器高效地传递参数和返回值,同时定义了栈的使用和寄存器的保存规则。这种调用约定旨在优化函数调用的性能,同时保持代码的可移植性和一致性。

RISC-V calling convention register usage.

18.3 软浮点调用约定

核心内容总结

软浮点调用约定是针对缺乏浮点硬件的RISC-V实现(如RV32I或RV64I)设计的调用约定。它完全避免使用浮点寄存器和浮点指令,而是通过整数寄存器来传递和处理浮点参数。以下是关键要点的总结:


1. 浮点参数的处理
  1. 浮点参数传递
    • 浮点参数通过整数寄存器传递,遵循与相同大小的整数参数相同的规则。
    • RV32
      • 单精度浮点(float):通过单个整数寄存器传递。
      • 双精度浮点(double):通过两个连续的整数寄存器传递(例如,a2a3)。
      • 扩展精度浮点(long double):通过引用传递(存储在内存中,传递指针)。
    • RV64
      • 单精度浮点(float)和双精度浮点(double):通过单个整数寄存器传递。
      • 扩展精度浮点(long double):通过引用传递。
  2. 返回值
    • 浮点返回值通过整数寄存器返回。
    • RV32
      • 单精度浮点:通过a0返回。
      • 双精度浮点:通过a0a1返回。
    • RV64
      • 单精度浮点和双精度浮点:通过a0返回。
  3. 示例
    • 在RV32中,函数double foo(int, double, long double)
      • 第一个参数(int)通过a0传递。
      • 第二个参数(double)通过a2a3传递。
      • 第三个参数(long double)通过a4以引用方式传递。
      • 返回值(double)通过a0a1返回。
    • 在RV64中,参数通过a0a1a2-a3对传递,返回值通过a0返回。

2. 动态舍入模式和异常处理
  • 动态舍入模式和累积异常标志可以通过C99标准头文件<fenv.h>提供的例程访问。
  • 这些功能在软浮点实现中通过软件模拟完成,因为硬件不支持浮点操作。

3. 总结

软浮点调用约定是为缺乏浮点硬件的RISC-V实现设计的,通过整数寄存器传递和处理浮点参数。这种调用约定确保了即使在没有浮点单元的情况下,浮点运算也能通过软件模拟完成,同时保持与RVG调用约定的兼容性。


文章作者: MIKA
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 MIKA !
  目录