首页 > 编程语言 >一起学RISC-V汇编第9讲之RISC-V ABI之函数调用

一起学RISC-V汇编第9讲之RISC-V ABI之函数调用

时间:2024-10-07 19:11:07浏览次数:8  
标签:int ABI sum float RISC 函数调用 传递 XLEN 寄存器

目录

1 RISC-V ABI接口

ABI(Application Binary Interface)为应用程序二进制接口,它定义了应用程序之间或应用程序和操作系统之间进行二进制级交互时必须遵循的规则和约定。ABI包括了关于函数调用约定(参数传递,函数返回值等)、数据类型、对齐方式、字节序、 函数栈布局、系统调用等方面的规范。

具体来说,ABI定义了以下内容:

  1. 数据类型、对齐方式和字节序;
  2. 寄存器使用约定:哪些寄存器用于传递函数参数、返回值和保存临时变量;
  3. 函数调用约定:函数调用的具体步骤,包括参数传递、返回值处理等;
  4. 栈的使用:栈的增长方向、如何保存和恢复栈指针等;
  5. 系统调用:操作系统提供的服务调用方式和参数传递规则;

ABI的存在使得不同的编程语言、编译器和操作系统之间能够进行二进制级的互操作,确保它们能够正确地调用和使用彼此所生成的代码。由于不同的体系结构和操作系统可能有不同的ABI,因此跨平台开发时需要考虑ABI的兼容性。

这里只讲函数调用约定、寄存器使用约定,栈帧这几部分内容,内容放在一起比较长,所以文章分三部分。

2 RISC-V 函数调用约定

函数调用约定主要是约定怎么传递函数参数、怎么返回值。

第2讲寄存器这一章,列出了32个通用寄存器以及32个浮点寄存器:

image-20240409222851399

一般规则如下:

  1. 入参的传递:函数调用时,优先使用 a0-a7 这8个标量寄存器(对于浮点类型是fa0-fa7这8个浮点寄存器)来传递入参
  2. 返回值的传递:一般用a0-a1来传递(浮点数用fa0-fa1来传递)返回值

下面讲述更详细的规则。

2.1 入参的传递

但是如果入参超过8个,只用寄存器不够该怎么办?如果函数入参是结构体类型该如何处理呢?如果入参有标量又有浮点数该如何处理呢?

有如下几种情况:

  • 当一个标量位宽不超过XLEN位或者一个浮点实数参数不超过FLEN位时,使用单个参数寄存器传递。若没有可用的参数寄存器,则在栈上传递
  • 当一个标量位宽超过XLEN位但是不超过2×XLEN时,则可以在一对参数寄存器中传递,低XLEN位在小编号寄存器中,高XLEN位在大编号寄存器中;若没有可用的参数寄存器,则在栈上传递标量;若只有一个寄存器可用,则低XLEN位在寄存器中传递,高XLEN位在栈上传递
  • 若一个标量宽度大于2×XLEN,则通过引用传递(栈传递),并在参数列表中用地址替换。通过引用传递的实参可以由被调用方修改

常见的有如下几个场景:

几种场景 参数列表 参数传递
情形1 n1,n2,n3 a0,a1,a2
情形2 n1,n2,n3,n4,n5,n6,n7,n8,n9,n10 a0,a1,a2,a3,a4,a5,a6,a7,a8,stack
情形3 l1, l2 a0,a1,a2,a3
情形4 s1,s2,s3
d1,d2,d3
s1,d1,d2
fa0,fa1,fa2
情形5 s1,s2,s3,s4,s5,s6,s7,s8,s9,s10 fa0,fa1,fa2,fa3,fa4,fa5,fa6,fa7,a0,a1
情形6 n1,n2,n3,s1,d1 a0,a1,a2,fa0,fa1

表中:n1,n2... 表示标量位宽不超过XLEN的整数,l1,l2...表示标量位宽超过XLEN,不超过2×XLEN的整数,s1,s2...表示单精度浮点,d1,d2...表示双精度浮点。

举例如下:

情形1:标量位宽不超过XLEN,且函数入参个数小于8,使用寄存器传递

源码:

#include <stdio.h>

int test(int arg0, int arg1, int arg2, int arg3, int arg4,
         int arg5, int arg6, int arg7)
{
    int sum = 0;
    sum += arg0 + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7;
    return sum;
}

