首页 > 其他分享 >RISC-V指令精讲(一):算术指令实现与调试

RISC-V指令精讲(一):算术指令实现与调试

时间:2022-10-03 23:11:08浏览次数:55  
标签:函数 精讲 RISC ins a0 指令 寄存器 addi

本节来看下RV32I(32位整数指令集)的算数指令,先学习下加减指令(add、sub),接着了解下数值比较指令(slt),这些指令都有两个版本:一个是立即数版本,一个是寄存器版本

RISCV-V指令格式

RISC-V 机器指令是一种三操作数指令,其对应的汇编语句格式如下:

指令助记符 目标寄存器,源操作数1,源操作数2

例如“add a0,a1,a2”,其中 add 就是指令助记符,表示各种指令,add 是加法指令;a0 是目标寄存器,目标寄存器可以是任何通用寄存器;a1,a2 是源操作数 1 与源操作数 2,源操作数 1 可以是任何通用寄存器,源操作数 2 可以是任何通用寄存器和立即数。立即数就是写指令中的常数,比如 0、1、100、1024 等。

加法指令

一个 CPU 要执行基本的数据处理计算,加减指令是少不了的,否则基础的数学计算和内存寻址操作都完成不了,用这样的 CPU 做出来的计算机将毫无用处。

立即数加减法如何实现

加法指令有两种形式。

  • 一种形式是一个寄存器和一个立即数相加,结果写入目标寄存器,我们称之为立即数加法指令
  • 另一种形式是一个寄存器和另一个寄存器相加,结果写入目标寄存器,我们称之为寄存器加法指令。

立即数加法指令,形式如下:

addi rd,rs1,imm
#addi 立即数加法指令
#rd 目标寄存器
#rs1 源寄存器1
#imm 立即数

上述代码 rd、rs1 可以是任何通用寄存器。 imm 立即数可以是** -2048~2047,其完成的操作是将 rs1 寄存器里的值加上立即数,计算得到的数值会写到 rd 寄存器当中,也就是 rd = rs1 + imm**。

先构建一个 main.c 文件,在里面用 C 语言写上 main 函数,想让链接器工作这一步必不可少。接着,我们写一个汇编文件 addi.S,并在里面用汇编写上 addi_ins 函数
addi_ins 函数的代码如下所示:

addi_ins:
    addi a0,a0,5          #a0 = a0+5,a0是参数,又是返回值,这样计算结果就返回了
    jr ra          #函数返回

C 函数的函数名对应到汇编语言中就是标号,这里加上一条“jr ra”返回指令,就构成了一个 C 语言中的函数。

这里 a0 寄存器里的数值即是 C 语言函数里的第一个参数,也是返回值。所以这个汇编函数完成的功能,就是把传递进来的参数加上 5,再把这个结果作为返回值返回。

在C语言的main函数中调用addi_ins,然后打印一个结果:

#include "stdio.h"
int addi_ins(int x); //声明一下汇编语言中的函数:addi_ins
int main()
{
    int result = 0;
    result = addi_ins(4);    //result = 9 = 4 + 5
    printf("This result is:%d\n", result);
    return 0;
}

运行结果:
image

上图中是程序刚刚执行完 addi a0,a0,5 指令之后,执行 jr ra 指令之前的状态。可以看到 a0 寄存器中的值已经变成了 9,这说明运算的结果是正确的。

addi_ins 函数返回后,输出的结果如下图所示:
image

在 addi.S 文件中再写一个函数,也就是 addi_ins2 函数,代码如下所示:

.globl addi_ins2
addi_ins2:
    addi a0,a0,-2048       #a0 = a0-2048,a0是参数,又是返回值,这样计算结果就返回了
    jr ra                   #函数返回

addi_ins2 函数的指令和 addi_ins 函数一样,只不过立即数变成了负数。我们很清楚所谓减法就是加上一个负数,所以通过 addi_ins2 函数就实现了立即数减法指令。

同样地,在 main 函数中调用它,代码如下所示:

