RISC-V指令精讲(一):算术指令--加法指令、比较指令
本节来看下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;
}
运行结果:
上图中是程序刚刚执行完 addi a0,a0,5 指令之后,执行 jr ra 指令之前的状态。可以看到 a0 寄存器中的值已经变成了 9,这说明运算的结果是正确的。
addi_ins 函数返回后,输出的结果如下图所示:
在 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。如下所示:
和之前一样,上图中是刚刚执行完 addi a0,a0,-2048 指令之后,执行 jr ra 指令之前的状态。这时 a0 寄存器中的值已经变成了 0,这说明运算的结果正确。
addi_ins2 函数返回后,输出的结果如下图所示:
上图中已经证明了结果符合我们的预期,用 addi 指令完成了立即数的减法计算。这也是 RISC-V 指令集中没有立即数据减法指令的原因。为了保证这一特性,所有的立即数必须总是进行符号扩展,这样就可以用立即数表示负数,所以我们并不需要一个立即数版本的减法指令。
为了进一步搞清楚这条指令的机器码数据,看下 addi_ins 函数和 addi_ins2 函数的二进制数据什么样。
打开工程目录下的 addi.bin 文件,如下所示:
以上是四条指令数据,其中两个 0x00008067 数据为两个函数的返回指令,即:jr ra,0x00550513,它对应的汇编语句 addi a0,a0,5,0x80050513,对应汇编语句 addi a0,a0,-2048。
来详细拆分一下 addi 指令的各位段的数据,看看它是如何编码的。
对照上图,可以看到一条指令数据为 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。
上图展示的是执行完 add a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。这时 a0 寄存器中的值确实已经变成了 2,这说明运算的结果正确。
当 add_ins 函数返回后,输出的结果如下图所示:
这个结果证明了 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,如下所示:
上图中依然是执行完 sub a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。这时 a0 寄存器中的值确实已经变成 1 了,证明运算结果没问题。
当 sub_ins 函数返回后,就会输出下图所示的结果。
经过调试,sub 指令执行的结果也符合我们的预期了。
继续研究机器编码,来看看 add_ins 函数和 sub_ins 函数的二进制数据。打开工程目录下的 addsub.bin 文件,如下所示:
以上 4 个 32 位数据是四条指令,其中两个 0x00008067 数据是两个函数的返回指令即:jr ra,0x00b50533 为 add a0,a0,a1,0x40b50533 为 sub a0,a0,a1。
来拆分一下 add、sub 指令的各位段的数据,看看它们是如何编码的。如下所示:
从图里可以看到,操作码占了 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 寄存器。
运行结果:
上图中是执行完 slti a0,a0,-2048
指令之后,执行 jr ra
指令之前的状态。如果看到 a0 寄存器中的值确实已经变成 1 了,就说明运算的结果是正确的。
当 slti_ins 函数返回后,输出的结果如下所示:
因为 -2049 比 -2048 确实要小,所以返回 1,这证明结果是正确的。
sltiu_ins
函数调试方法类似
注意:
sltiu 指令的属性,它是无符号的比较指令,也就是说 sltiu 指令看到的数据是无符号的,而** -2048 数据编码为 0xfffff800**,如果把这个数据当成无符号数,则远大于 2047,所以返回 0。
有无符号寄存器版本:slt、sltu 指令
接着来看看 slt
、sltu 指令
,这是寄存器与寄存器的有无符号比较指令,它们的形式如下所示。
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 调试的效果如下:
上图中是执行完 slt a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。对照截图可以看到,执行指令之后,a0 寄存器中的值确实已经变成 1 了,这说明比较运算的结果是正确的。
当 slt_ins 函数返回后,输出的结果如下:
因为 1 确实小于 2,所以结果返回 1,通过调试表明运算结果是正确的。
sltu_ins 函数的调试我们也如法炮制。
同样,也来拆分一下 slti、sltiu、slt、sltu 指令的各位段的数据,看看它们是如何编码的。
从上图可以发现,立即数版本和寄存器版本的指令格式不一样,操作码也不一样,而它们之间的有无符号是靠功能位段来区分的,而立即数位段和源寄存器与目标寄存器位段,和之前的指令是相同的。
参考: