首页 > 其他分享 >risc-v中的函数调用

risc-v中的函数调用

时间:2024-06-10 21:43:49浏览次数:22  
标签:sp 字节 s0 risc 函数调用 a0 a5 寄存器

先来看一个普通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,最后恢复现场。

标签:sp,字节,s0,risc,函数调用,a0,a5,寄存器
From: https://www.cnblogs.com/algoshimo/p/18241088

相关文章

  • C语言杂谈:函数栈帧,函数调用时到底发生了什么
            我们都知道在调用函数时,要为函数在栈上开辟空间,函数后续内容都会在栈帧空间中保存,如非静态局部变量,返回值等。这段空间就叫栈帧。    当函数调用,就会开辟栈帧空间,函数返回时,栈帧空间就会被释放。这里的释放并非清空,而是让其无效化,可以后续的使用。1,......
  • GLM-4-9B领先!伯克利函数调用榜单BFCL的Function Calling评测方法解析与梳理
    智谱公布的GLM-4-9B基于BFCL榜单的工具调用能力测试结果©作者|格林来源|神州问学在智谱最新开源的GLM-4-9B-Chat中,其工具调用能力在BFCL(伯克利函数调用排行榜)榜上获得了超高的总BFCL分,和gpt-4-turbo-2024-04-09几乎不相上下。在榜单中,还提到了AST总分以及Exec总分两个......
  • 深入浅出CPU眼中的函数调用&栈溢出攻击
    深入浅出CPU眼中的函数调用——栈溢出攻击原理解读函数调用,大家再耳熟能详了,我们先看一个最简单的函数:#include<stdio.h>#include<stdlib.h>intfunc1(inta,intb){ intc=a+b;returnc;}intmain(){intres=func1();printf("%d",res);}函......
  • 深入探讨Function Calling:实现外部函数调用的工作原理
    引言FunctionCalling是一个允许大型语言模型(如GPT)在生成文本的过程中调用外部函数或服务的功能。FunctionCalling允许我们以JSON格式向LLM模型描述函数,并使用模型的固有推理能力来决定在生成响应之前是否调用该函数。模型本身不执行函数,而是生成包含函数名称和执行函数......
  • RISC-V 汇编语言--bbci 指令
    bbci 指令是RISC-V汇编语言中的一个条件分支指令,全称为"BranchifBitisClearandIncrement"。该指令会检查指定寄存器中的某一位是否被清除(即为0),如果是,则跳转到指定的标签或地址执行代码。在执行跳转之前,它还会将该位设置为1(即对该位进行“置位”操作)。具体来说,bbci ......
  • RISC-V精简指令集(RISC)介绍
    目录一 RISC-V的常用指令:二 RISC-V指令集实例:1. 基础算术和逻辑操作2. 加载和存储操作3. 控制流指令4. 其他指令一 RISC-V的常用指令:RISC-V是一个基于精简指令集(RISC)原则的开源指令集架构(ISA),其指令集设计简洁、高效,并且具有可扩展性。以下是一些RISC-V的常用......
  • Risc-V 移植 ssh 与 sftp 记录
    Risc-V移植ssh与sftp记录关于Risc-V  天下苦intel久矣,而ARM的授权费也不低,导致市面上的SOC要么都很贵,要么厂家和品类没那么丰富,全志、ST、TI、RK、晶晨、海思、高通、联发科。。。都是中大规模的公司,小厂做不了,感觉限制了它的发展。  后来出了个Risc-V指令......
  • openAI assistants的自定义函数调用——类似HiAgent
    openAIassistants的自定义函数调用功能先添加函数: 和字节的hiagent非常相似。定义好函数以后。然后就是在客户端通过如下代码调用:  #读取系统变量fromdotenvimportload_dotenvload_dotenv()fromopenaiimportOpenAI#初始化客户端client=OpenAI()......
  • 不同场景下的构造函数调用
    本文为对不同场景下的构造函数调用进行跟踪。构造函数默认情况下,在C++之后至少存在六个函数默认构造/析构函数,复制构造/复制赋值,移动构造/移动赋值。以下代码观测发生调用的场景#include<iostream>structFoo{Foo():fd(0){std::cout<<"Foo::Foo()this="<<......
  • 函数调用、函数参数、类型提示、名称空间
    【一】函数的调用方法【1】直接调用函数defstudent(name,age):print(f"mynameis{name}andmyageis{age}")直接调用函数student(name='qwer',age=22)【2】用表达式调用函数用一个新变量存储函数的内存地址然后调用defadd(x,y):returnx+yresult=......