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,最后恢复现场。

posted @ 2024-06-10 21:42  拾墨、  阅读(131)  评论(0编辑  收藏  举报