int main(void)
{
    int sum = 0;
    sum = test(1, 2, 3, 4, 5, 6, 7, 8);
    printf("sum = %d\r\n", sum);
    return 0;
}

例子中,标量位宽为32bit,XLEN=32bit(-march=rv32imafdc),从反汇编可以看出,入参使用的是a0-a7寄存器传递,返回值使用的是a0

image-20240618230302545

情形2:标量位宽不超过XLEN,且函数入参个数大于8,前8个参数使用寄存器传递,剩下的使用栈stack传递

源码:

#include <stdio.h>

int test(int arg0, int arg1, int arg2, int arg3, int arg4,
         int arg5, int arg6, int arg7, int arg8, int arg9)
{
    int sum = 0;
    sum += arg0 + arg1 + arg2 + arg3 + arg4 + arg5 +
           arg6 + arg7 + arg8 + arg9;
    return sum;
}

int main(void)
{
    int sum = 0;
    sum = test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    printf("sum = %d\r\n", sum);
    return 0;
}

例子中,标量位宽为32bit,XLEN=32bit(-march=rv32imafdc),从反汇编可以看出,前8个入参使用的是a0-a7寄存器传递,后面两个入参使用的是栈传递(caller 将多的数据存到栈中,callee在栈对应位置取数),返回值使用的是a0

image-20240618230152517

情形3:标量位宽超过XLEN,但是不超过2×XLEN时,则可以使用一对寄存器来保存一个入参

源码:

#include <stdio.h>

long long int test(long long int arg0, long long int arg1,
                   long long int arg2, long long int arg3)
{
    long long int sum = 0;
    sum += arg0 + arg1 + arg2 + arg3;
    return sum;
}

int main(void)
{
    long long int sum = 0;
    sum = test(1, 2, 3, 4);
    printf("sum = %ld\r\n", sum);
    return 0;
}

例子中,标量位宽为64bit,XLEN=32bit(-march=rv32imafdc),从反汇编可以看出,使用2个标量寄存器保存一个入参,且低XLEN位在小编号寄存器中,高XLEN位在大编号寄存器中,若入参没有足够可用的参数寄存器,则在栈上传递标量;若只有一个寄存器可用,则低XLEN位在寄存器中传递,高XLEN位在栈上传递。

image-20240618231430562

情形4:如果数据类型是浮点,位宽不超过FLEN,且函数入参个数小于8,使用寄存器传递

源码:

#include <stdio.h>

float test(float arg0, float arg1, float arg2, float arg3, float arg4,
         float arg5, float arg6, float arg7)
{
    float sum = 0;
    sum += arg0 + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7;
    return sum;
}

float main(void)
{
    float sum = 0;
    sum = test(1, 2, 3, 4, 5, 6, 7, 8);
    printf("sum = %d\r\n", sum);
    return 0;
}

例子中,标量位宽为32bit,FLEN=64bit(-march=rv32imafdc),从反汇编可以看出,入参使用的是fa0-fa7寄存器传递,返回值使用的是fa0

image-20240622180443532

情形5:如果数据类型是浮点,位宽不超过FLEN,且函数入参个数大于8,如果标量寄存器有剩余,则可以使用标量寄存器来传递浮点参数

源码:

#include <stdio.h>

float test(float arg0, float arg1, float arg2, float arg3, float arg4,
         float arg5, float arg6, float arg7, float arg8, float arg9)
{
    float sum = 0;
    sum += arg0 + arg1 + arg2 + arg3 + arg4 + 
           arg5 + arg6 + arg7 + arg8 + arg9;
    return sum;
}

float main(void)
{
    float sum = 0;
    sum = test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    printf("sum = %d\r\n", sum);
    return 0;
}

例子中,FLEN=64bit(-march=rv32imafdc),从反汇编可以看出,前8个入参使用的是fa0-fa7寄存器传递,后面两个入参使用的标量寄存器a0, a1传递

image-20240622181654041

情形6:入参包括整数,单精度浮点,双精度浮点

源码:

#include <stdio.h>

double test(int arg0, int arg1, float arg2, double arg3)
{
    double sum = 0;
    sum += arg0 + arg1 + arg2 + arg3;
    return sum;
}