#include "stdio.h"
int addi_ins(int x); //声明一下汇编语言中的函数:addi_ins
int addi_ins2(int x); //声明一下汇编语言中的函数:addi_ins2
int main()
{
    int result = 0;
    result = addi_ins(4);    //result = 9 = 4 + 5
    printf("This result is:%d\n", result);
    result = addi_ins2(2048);    //result = 0 = 2048 - 2048
    printf("This result is:%d\n", result);
    return 0;
}

按下“F5”键调试一下,第二个 printf 输出的结果为 0,因为 2048-2048 肯定等于 0。如下所示:
image
和之前一样,上图中是刚刚执行完 addi a0,a0,-2048 指令之后,执行 jr ra 指令之前的状态。这时 a0 寄存器中的值已经变成了 0,这说明运算的结果正确。

addi_ins2 函数返回后,输出的结果如下图所示:
image

上图中已经证明了结果符合我们的预期,用 addi 指令完成了立即数的减法计算。这也是 RISC-V 指令集中没有立即数据减法指令的原因。为了保证这一特性,所有的立即数必须总是进行符号扩展,这样就可以用立即数表示负数,所以我们并不需要一个立即数版本的减法指令。

为了进一步搞清楚这条指令的机器码数据,看下 addi_ins 函数和 addi_ins2 函数的二进制数据什么样。

打开工程目录下的 addi.bin 文件,如下所示:
image
以上是四条指令数据,其中两个 0x00008067 数据为两个函数的返回指令,即:jr ra,0x00550513,它对应的汇编语句 addi a0,a0,5,0x80050513,对应汇编语句 addi a0,a0,-2048。

来详细拆分一下 addi 指令的各位段的数据,看看它是如何编码的。
image
对照上图,可以看到一条指令数据为 32 位,其中操作码占 7 位,目标寄存器和或者源寄存器各占 5 位。通过 5 位二进制数,正好可以编码 32 个通用寄存器。上图中寄存器编码对应 10,正好是 x10,也即 a0 寄存器,立即数占 12 位。由于 RISC-V 指令总是按有符号数编码,所以立即数只能表示 -2048~2047 的范围。

寄存器版本的加减法如何实现

寄存器版本的加法指令的形式如下:

add rd,rs1,rs2
#add 加法指令
#rd 目标寄存器
#rs1 源寄存器1
#rs2 源寄存器2

类似立即数加法指令,寄存器版本的加法指令也是两个源寄存器相加,结果放在目标寄存器中,代码中 rd、rs1、rs2 可以是任何通用寄存器,计算操作也和前面 addi 指令一样。

通过写代码来做个验证,写一个 addsub.S 文件,并在其中用汇编写上 add_ins 函数 ,如下所示:

add_ins:
    add a0,a0,a1          #a0 = a0+a1,a0、a1是C语言调用者传递的参数,a0是返回值,这样计算结果就返回了
    jr ra                   #函数返回

a0,a1 是 C 语言函数调用的第一、二个参数

用 VSCode 打开工程目录,按下“F5”键调试一下,输出的结果为 2,因为 1+1 的结果肯定等于 2。

image
上图展示的是执行完 add a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。这时 a0 寄存器中的值确实已经变成了 2,这说明运算的结果正确。

当 add_ins 函数返回后,输出的结果如下图所示:
image

这个结果证明了 add 指令执行的结果符合我们的预期

在 addsub.S 文件中再写一个函数,也就是 sub_ins 函数,代码如下:

sub_ins:
    sub a0,a0,a1          #a0 = a0-a1,a0、a1是C语言调用者传递的参数,a0是返回值,这样计算结果就返回了
    jr ra                   #函数返回

这段代码就是减法指令,和加法指令的模式一样,除了助记符是 sub,实现的操作是 a0 = a0 - a1。sub 指令后的目标寄存器、源寄存器可以是任何通用寄存器

F5”键调试一下,其结果应为 1,如下所示:
image

上图中依然是执行完 sub a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。这时 a0 寄存器中的值确实已经变成 1 了,证明运算结果没问题。

当 sub_ins 函数返回后,就会输出下图所示的结果。
image

经过调试,sub 指令执行的结果也符合我们的预期了。
继续研究机器编码,来看看 add_ins 函数和 sub_ins 函数的二进制数据。打开工程目录下的 addsub.bin 文件,如下所示:
image

