实训——RISC-V 汇编语言实践

RISC-V 指令集架构

这些指令按照功能可以分为如下几类:

整数运算指令:算术、逻辑、比较等基础运算功能。
分支转移指令:实现条件转移、无条件转移操作
加载存储指令:实现字节、半字(half word)、字(word)、双字(RV64I)的加载,存储操作,采用的都是寄存器相对寻址方式

指令格式

RISC-V有六种基本指令格式:

R 类型指令,用于寄存器-寄存器操作
I 型指令,用于短立即数和访存 load 操作
S 型指令,用于访存 store 操作
B 类型指令,用于条件跳转操作
U 型指令,用于长立即数
J 型指令,用于无条件跳转

实训

赋值语句

移位运算

示例代码

int main() {
    long long a=0xffffffffffe54b31 , b=0x0000000000004d0e;
    int shift=0x0000000000000004 ;
    long long res1, res2, res3;

    res1 = a >> shift; //FFFF FFFF FFFE 54B3
    res2 = (unsigned long long) a >> shift; //FFF FFFF FFFE 54B3
    res3 = b << shift;//4 D0E0

    return 0;
}

汇编指令

#  (1)a, b, shift 分别存放在寄存器 x28, x29, x30 中
#  (2)res1, res2, res3 存放在寄存器 x5, x6, x7 中

    sra x5, x28, x30    #算术右移,高位补原来高位
    srl x6, x28, x30    #逻辑右移, 高位补零
    sll x7, x29, x30    #逻辑左移,低位补零

R/ I 格式
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

结构控制语句

条件判断

汇编指令和含义
在这里插入图片描述

示例C语言程序1
int abs(int value) {
    int res;
    if (value >= 0) 
        res = value;
    else
        res = -value;
    return res;
}

对应汇编语句(借助符号)

# value 存储在寄存器 x10 中,res 存储在寄存器 x6 中

    blt x10, x0, negative   
positive:
    addi x6, x10, 0
    jal x0, finish
negative:
    sub x6, x0, x10
finish:
    # ...

nagative 标号表示 sub x6, x0, x10 指令的地址,sub x6, x0, x10 指令为 blt x10, x0, negative 之后的第 3 条指令,由于一条指令的长度为 4 字节,故目标地址偏移量为 3∗4=12,因此,上述 RISC-V 汇编指令段的无标号表达形式为:

    blt x10, x0, 12
    addi x6, x10, 0
    jal x0, 8
    sub x6, x0, x10
    # ...
示例C语言程序2
int main(){
    long long a, b, c;
    long long res;
//**************************
    if (a + b > c && a < c)
        res = c - a;
    else
        res = c & a;
//***************************
    return 0;
}

仅标记部分对应汇编语句(借助符号)

# a, b, c 分别存放在寄存器 x28, x29, x30 中
# res 存放在寄存器 x6 中

    add x7, x28, x29 
    slt x1, x30, x7       #a+b>c

    slt x2, x28, x30      #a<c

    and x1, x1, x2        #(a + b > c && a < c)是否为0
    beq x0, x1, negative       #if (0) ,跳转到negative
positive:
    sub x6, x30, x28
    jal x0, 8
negative:
    and x6, x30, x28
finish:
    # ...

无标号表达形式

    add x7, x28, x29 
    slt x1, x30, x7       
    slt x2, x28, x30      
    and x1, x1, x2        
    beq x0, x1, 12      
    sub x6, x30, x28
    jal x0, 8
    and x6, x30, x28
    # ...
循环
示例C语言程序1

使用 a、b 两个变量作为斐波那契数列的前两个元素,经过 10 次计算后,将斐波那契数列的第 22 个元素赋值给 res

#include <stdio.h>
int main(){
    long long a, b;
    long long res;
    int i;
    //
    for(i = 0; i < 10; ++i)
    {
        a = a + b;
        b = b + a;
    }
    res = b;
    /
    return 0;
}

仅标记部分对应汇编语句(借助符号)

#      (1)a, b, i 分别存放在寄存器 x28, x29, x30 中
#      (2)res 存放在寄存器 x6 中
star:
    addi x7,  x30, -10 
    beq  x0,  x7,  oout
    addi x30, x30, 1

inn:
    add x28, x28, x29
    add x29, x29, x28
    jal x0, star
oout:
    add x6, x29, x0

无标号表达形式

    addi x7,  x30, -10 
    beq  x0,  x7,  20
    addi x30, x30, 1
    add x28, x28, x29
    add x29, x29, x28
    jal x0, -20
    add x6, x29, x0
