本节来看下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 指令

接着来看看 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 调试的效果如下:



上图中是执行完 slt a0,a0,a1 指令之后,执行 jr ra 指令之前的状态。对照截图可以看到,执行指令之后,a0 寄存器中的值确实已经变成 1 了,这说明比较运算的结果是正确的。

当 slt_ins 函数返回后,输出的结果如下:

因为 1 确实小于 2,所以结果返回 1,通过调试表明运算结果是正确的。

sltu_ins 函数的调试我们也如法炮制。

同样,也来拆分一下 slti、sltiu、slt、sltu 指令的各位段的数据,看看它们是如何编码的。



从上图可以发现,立即数版本和寄存器版本的指令格式不一样,操作码也不一样,而它们之间的有无符号是靠功能位段来区分的,而立即数位段和源寄存器与目标寄存器位段,和之前的指令是相同的。

参考:

RISC-V指令精讲(一):算术指令--加法指令、比较指令的更多相关文章

  1. Linux实战教学笔记12:linux三剑客之sed命令精讲

    第十二节 linux三剑客之sed命令精讲 标签(空格分隔): Linux实战教学笔记-陈思齐 ---更多资料点我查看 1,前言 我们都知道,在Linux中一切皆文件,比如配置文件,日志文件,启动文件 ...

  2. Linux高频命令精讲(三)

    [教程主题]:2.Linux高频命令精讲 [2.1]Linux的运行方式 图形运行方式 - 本地使用KDE/Gnome集成环境 - 运行X Server远程使用图形环境 命令行(字符运行)方式 - 本 ...

  3. (转)不看绝对后悔的Linux三剑客之grep实战精讲

    不看绝对后悔的Linux三剑客之grep实战精讲 原文:http://blog.51cto.com/hujiangtao/1923675 https://www.cnblogs.com/peida/a ...

  4. (转)不看绝对后悔的Linux三剑客之sed实战精讲

    不看绝对后悔的Linux三剑客之sed实战精讲 原文:http://blog.51cto.com/hujiangtao/1923718 二.Linux三剑客之sed命令精讲 1,前言 我们都知道,在L ...

  5. 深入Java核心 Java内存分配原理精讲

    深入Java核心 Java内存分配原理精讲 栈.堆.常量池虽同属Java内存分配时操作的区域,但其适用范围和功用却大不相同.本文将深入Java核心,详细讲解Java内存分配方面的知识. Java内存分 ...

  6. iOS开发——语法篇OC篇&高级语法精讲二

    Objective高级语法精讲二 Objective-C是基于C语言加入了面向对象特性和消息转发机制的动态语言,这意味着它不仅需要一个编译器,还需要Runtime系统来动态创建类和对象,进行消息发送和 ...

  7. iOS-UI控件精讲之UIView

    道虽迩,不行不至:事虽小,不为不成. 相关阅读 1.iOS-UI控件精讲之UIView(本文) 2.iOS-UI控件精讲之UILabel ...待续 UIView是所有UI控件的基类,在布局的时候通常 ...

  8. Linux实战教学笔记18:linux三剑客之awk精讲

    Linux三剑客之awk精讲(基础与进阶) 标签(空格分隔): Linux实战教学笔记-陈思齐 快捷跳转目录: * 第1章:awk基础入门 * 1.1:awk简介 * 1.2:学完awk你可以掌握: ...

  9. Java岗 面试考点精讲(基础篇01期)

    即将到来金三银四人才招聘的高峰期,渴望跳槽的朋友肯定跟我一样四处找以往的面试题,但又感觉找的又不完整,在这里我将把我所见到的题目做一总结,并尽力将答案术语化.标准化.预祝大家面试顺利. 术语会让你的面 ...

  10. Keepalived原理与实战精讲--VRRP协议

    . 前言 VRRP(Virtual Router Redundancy Protocol)协议是用于实现路由器冗余的协议,最新协议在RFC3768中定义,原来的定义RFC2338被废除,新协议相对还简 ...

随机推荐

  1. Vue press 支持图片放大功能的代码分享

    介绍 VuePress 由两部分组成:一个以 Vue 驱动的主题系统的简约静态网站生成工具,和一个为编写技术文档而优化的默认主题.它是为了支持 Vue 子项目的文档需求而创建的. 由 VuePress ...

  2. DeepSeek-R1的“思考”艺术,你真的了解吗?

    大家好~,这里是AI粉嫩特攻队!今天咱们来聊聊一个有趣的话题--DeepSeek-R1到底什么时候会"思考",什么时候又会选择"偷懒"? 最近有朋友问我:&qu ...

  3. “未能加载工具箱项xxx,将从工具箱中将其删除”提示出现原因及解决方案

    https://www.thinbug.com/q/27289366 https://social.msdn.microsoft.com/Forums/vstudio/en-US/77e10b58-4 ...

  4. python进程 - 调试报错 you are not using fork to start your child processes

    在走这段代码的时候报错了,记录一下我的调试过程,感觉有个思路来走就挺好的. 1.报错与解决 文件名字:ClassifierTest.py import torch import torchvision ...

  5. Linux常用命令-练习记录

    具体命令 1.复制文件到指定目标,若目录不存在则创建目录 mkdir 和 cp 结合使用 mkdir ../dst/sh_test && cp sh_test/hello_os.sh ...

  6. pve节点频繁宕机问题排查

    1.时间: 我是大概20220521日上午11:03分收到这个事情开始跟进: 再这之前一直是其他同事在处理,由于最近比较忙,没有安排的事情基本也都没有深入跟进,只是知道个大概. 2.问题现象: ​ q ...

  7. 网站支持https之一:https原理和SSL证书类型

    1 https原理 https加密请求过程 Client和Server之间会进行一下几个步骤的交互: ① Client发送https请求: ② Client和Server通过tcp的三次握手建立连接, ...

  8. Sa-Token v1.41.0 发布 🚀,来看看有没有令你心动的功能!

    Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证.权限认证.单点登录.OAuth2.0.微服务网关鉴权 等一系列权限相关问题. 目前最新版本 v1.41.0 已推送至 Mav ...

  9. Delphi 增加/获得windows用户帐号

    unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms ...

  10. RabbitMQ持久化+消息执行优先级

    持久化   channel.QueueDeclare(queue:"hello",//队列名 durable:true,//持久化  exclusive:false,//排他性,该 ...