跳到主要内容

调用约定

调用约定(Calling Convention)定义了函数调用时参数传递、返回值处理、寄存器保存等规则。遵循统一的调用约定可以确保不同模块之间的正确协作。

RISC-V 调用约定概述

RISC-V 采用标准的调用约定,定义了:

  • 参数传递方式
  • 返回值处理
  • 寄存器保存责任
  • 栈帧布局

寄存器分类

调用者保存寄存器(Caller-Saved)

调用者保存寄存器在函数调用前需要由调用者保存(如果需要保留其值):

寄存器ABI 名称用途
x5-x7t0-t2临时寄存器
x10-x17a0-a7参数/返回值
x28-x31t3-t6临时寄存器

调用者保存寄存器的特点:

  • 被调用函数可以自由使用这些寄存器
  • 调用者需要在调用前保存重要值
  • 函数返回后,调用者不能假设这些寄存器的值不变

被调用者保存寄存器(Callee-Saved)

被调用者保存寄存器需要由被调用函数保存和恢复:

寄存器ABI 名称用途
x8s0/fp保存寄存器 0 / 帧指针
x9s1保存寄存器 1
x18-x27s2-s11保存寄存器 2-11
x1ra返回地址
x2sp栈指针

被调用者保存寄存器的特点:

  • 被调用函数必须在使用前保存这些寄存器
  • 函数返回前必须恢复这些寄存器
  • 调用者可以安全地假设这些寄存器的值不变

参数传递

整数参数

前 8 个整数参数通过 a0-a7 传递:

// C 函数声明
int function(int a, int b, int c, int d, int e, int f, int g, int h);

// 参数传递
// a -> a0
// b -> a1
// c -> a2
// d -> a3
// e -> a4
// f -> a5
// g -> a6
// h -> a7

超过 8 个参数时,多余的参数通过栈传递:

// C 函数声明
int function(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j);

// 参数传递
// a-h -> a0-a7
// i -> 栈 (sp + 0)
// j -> 栈 (sp + 8)

浮点参数

前 8 个浮点参数通过 fa0-fa7 传递:

// C 函数声明
float function(float a, float b, float c);

// 参数传递
// a -> fa0
// b -> fa1
// c -> fa2

混合参数

整数和浮点参数分别计数:

// C 函数声明
float function(int a, float b, int c, float d);

// 参数传递
// a -> a0
// b -> fa0
// c -> a1
// d -> fa1

结构体参数

小结构体(不超过 16 字节)通过寄存器传递:

struct Point {
int x;
int y;
};

void function(struct Point p);
// p.x -> a0
// p.y -> a1

大结构体通过引用传递(地址):

struct BigStruct {
int data[100];
};

void function(struct BigStruct* p);
// p -> a0(结构体地址)

返回值

整数返回值

整数返回值通过 a0 和 a1 返回:

// 单个返回值
int function() {
return 42;
}
// 返回值 -> a0

// 两个返回值(结构体)
struct Pair {
int first;
int second;
};
struct Pair function() {
return (struct Pair){1, 2};
}
// first -> a0
// second -> a1

浮点返回值

浮点返回值通过 fa0 和 fa1 返回:

float function() {
return 3.14f;
}
// 返回值 -> fa0

栈帧布局

基本栈帧结构

高地址
+------------------+
| 参数 n | 调用者栈帧
| ... |
| 参数 9 |
+------------------+ <- 调用前的 sp
| 返回地址 | ra (x1)
+------------------+
| 保存的寄存器 | s0-s11
| ... |
+------------------+
| 局部变量 |
| ... |
+------------------+
| 临时空间 |
| ... |
+------------------+ <- 当前 sp
低地址

栈帧示例

# 函数序言
function:
addi sp, sp, -32 # 分配栈空间
sw ra, 28(sp) # 保存返回地址
sw s0, 24(sp) # 保存帧指针
addi s0, sp, 32 # 设置帧指针

# 保存被调用者保存寄存器
sw s1, 20(sp)
sw s2, 16(sp)

# 函数体
# ...

# 函数尾声
lw s2, 16(sp) # 恢复被调用者保存寄存器
lw s1, 20(sp)
lw s0, 24(sp) # 恢复帧指针
lw ra, 28(sp) # 恢复返回地址
addi sp, sp, 32 # 释放栈空间
ret # 返回

帧指针的使用

帧指针(fp/s0)指向栈帧的固定位置,便于访问局部变量:

# 使用帧指针访问局部变量
function:
addi sp, sp, -64
sw ra, 60(sp)
sw s0, 56(sp)
addi s0, sp, 64 # fp = 原始 sp

# 局部变量在 fp 下方
# var1 在 fp - 8
# var2 在 fp - 12
sw a0, -8(s0) # 保存 var1
sw a1, -12(s0) # 保存 var2