示例C语言程序2
	while (save[i]==k)
		i+=1;
#假设i和k对应于x22和x24,数组的基址保存在x25
Loop: 
	slli x10, x22, 3
	add  x10, x10, x25     //x10 = address of save[i]
	ld   x9,  0(x10)       //reg x9=save[i]
	bne  x9, x24, Exit  //循环判断:是否退出
	addi x22, x22, 1
	beq  x0, x0, Loop
Exit: 
	#......

子程序调用(被调用方

在这里插入图片描述

寄存器 x0 被硬编码为常量 0
寄存器 x1 用于保存返回地址,即子程序调用完成后从被调用方返回到调用方
寄存器 x2 为栈指针,存储当前栈顶的内存地址
寄存器 x5-x7、x28-x31 为临时寄存器,由调用方按需保存
寄存器 x8-x9、x18-x27 为保存寄存器,由被调用方按需保存和恢复
寄存器 x10-x11 用于传递函数返回值
寄存器 x10-x17 用于传递函数参数

函数 P 中调用函数 Q,函数 Q 接收一个整型变量作为参数,将该参数 “加 10”后作为返回结果,该过程需要完成下列步骤:

函数 P 将变量 val 作为参数传递给函数 Q
函数 P 将程序执行控制流转给函数 Q (转到函数 Q 执行)
函数 Q 获取用于存储局部变量等数据的内存空间(例如用于存放变量 tmp 的空间)
函数 Q 获取由函数 P 传递的参数并执行函数功能
函数 Q 将返回结果传递回函数 P
函数 Q 将程序执行控制流转回函数 P(返回到函数 P 中调用 Q 的位置)
函数 P 获取由函数 Q 传递的返回值,继续完成函数 P 其他功能

被调用方


long long callee(long long val_a, long long val_b){
   long long val_a_tmp = val_a & 0xffff0000;
   long long val_b_tmp = val_b & 0xffff;
   return val_a_tmp + val_b_tmp;
}
//
int main(){
    long long a, b;
    long long res;
    res = callee(a, b);
    return 0;
}

仅标记部分对应汇编语句 (测评通过,但有点冗余,后续会简化)

callee:
  addi sp,sp,-48
  sw s0,44(sp)
  addi s0,sp,48
  sw a0,-40(s0)

  sw a1,-48(s0)

  lw a4,-40(s0)
  li a5,65535
  slli a5, a5, 16
  and a5,a4,a5
  sw a5,-24(s0)

  lw a4,-48(s0)
  li a5,65536
  addi a5,a5,-1
  and a5,a4,a5
  sw a5,-32(s0)

  lw a5,-24(s0)

  lw a1,-32(s0)

  add a3,a5,a1

  mv a0,a3

  lw s0,44(sp)
  addi sp,sp,48
  jr ra

调用方

long long callee(long long val_a, long long val_b){
   long long val_a_tmp = val_a & 0xffff0000;
   long long val_b_tmp = val_b & 0xffff;
   return val_a_tmp + val_b_tmp;
}
//
long long caller(long long val_a, long long val_b){
    return callee(val_a, val_b);
}
///
int main(){
    long long a, b;
    long long res;
    res = caller(a, b);
    return 0;
}

仅标记部分对应汇编语句

caller:
    addi  sp, sp, -8
    sd    ra, 0(sp)
    jal   ra, callee
    ld    ra, 0(sp)
    addi  sp, sp, 8
    jr    ra
long long callee(long long val_a, long long val_b){
   long long val_a_tmp = val_a & 0xffff0000;
   long long val_b_tmp = val_b & 0xffff;
   return val_a_tmp + val_b_tmp;
}
int main(){
    long long a, b;
    long long res;
    res = callee(a, b);
    return 0;
}
long long callee(long long val_a, long long val_b){
   long long val_a_tmp = val_a & 0xffff0000;
   long long val_b_tmp = val_b & 0xffff;
   return val_a_tmp + val_b_tmp;
}
int main(){
    long long a, b;
    long long res;
    res = callee(a, b);
    return 0;
}

完整汇编语言程序设计

首先要理解 栈帧,我也难讲呜呜,,,

示例1
#include <stdio.h>
int n = 10;
int fibonacci_array[100];
int main() {
    fibonacci_array[0] = 1;
    fibonacci_array[1] = 1;
    int i;
    for(i = 2; i <= n; ++ i) {
        fibonacci_array[i] = fibonacci_array[i-2] + fibonacci_array[i-1];
    }
    
    printf("%d", fibonacci_array[n]);
    return 0;
}

汇编语句

.data
    n: .word 10
    fibonacci_array: .zero 400
.text
    main:
        addi x5, x0, 1
        la x6, fibonacci_array
        sw x5, 0(x6)
        sw x5, 4(x6)
        la x7, n
        lw x7, 0(x7)
        addi x28, x0, 2
        for_loop:
            blt x7, x28, end_loop
            # load fibonacci_array[i-2] to x29
            addi x29, x28, -2
            slli x29, x29, 2
            add x29, x6, x29
            lw x29, 0(x29)
            # load fibonacci_array[i-1] to x30
            addi x30, x28, -1
            slli x30, x30, 2
            add x30, x6, x30
            lw x30, 0(x30)
            # add x29 (fibonacci_array[i-2]) and x30 (fibonacci_array[i-1]) to x31
            add x31, x29, x30
            # store x31 to fibonacci_array[i]
            addi x29, x28, 0
            slli x29, x29, 2
            add x29, x6, x29
            sw x31, 0(x29)
            # i = i + 1            
            addi x28, x28, 1
            j for_loop
        end_loop:
            addi x29, x7, 0
            slli x29, x29, 2
            add x29, x6, x29
            lw x29, 0(x29)
            addi a7, x0, 1
            addi a0, x29, 0
            ecall
        addi a7, x0, 10
        ecall

在这里插入图片描述
其中:

  • 项目

    • 项目
      • 项目
  • 全局变量的定义通过汇编指示语句实现,即在 .data 段定义初始化的全局变量,在该例子中,.data 段内定义了整型变量 n,整型类型为 4 字节,对应 RISC-V 中的一个“字”,因此其定义语句为 n: .word 10,该指示语句由三部分组成,即标签、数据类型、变量值,分别对应变量名称、变量大小、变量数值;.data 段还定义了整型数组 fibonacci_array,该数组类型为整型、长度为 100,故其大小为 400 字节,其定义语句为 fibonacci_array: .zero 400,该指示语句表示为 fibonacci_array 初始化 400 字节的内存空间,每个字节数值设置为 0,即定义长度为 100 的整型数组,并将数组元素值初始化为 0。

  • 变量的赋值通过组合一系列指令实现,该例子中,局部变量 i 直接存储在 x28 寄存器中,因此 i 的赋值可直接通过运算类指令 addi 实现;实际情况下的变量通常存储在内存区域中,则此时变量的运算和赋值需要通过访存指令和运算类指令实现,如例子中对 fibonacci_array[0]=1 的实现可分为如下步骤:

  1. 运算赋值语句右边表达式的值。本例中该值为常数 1,因而直接由指令 addi x5, x0, 1 得到赋值语句的右表达式值,并将其存放在 x5 寄存器中
  2. 获取变量内存地址。本例中通过伪指令 la x6,fibonacci_array 将数组的地址存放在 x6 寄存器中
  3. 存储变量值。本例中,fibonacci_array[0] 的地址可由 fibonacci_array 作为基地址、0 (即 0 * 4)作为偏移量计算得到,因此,可由指令 sw x5, 0(x6) 将赋值语句的右表达式值(当前存储在 x5 寄存器中)存储到变量对应的内存区域
  • 库函数的调用遵循子程序调用流程,但实际实现时候输入/输出功能由系统调用实现,在本例中,printf 函数仅输出一个整型数值,该功能可由系统调用直接实现,若编译器中规定该系统调用功能编号为 1、系统调用功能选择由 a7 寄存器指定,则本例中的 printf(“%d”, fibonacci_array[n]); 语句的实现可分为如下步骤:
  1. 获取输出整型的数值。由于变量 n 的地址已经通过伪指令 la x7, n 存放在 x7 寄存器中,故由 addi x29, x7, 0 和 slli x29, x29, 2 两条指令计算 fibonacci_array[n] 相对于 fibonacci_array 的偏移量(由于数组类型为整型,故偏移量为 n * 4,实际可通过逻辑左移两位实现等价功能);此例中数组 fibonacci_array 的地址已经通过伪指令 la x6, fibonacci_array 存放在 x6 寄存器中,因此 fibonacci_array[n] 的实际地址可以通过 x6 寄存器内容加上偏移量得到,即指令 addi x29, x6, x29 计算 fibonacci_array[n] 的实际地址并将其存放在 x29 寄存器中;最后,通过 lw x29, 0(x29) 指令将该数组元素的实际值从内存中加载到 x29 寄存器
  2. 设置系统调用编号及参数。本例使用的 RISC-V 汇编语言假设约定系统调用编号存放在 a7 寄存器中、参数存放在 a0 寄存器中,且打印整型数值的系统调用编号为 1,因此,可以通过指令 addi, a7, x0, 1 设置选择打印整型数值,通过指令 addi a0, x29, 0 将获取的整型数值存放在参数寄存器中,通过指令 ecall 完成系统调用
  • main 函数执行 return 0; 后实际程序还有其他处理流程,本例子中假设 main 函数执行 return 0; 表示程序完成功能、退出执行,因而可以通过 10 号系统调用退出程序执行。注意:实际的完整汇编程序中,return 0; 语句执行后会返回到调用 main 函数的地方继续执行
示例2
#include <stdio.h>
long long int n = VALUE;
long long int fact (long long int n) {
    if (n < 1) return 1;
    else return n * fact(n - 1);
}
int main() {
    long long int res = fact(n);
    printf("%lld", res);
    return 0;
}

汇编语句

.data
    n: .dword VALUE

.text
    main:
        addi sp, sp, -32
        sd  ra, 24(sp)
        sd  s0, 16(sp)
        addi s0, sp, 32

        la a0, n 
        lw  a0,0(a0)
        call fact

        addi a7, x0, 1
        ecall

        addi a7, x0, 10
        ecall
        
    fact:
        addi sp sp -32
        sd ra 24(sp)
        sd s0 16(sp)
        addi s0 sp 32
        sd a0 -24(s0)
        ld x15 -24(s0)
        blt x0 x15 L1
        addi x15 x0 1
        jal x0 L2
    L1:
        ld x15 -24(s0)
        addi x15 x15 -1
        addi a0 x15 0
        jal ra fact
        addi x14 a0 0
        ld x15 -24(s0)
        mul x15 x14 x15
    L2:
        addi a0 x15 0
        ld ra 24(sp)
        ld x8 16(sp)
        addi sp sp 32
        jalr x0 ra 0

有点难

没完全理解,后面会加进理解后持续更新

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个优化后的RISC-V递归求斐波那契数列的代码: ``` .global main .data fib1: .word 1 fib2: .word 1 n: .word 10 .text main: lw a0, n # 加载n的值 addi sp, sp, -16 # 分配栈空间 sw ra, 12(sp) # 保存返回地址 sw s0, 8(sp) # 保存s0的值 addi s0, sp, 16 # 设置s0的值为栈顶地址 jal fib # 调用递归函数 lw ra, 12(sp) # 恢复返回地址 lw s0, 8(sp) # 恢复s0的值 addi sp, sp, 16 # 释放栈空间 li a7, 10 # 系统调用退出程序 ecall fib: addi sp, sp, -16 # 分配栈空间 sw ra, 12(sp) # 保存返回地址 sw s0, 8(sp) # 保存s0的值 sw a0, 0(sp) # 保存参数n的值 addi s0, sp, 16 # 设置s0的值为栈顶地址 lw a0, 0(sp) # 加载参数n的值 li a1, 2 # 设置常数2 blt a0, a1, base # n < 2,直接返回1 addi a0, a0, -1 # 调用递归函数计算fib(n-1) jal fib sw a0, -4(s0) # 保存fib(n-1)的结果 addi a0, a0, -2 # 调用递归函数计算fib(n-2) jal fib lw a1, -4(s0) # 加载fib(n-1)的结果 add a0, a1, a0 # 计算fib(n)的结果 lw ra, 12(sp) # 恢复返回地址 lw s0, 8(sp) # 恢复s0的值 lw a0, 0(sp) # 恢复参数n的值 addi sp, sp, 16 # 释放栈空间 jr ra base: li a0, 1 # n < 2,返回1 j end end: jr ra ``` 这个代码使用了栈来保存递归函数的返回地址和s0寄存器的值。在每次递归调用之前,它会将这些值保存在栈上,并在递归完成后恢复它们。这样可以确保每次递归调用都有自己的返回地址和s0寄存器的值。 此外,该代码还使用了一个基本情况来处理n < 2的情况,这样就可以避免无限递归的问题。如果n < 2,则直接返回1。 最后,该代码还将fib(n-1)的结果保存在栈上,以便在计算fib(n)时可以直接使用。这样可以避免重复计算fib(n-1)的问题。 综上所述,这个代码使用了栈来保存状态,处理了基本情况,并避免了重复计算的问题,从而优化了递归求斐波那契数列的性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值