double main(void)
{
    double sum = 0;
    sum = test(1, 2, 3.0f, 4.0);
    printf("sum = %f\r\n", sum);
    return 0;
}

例子中,XLEN=32bit,FLEN=64bit(-march=rv32imafdc),从反汇编可以看出,标量用a0-a7传递,浮点数用fa0-fa7传递。

image-20240622182333942

情形7:结构体作为入参

又分如下几种情况:

  1. 若结构体的宽度不超过XLEN位,则这个结构体可以在寄存器中传递,若没有可用的寄存器,则在栈上传递。

  2. 若结构体的宽度超过XLEN位,不超过2×XLEN位,则可以在一对寄存器中传递。若只有一个寄存器可用,则结构体的前半部分在寄存器中传递,后半部分在栈上传递;若没有可用的寄存器,则在栈上传递结构体。

  3. 若一个结构体的宽度大于2×XLEN位,则通过引用传递,并在参数列表中被替换为地址。传递到栈上的结构体会对齐到类型对齐和XLEN中的较大者,但不会超过栈对齐要求。

2.2 返回值的传递

几种场景 返回值类型 返回值存放
情形1 void 当函数没有返回值时,不需要考虑返回寄存器的处理
情形2 标量(整形或指针类型)位宽不超过XLEN a0
情形3 标量(整形或指针类型)位宽超过XLEN,不超过2xXLEN a0,a1
情形4 单精度浮点(float)或 双精度浮点(double) fa0
情形5 struct 需要细分,如果寄存器能放下使用寄存器,否则用栈

情形1:当函数没有返回值时,不需要考虑返回寄存器的处理

情形2:当函数返回类型是标量(整形或指针类型)位宽不超过XLEN,返回值存放在整型寄存器a0上

情形3:当函数返回类型是标量(整形或指针类型)位宽超过XLEN,不超过2xXLEN,返回值存放在整型寄存器a0,a1上

同样举例:

#include <stdio.h>

long long test(int arg0, int arg1, int arg2, int arg3, int arg4,
         int arg5, int arg6, int arg7)
{
    long long sum = 0;
    sum += arg0 + arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7;
    return sum;
}

int main(void)
{
    long long sum = 0;
    sum = test(1, 2, 3, 4, 5, 6, 7, 8);
    printf("sum = %ld\r\n", sum);
    return 0;
}

例子中,标量位宽为64bit,XLEN=32bit(-march=rv32imafdc),即返回值位宽超过XLEN,但是不超过2×XLEN,从反汇编可以看出,使用的是a0、a1来存放返回值。

image-20241005234556396

情形4:当函数返回类型是单精度浮点或双精度浮点(float 或double),返回值存放在整型寄存器fa0上

情形5:返回值为结构体(struct),返回值需要细分,如果寄存器能放下则使用寄存器(标量使用a0/a1, 浮点使用fa0/fa1),否则用栈传递

寄存器能放下的情况,结构体放到a0 a1上返回:

#include <stdio.h>

typedef struct Point {
    int x;
    int y;
} Point;

Point create_point(int x, int y) {
    Point p = {x, y};
    return p;
}

int main() 
{
    Point my_point = create_point(10, 20);
    printf("Point: (%d, %d)\n", my_point.x, my_point.y);
    
    return 0;
}

image-20241006095115309

寄存器不能放下的情况,使用栈返回:

#include <stdio.h>

typedef struct Point {
    int x;
    int y;
    int z;
} Point;

Point create_point(int x, int y, int z) {
    Point p = {x, y, z};
    return p;
}

int main() 
{
    Point my_point = create_point(10, 20, 30);
    printf("Point: (%d, %d, %d)\n", my_point.x, my_point.y, my_point.z);
    
    return 0;
}

image-20241006193633534

参考:

  1. Releases · riscv-non-isa/riscv-elf-psabi-doc (github.com)
  2. RISC-V函数调用规范 - 知乎 (zhihu.com)

标签:int,ABI,sum,float,RISC,函数调用,传递,XLEN,寄存器
From: https://www.cnblogs.com/sureZ-learning/p/18450437

