risc-v中的函数调用
先来看一个普通main函数的完整执行过程(以a=b problem为例)
int main()
{
int a = 2;
int b = 3;
int c = a + b;
}
其risc-v(rv32)的汇编如下
main:
addi sp,sp,-32 # 将栈指针sp向下移动32个字节,预留栈空间
sw ra,28(sp) # 将返回地址ra存储到栈的偏移28字节处
sw s0,24(sp) # 将保存寄存器s0存储到栈的偏移24字节处
addi s0,sp,32 # 设置帧指针s0,指向当前栈顶
li a5,2 # 将立即数2加载到寄存器a5
sw a5,-20(s0) # 将a5的值存储到栈的偏移-20字节处
li a5,3 # 将立即数3加载到寄存器a5
sw a5,-24(s0) # 将a5的值存储到栈的偏移-24字节处
lw a4,-20(s0) # 从栈的偏移-20字节处加载数据到a4
lw a5,-24(s0) # 从栈的偏移-24字节处加载数据到a5
add a5,a4,a5 # 将a4和a5的值相加,结果存入a5
sw a5,-28(s0) # 将a5的值存储到栈的偏移-28字节处
li a5,0 # 将立即数0加载到寄存器a5
mv a0,a5 # 将a5的值移动到a0
lw ra,28(sp) # 从栈的偏移28字节处加载返回地址ra
lw s0,24(sp) # 从栈的偏移24字节处加载保存寄存器s0
addi sp,sp,32 # 将栈指针sp向上移动32个字节,释放栈空间
jr ra # 跳转到返回地址ra(函数返回)
一个函数的执行大致可以分为三个部分:开场(prologue),执行过程,结语(epilogue)
(这三个中文是我瞎起,别当真)
prologue
将栈指针-32进行开栈(rv32中一个字四字节,留了8个字的大小)
将返回地址(寄存器ra中的值)store到28(sp)中
将s0(帧指针fp,指向栈帧起始位置)的值store到24(sp)中
将sp+32的值赋给s0,使帧指针s0指向栈帧起始位置
执行过程
先将一个立即数放到寄存器a5,将a5的值压栈
再将另一个立即数放到寄存器a5,然后压栈
将两个立即数分别load到a4,a5寄存器,执行add指令,将结果保存在寄存器a5,然后再将a5的值store到-28(s0)
解释最后将立即数0 load到寄存器a5,然后又将这个0从a5 mv 到 a0的操作 :
首先需要区分两个概念 : 返回地址(ra存储)和返回值(a0存储)
然后,因为源代码没有指明返回值,所以默认return 0。这也就是立即数0的由来
至于为什么需要先放到寄存器a5再由a5 mv 到 a0是一个习惯问题,我自己也没咋搞懂,就先不深究
epilogue
先将在prologue时保存在栈帧上的ra和s0的值load回对应寄存器,然后将sp+32来回收栈。最后跳转到返回地址。
调用一个函数
int f(int x , int y)
{
return 0;
}
int main()
{
int a = 1;
f(1,2);
int b = 1;
}
f:
addi sp, sp, -32 # 栈指针向下移动 32 字节,分配栈帧空间
sw ra, 28(sp) # 将返回地址寄存器 `ra` 保存到栈偏移 28 字节处
sw s0, 24(sp) # 将寄存器 `s0` 保存到栈偏移 24 字节处
addi s0, sp, 32 # 设置新的帧指针 `s0` 为 `sp` 加 32 字节
sw a0, -20(s0) # 将寄存器 `a0` 的值保存到 `s0` 偏移 -20 字节处
sw a1, -24(s0) # 将寄存器 `a1` 的值保存到 `s0` 偏移 -24 字节处
li a5, 0 # 将立即数 0 加载到寄存器 `a5`
mv a0, a5 # 将寄存器 `a5` 的值移动到寄存器 `a0`
lw ra, 28(sp) # 从栈偏移 28 字节处恢复返回地址到 `ra`
lw s0, 24(sp) # 从栈偏移 24 字节处恢复 `s0` 的值
addi sp, sp, 32 # 恢复栈指针,释放 32 字节的栈帧
jr ra # 跳转到返回地址寄存器 `ra` 的地址(即返回调用者)
main:
addi sp,sp,-32 # 分配32字节的栈空间
sw ra,28(sp) # 保存返回地址到栈
sw s0,24(sp) # 保存 s0 到栈
addi s0,sp,32 # 设置 s0 指向栈顶
#######################################################################
li a5,1 # 将立即数1加载到 a5
sw a5,-20(s0) # 将 a5 的值存储到栈的偏移 -20 处
li a1,2 # 将立即数2加载到 a1
li a0,1 # 将立即数1加载到 a0
#######################################################################
call f # 调用函数 f
#######################################################################
li a5,1 # 将立即数1加载到 a5
sw a5,-24(s0) # 将 a5 的值存储到栈的偏移 -24 处
#######################################################################
li a5,0 # 将立即数0加载到 a5
mv a0,a5 # 将 a5 的值移动到 a0
lw ra,28(sp) # 从栈的偏移 28 处加载返回地址到 ra
lw s0,24(sp) # 从栈的偏移 24 处加载 s0 的值
addi sp,sp,32 # 释放栈空间
jr ra # 跳转到返回地址 ra(函数返回)
在调用函数f前,先做了一步将函数f的两个参数放入寄存器a1,a0中。这是因为寄存器a1,a0是调用者保存寄存器(a调用b的话,由a函数来管理调用者保存寄存器的内容)
调用函数f时,将保存着函数参数的a1,a0寄存器压入栈中。
这时我们可以发现,当一个参数寄存器(形式为ax)保存一个数后必定会先将其压入栈中再执行其他操作
其他操作则在第一个例子全都讲过。
递归调用
以一个斐波那契数列举例
int f(int x)
{
if(x == 1 || x == 2) return 1;
return f(x-1) + f(x-2);
}
f:
addi sp, sp, -32 # 栈指针向下移动 32 字节,分配栈帧空间
sw ra, 28(sp) # 将返回地址寄存器 `ra` 保存到栈偏移 28 字节处
sw s0, 24(sp) # 将 `s0` 寄存器保存到栈偏移 24 字节处
sw s1, 20(sp) # 将 `s1` 寄存器保存到栈偏移 20 字节处
addi s0, sp, 32 # 设置新的帧指针 `s0` 为 `sp` 加 32 字节
sw a0, -20(s0) # 将 `a0` 的值保存到 `s0` 偏移 -20 字节处
lw a4, -20(s0) # 将 `s0` 偏移 -20 字节处的值加载到 `a4`
li a5, 1 # 将立即数 1 加载到寄存器 `a5`
beq a4, a5, .L2 # 如果 `a4` == 1,则跳转到标签 .L2
lw a4, -20(s0) # 将 `s0` 偏移 -20 字节处的值加载到 `a4`
li a5, 2 # 将立即数 2 加载到寄存器 `a5`
bne a4, a5, .L3 # 如果 `a4` != 2,则跳转到标签 .L3
.L2:
li a5, 1 # 将立即数 1 加载到寄存器 `a5`
j .L4 # 跳转到标签 .L4
.L3:
lw a5, -20(s0) # 将 `s0` 偏移 -20 字节处的值加载到 `a5`
addi a5, a5, -1 # 将 `a5` 减 1
mv a0, a5 # 将 `a5` 的值移动到 `a0`
call f # 递归调用函数 `f`
mv s1, a0 # 将返回值保存到 `s1`
lw a5, -20(s0) # 将 `s0` 偏移 -20 字节处的值加载到 `a5`
addi a5, a5, -2 # 将 `a5` 减 2
mv a0, a5 # 将 `a5` 的值移动到 `a0`
call f # 递归调用函数 `f`
mv a5, a0 # 将返回值保存到 `a5`
add a5, s1, a5 # 将 `s1` 和 `a5` 的值相加
.L4:
mv a0, a5 # 将 `a5` 的值移动到 `a0`
lw ra, 28(sp) # 从栈偏移 28 字节处恢复返回地址到 `ra`
lw s0, 24(sp) # 从栈偏移 24 字节处恢复 `s0` 的值
lw s1, 20(sp) # 从栈偏移 20 字节处恢复 `s1` 的值
addi sp, sp, 32 # 恢复栈指针,释放 32 字节的栈帧
jr ra # 跳转到返回地址寄存器 `ra` 的地址(即返回调用者)
先看.L2上面那段 :
在原来的基础上,递归函数又引入了一个寄存器s1用来保存递归的中间结果。
然后将函数参数a0和1比较,若相等则跳转到.L2
和2比较,若不相等,则跳转到.L3
否则就等于2,直接进入.L2
.L2
将a5置1,跳转到.L4
.L3
将-20(s0) (也就是原本函数的参数a0)放入寄存器a5
然后将a5-1的值作为参数(传给用来保存参数的寄存器a0),之后调用f函数
将再次调用后的返回值a0保存到存中间结果的寄存器s1
如法炮制计算f(x-2),将s1和第二次返回的值相加,就是最终结果
.L4
将a5存储的最终结果交给返回值寄存器a0,最后恢复现场。