RISC-V 函数调用规范描述了在函数调用过程中,如何通过寄存器传递参数和返回值,如何管理堆栈以及调用方和被调用方的职责。以下是整型和浮点型数据的调用约定:
1. RISC-V 寄存器分类
RISC-V 的寄存器划分如下表所示:
| 寄存器类型 | 名称 | 编号 | 用途 |
|---|---|---|---|
| 临时寄存器 | t0-t6 | x5-x7, x28-x31 | 临时数据,调用方保存 |
| 保存寄存器 | s0-s11 | x8-x9, x18-x27 | 保存数据,被调用方保存 |
| 参数寄存器 | a0-a7 | x10-x17 | 用于传递整型或指针参数,以及返回值 |
| 返回地址寄存器 | ra | x1 | 返回地址 |
| 堆栈指针 | sp | x2 | 指向当前堆栈帧的顶部 |
| 浮点临时寄存器 | ft0-ft11 | f0-f11 | 临时浮点数据,调用方保存 |
| 浮点保存寄存器 | fs0-fs11 | f12-f31 | 保存浮点数据,被调用方保存 |
| 浮点参数寄存器 | fa0-fa7 | f10-f17 | 用于传递浮点参数和浮点返回值 |
2. 整型调用规范
2.1 参数传递:
前 8 个整型或指针参数通过 a0 到 a7 寄存器传递
如果参数超过 8 个,剩余的参数通过堆栈传递,按 8 字节对齐
2.2 返回值:
单个整型返回值存储在 a0
两个整型返回值分别存储在 a0 和 a1
超过两个的返回值通过调用方提供的内存缓冲区返回,指针通过 a0 传递
2.3 调用方职责:
调用方需保存 t0-t6 中的重要数据
在传递参数时,需确保参数按照约定存储到相应寄存器或堆栈
2.4 被调用方职责:
被调用方需保存并恢复 s0-s11
必要时创建新的堆栈帧
3. 浮点调用规范
3.1 参数传递:
前 8 个浮点参数通过 fa0 到 fa7 寄存器传递
如果浮点参数超过 8 个,剩余参数通过堆栈传递,按 8 字节对齐
混合参数中,整型使用 a0-a7,浮点使用 fa0-fa7
3.2 返回值:
单精度浮点返回值存储在 fa0
双精度浮点返回值存储在 fa0
两个浮点返回值分别存储在 fa0 和 fa1
3.3 调用方职责:
保存 ft0-ft11 中的数据避免丢失
3.4 被调用方职责:
保存并恢复 fs0-fs11
4. 堆栈管理
4.1 堆栈对齐:
堆栈指针 sp 必须保持 16 字节对齐
堆栈用于传递参数或保存局部变量时,需保持对齐约定
4.2 堆栈帧结构:
保存返回地址 ra
保存的 s0-s11
局部变量
超出寄存器的参数存放区域
5. 调用与返回
5.1 调用函数:
使用 jal 指令跳转,返回地址存储在 ra
示例:jal ra, func
5.2 函数返回:
使用 ret 指令,等价于 jalr x0, ra, 0
6. 职责对比
6.1 调用方:
保存 t0-t6 和 ft0-ft11
参数压栈并按 16 字节对齐
6.2 被调用方:
保存 s0-s11 和 fs0-fs11
必要时创建堆栈帧
7. 示例
整型函数调用:
# 函数原型:int add(int x, int y);
add:
add a0, a0, a1 # a0 = x + y
ret # 返回结果存储在 a0
浮点函数调用:
# 函数原型:float multiply(float x, float y);
multiply:
fmul.s fa0, fa0, fa1 # fa0 = x * y
ret # 返回结果存储在 fa0
7.1 进入示例
下载解压并进入示例目录
7.2 编译
make
7.3 运行
make run
运行后LOG如下
qemu-system-riscv64 -nographic -machine virt -m 128M -bios m_mode.bin -device loader,file=s_mode.bin,addr=0x80200000 -kernel s_mode.elf
_____ ____ _____
/ ____| _ \_ _|
| (___ | |_) || |
\ \___ \| _ < | |
____) | |_) || |_
|_|_____/|____/_____|
add: 5
nmultiply: 8.32
详见请参考文档:实验环境搭建。