相关文章

  • SciTech-Mathmatics-Probability+Statistics:Quantifing Uncertainty_统计分析: SciTec
    一般数学表示方法概率数学表示方法\(\large\begin{array}{rl}\\\bm{X}:&符合某种概率分布的Random\Variable(随机变量)\\\bm{x}:&\bm{rnorm},随机变量X的一个实例,,…\\\bm{f}:&\bm{pdf},\bm{dnorm},\text{ProbabilityDistributionFunction}\of\\text......
  • SciTech-Mathmatics-Probability+Statistics:Quantifing Uncertainty_多元数据统计分
    Chapt1学习目标理解多元数据及多元统计分析与一元统计分析的区别。掌握数据的计量尺度与数据类型。了解多元统计分析的应用分类。1.1 多元数据认知1.1.1多元数据的概念对任何一个现实问题要转化为一个统计问题,首要的工作是要对其特征进行刻画;一般采用随机变量,多......
  • SciTech-Mathmatics-Probability+Statistics:Quantifing Uncertainty_统计分析: SciTec
    一般数学表示方法概率数学表示方法\(\large\begin{array}{rl}\\\bm{X}:&符合某种概率分布的Random\Variable(随机变量)\\\bm{x}:&\bm{rnorm},随机变量X的一个实例,,…\\\bm{f}:&\bm{pdf},\bm{dnorm},\text{ProbabilityDistributionFunction}\of\\text......
  • SciTech-Mathmatics-Probability+Statistics:Quantifing Uncertainty_统计数据分析:
    多元数据和多元统计分析<<实用多元统计分析>>清华大学出版社,5校正文1.indd12023/9/1217:14:25Chapt1学习目标理解多元数据及多元统计分析与一元统计分析的区别。掌握数据的计量尺度与数据类型。了解多元统计分析的应用分类。1.1 多元数据认知1.1.1多元数据的......
  • Databinding(kotlin)
    简单使用(只作为view获取)build.gradle.kts配置android{dataBinding{enable=true}}activity注入//setContentView(R.layout.activity_main)valbinding:ActivityMainBinding=DataBindingUtil.setContentView(this,R.layout.activity_main......
  • [题解]P7077 [CSP-S2020] 函数调用
    P7077[CSP-S2020]函数调用题意简述给定一个长度为\(n\)的序列\(a_1,a_2,\dots,a_n\),给定\(m\)个函数,每个函数可能是下面\(3\)种类型,用\(T_x\)表示函数\(x\)的类型:\(T_x=1\),对下标\(p\)增加\(v\)。\(T_x=2\),对所有元素乘\(v\)。\(T_x=3\),由若干类型\(1\)和类型\(2\)组成......
  • 题解:TopCoder12316 ThreeColorability
    Vjudge可以出成《三色绘恋》背景。题意给一个格点数为\((n+1)\times(m+1)\)的网格,给格点染色,相邻的格点不能染成同样的颜色。每个格子有一条对角线的边,可选N形和Z形。现在有一个残缺的网格,存在一些格子的对角线连法不确定,构造一种字典序最小的方案使得至少存在一种染色......
  • PICO 2 RP2350使用官方推荐RISC-V编译器在O3优化下的coremark跑分,与Hazard3库宣传跑分
    编译环境:WSLUbuntu22.04GCC13.2.0 Hazard3存储库https://github.com/Wren6991/Hazard3/RP2350默认频率150MHz,编译内核为其RISC-V架构内核,在此频率下实测O3等级跑分453左右,O2等级跑分429左右。在测试时,当我打开第二个核心后,并且第二个核心只用来控制led灯,此时coremark跑......
  • RISC-V
    异常时只在与当前RISC-V线程中的指令相关联的运行时发生的不寻常条件术语trap陷阱指由RISC-V线程中发生的异常情况导致的控制同步转移到陷阱处理程序。、术语中断指与当前RISC-V线程异步发生的外部事件。当必须服务的中断发生时,一些指令被选择接收中断异常,随后遇到陷阱。数据......
  • 从汇编视角解析函数调用中的堆栈运作
    引言汇编语言是计算机硬件操作的最直接表达方式,通过汇编代码可以深入理解计算机底层的工作机制。本文将以一个简单的C语言代码为例,深入分析其对应的汇编代码中的堆栈变化,探讨计算机在执行过程中如何通过堆栈来进行函数调用、参数传递和结果返回。C语言代码与汇编代码概述我们......