以上 4 个 32 位数据是四条指令,其中两个 0x00008067 数据是两个函数的返回指令即:jr ra,0x00b50533 为 add a0,a0,a1,0x40b50533 为 sub a0,a0,a1。

来拆分一下 add、sub 指令的各位段的数据,看看它们是如何编码的。如下所示:
image
从图里可以看到,操作码占了 7 位,目标寄存器和两个源寄存器它们各占 5 位。目标寄存器和源寄存器编码对应 10,正好是 x10,即 a0 寄存器。而源寄存器 2 编码对应 11,正好是 x11 也即是 a1。其它位段为功能编码,add、sub 指令就是用高段的功能码区分的。

比较指令

现在大多数处理器都会包含数据比较指令,用于判断数值大小,以便做进一步的处理。

有无符号立即数版本:slti、sltiu 指令

RISC-V 指令集中有四条比较指令,这四条又分为有无符号立即数版本和有无符号寄存器版本,分别是 slti、sltiu、slt、sltu

slti、sltiu 指令的形式如下所示:

slti rd,rs1,imm
#slti 有符号立即数比较指令
#rd 目标寄存器
#rs1 源寄存器1(有符号数据)
#imm 有符号立即数(-2048~2047)
sltiu rd,rs1,imm
#sltiu 无符号立即数比较指令
#rd 目标寄存器
#rs1 源寄存器1(无符号数据)
#imm 有符号立即数(-2048~2047)

上述代码中 rd、rs1 可以是任何通用寄存器。有、无符号是指 rs1 寄存器中的数据,有符号立即数 imm 的数值范围是 -2048~2047。

slti、sltiu 完成的操作用伪代码描述如下:

if(rs1 < imm)
    rd = 1;
else
    rd = 0;

下一步又到了写代码验证的环节。建立一个 slti.S 文件,在其中用汇编写上 slti_ins、sltiu_ins 函数,然后写下这两个函数:

.global slti_ins
slti_ins:
    slti a0, a0, -2048      #if(a0<-2048) a0=1 else a0=0,a0是参数,又是返回值,这样计算结果就返回了
    jr ra                   #函数返回

.global sltiu_ins
sltiu_ins:
    sltiu a0,a0,2047      #if(a0<2047) a0=1 else a0=0,a0是参数,又是返回值,这样计算结果就返回了
    jr ra                   #函数返回

slti_ins 与 sltiu_ins 函数分别执行了 slti 和 sltiu 指令,都是拿 a0 寄存器和一个立即数比较,如果 a0 小于立即数就把 1 写入 a0 寄存器。

运行结果:
image
上图中是执行完 slti a0,a0,-2048 指令之后,执行 jr ra 指令之前的状态。如果看到 a0 寄存器中的值确实已经变成 1 了,就说明运算的结果是正确的。

当 slti_ins 函数返回后,输出的结果如下所示:
image

因为 -2049 比 -2048 确实要小,所以返回 1,这证明结果是正确的。

sltiu_ins函数调试方法类似

注意:
sltiu 指令的属性,它是无符号的比较指令,也就是说 sltiu 指令看到的数据是无符号的,而** -2048 数据编码为 0xfffff800**,如果把这个数据当成无符号数,则远大于 2047,所以返回 0。

有无符号寄存器版本:slt、sltu 指令

接着来看看 sltsltu 指令,这是寄存器与寄存器的有无符号比较指令,它们的形式如下所示。

slt rd,rs1,rs2
#slt 有符号比较指令
#rd 目标寄存器
#rs1 源寄存器1(有符号数据)
#rs2 源寄存器2(有符号数据)
sltu rd,rs1,rs2
#sltu 无符号比较指令
#rd 目标寄存器
#rs1 源寄存器1(无符号数据)
#rs2 源寄存器2(无符号数据)

上述代码中 rd、rs1、rs2 可以是任何通用寄存器。有、无符号同样代表 rs1、rs2 寄存器中的数据。

先看看 slt、sltu 这两个指令完成的操作,用伪代码怎么描述:

if(rs1 < rs2)
    rd = 1;
else
    rd = 0;