# 访问局部变量
lw t0, -8(s0) # 读取 var1
lw t1, -12(s0) # 读取 var2

# ...

lw s0, 56(sp)
lw ra, 60(sp)
addi sp, sp, 64
ret

函数调用示例

简单函数

// C 代码
int add(int a, int b) {
return a + b;
}
# RISC-V 汇编
add:
add a0, a0, a1 # a0 = a0 + a1
ret # 返回

带局部变量的函数

// C 代码
int sum_array(int* arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
return sum;
}
# RISC-V 汇编
sum_array:
addi sp, sp, -16
sw s0, 12(sp)
addi s0, sp, 16

mv s0, a0 # 保存 arr
mv t0, a1 # n
li t1, 0 # sum = 0
li t2, 0 # i = 0

loop:
bge t2, t0, end # if i >= n, exit
slli t3, t2, 2 # offset = i * 4
add t3, s0, t3 # address
lw t4, 0(t3) # arr[i]
add t1, t1, t4 # sum += arr[i]
addi t2, t2, 1 # i++
j loop

end:
mv a0, t1 # 返回 sum
lw s0, 12(sp)
addi sp, sp, 16
ret

调用其他函数

// C 代码
int add(int a, int b);
int compute(int x, int y) {
int result = add(x, y);
return result * 2;
}
# RISC-V 汇编
compute:
addi sp, sp, -16
sw ra, 12(sp) # 保存返回地址
sw s0, 8(sp) # 保存 s0

# 调用 add(x, y)
# 参数已经在 a0, a1 中
call add # 调用 add

# 返回值在 a0 中
slli a0, a0, 1 # result * 2

lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret

递归函数

// C 代码
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
# RISC-V 汇编
factorial:
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)

mv s0, a0 # 保存 n

li t0, 1
ble a0, t0, base # if n <= 1, base case

# 递归调用
addi a0, a0, -1 # n - 1
call factorial # factorial(n - 1)

# 返回值在 a0 中
mul a0, s0, a0 # n * factorial(n - 1)
j end

base:
li a0, 1 # 返回 1

end:
lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret

栈对齐

RISC-V 要求栈指针 16 字节对齐:

# 分配栈空间时确保 16 字节对齐
addi sp, sp, -32 # 32 是 16 的倍数

# 如果需要保存的寄存器不足 16 字节,填充到 16 字节
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)
# 0-7 字节未使用,用于对齐

可变参数函数

可变参数函数(如 printf)需要特殊处理:

// C 代码
int printf(const char* format, ...);

可变参数的传递规则:

  • 前面固定参数正常传递
  • 可变参数部分:
    • 整数参数使用 a0-a7
    • 浮点参数使用 fa0-fa7
    • 超过寄存器数量的参数通过栈传递
# 调用 printf("Value: %d, Float: %f\n", 42, 3.14)
la a0, format # format -> a0
li a1, 42 # 42 -> a1
la t0, float_val
flw fa0, 0(t0) # 3.14 -> fa0
call printf

尾调用优化

尾调用(Tail Call)是指函数的最后一步是调用另一个函数:

// C 代码
int tail_call(int n) {
return another_function(n);
}
# RISC-V 汇编(尾调用优化)
tail_call:
# 不需要保存 ra,直接跳转
j another_function

尾调用优化可以节省栈空间:

# 未优化的版本
tail_call:
addi sp, sp, -16
sw ra, 12(sp)
call another_function
lw ra, 12(sp)
addi sp, sp, 16
ret

# 优化后的版本
tail_call:
j another_function

调用约定最佳实践

1. 正确保存寄存器

# 好的做法:只保存实际使用的被调用者保存寄存器
function:
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp) # 只保存使用的 s0

# ...

lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret

2. 最小化栈使用

# 好的做法:尽量使用寄存器
function:
# 使用临时寄存器存储中间结果
add t0, a0, a1
sub t1, t0, a2
mv a0, t1
ret

3. 保持栈对齐

# 好的做法:确保栈 16 字节对齐
function:
addi sp, sp, -32 # 32 是 16 的倍数
# ...
addi sp, sp, 32
ret

4. 清晰的函数接口

# 好的做法:使用注释说明函数接口
# int binary_search(int* arr, int n, int target)
# a0: arr - 数组地址
# a1: n - 数组长度
# a2: target - 目标值
# 返回值 a0: 索引(-1 表示未找到)
binary_search:
# ...

小结

本章介绍了 RISC-V 调用约定:

  • 寄存器分类:调用者保存和被调用者保存
  • 参数传递:前 8 个参数通过 a0-a7 传递
  • 返回值:通过 a0/a1 或 fa0/fa1 返回
  • 栈帧布局:返回地址、保存寄存器、局部变量
  • 函数调用示例:简单函数、递归函数、尾调用优化

遵循调用约定是编写正确、高效程序的基础。下一章将介绍特权架构。