依然在 slti.S 文件中用汇编写上 slt_ins、sltu_ins 函数 ,如下所示:

.globl slt_ins
slt_ins:
    slt a0, a0, a1          #if(a0<a1) a0=1 else a0=0,a0,a1是参数,a0是返回值,这样计算结果就返回了
    jr ra                   #函数返回

.globl sltu_ins
sltu_ins:
    sltu a0, a0, a1         #if(a0<a1) a0=1 else a0=0,a0,a1是参数,a0是返回值,这样计算结果就返回了
    jr ra                   #函数返回    

slt_ins 与 sltu_ins 函数,分别是执行 slt 和 sltu 指令,都是拿 a0 寄存器和 a1 寄存器比较,如果 a0 小于 a1 寄存器,就把 1 写入到 a0 寄存器,否则写入 0 到 a0 寄存器。
VSCode 当中按 F5 调试的效果如下:
image
上图中是执行完 slt a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。对照截图可以看到,执行指令之后,a0 寄存器中的值确实已经变成 1 了,这说明比较运算的结果是正确的。
当 slt_ins 函数返回后,输出的结果如下:
image

因为 1 确实小于 2,所以结果返回 1,通过调试表明运算结果是正确的。
sltu_ins 函数的调试我们也如法炮制。

同样,也来拆分一下 slti、sltiu、slt、sltu 指令的各位段的数据,看看它们是如何编码的。
image
从上图可以发现,立即数版本和寄存器版本的指令格式不一样,操作码也不一样,而它们之间的有无符号是靠功能位段来区分的,而立即数位段和源寄存器与目标寄存器位段,和之前的指令是相同的。

image

参考:

标签:函数,精讲,RISC,ins,a0,指令,寄存器,addi
From: https://www.cnblogs.com/whiteBear/p/16751542.html

相关文章

  • RISC介绍
    CPU中包含了控制部件和运算部件,即中央处理器。1971年,Intel将运算器和控制器集成在一个芯片上,称为4004微处理器,这标志着CPU的诞生。到了1978年,开发的8086处理器奠......
  • 64位的单周期RISC-V 模拟器
    分享一个我最近完成过的小项目--64位的单周期RISC-V模拟器,这个项目我最近参与一生一芯计划过程中完成的一个小项目。需要用到的工具:Verilog、Verilator、计算机组成原理......
  • Vue2 指令操作
    概述指令是vue为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。vue中的指令按照不同的用途可分为如下6大类:内容渲染指令。属性绑定指令。事件绑定指......
  • CPU--指令系统
    1.机器的指令的一般格式:操作码字段,地址码字段; 2.数据在存储器中的存放方式:a,从任意位置开始--不浪费空间,读写控制比较复杂; :b,从一个存储......
  • 写过自定义指令吗,原理是什么?
    背景看了一些自定义指令的文章,但是探究其原理的文章却不多见,所以我决定水一篇。如何自定义指令?其实关于这个问题官方文档上已经有了很好的示例的,我们先来温故一下。除......
  • 15_内置指令
    1.v-text_指令<!DOCTYPEhtml><html><head><metacharset="UTF-8"/><title>v-text指令</title><!--引入Vue--><scriptty......
  • 16_自定义指令
    1.自定义指令<!DOCTYPEhtml><html><head><metacharset="UTF-8"/><title>自定义指令</title><scripttype="text/javascript"src="......
  • windows下运行make指令,windows下安装mingw
    原文链接:windows下运行make指令,windows下安装mingw–每天进步一点点(longkui.site)因为安装的scnuoj需要用到make指令,所以在windows上搞了一个程序用来执行make指令。......
  • Linux实用指令1
    Linux实用指令指定运行级别基本介绍0关机1单用户找回丢失密码2多用户状态没有网络服务3多用户状态有网络服务4系统未使用保留给用户5图形界面6系统重启......
  • vue3 自定义指令控制按钮权限
    经过1个周的摸索和查阅资料,终于搞定VUE3中自定义指令,实现按钮级别的权限控制。当然,只是简单的对按钮进行隐藏和删除的dom操作比较容易,一直纠结的是当按钮无权限时,不是直接......