本节来看下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. element vue 动态单选_VUE 动态构建混合数据Treeselect选择树,同时解决巨树问题

    今天在项目中需要通过行政区域选择,然后选择该行政区域下面的景区,也就是要构建行政区划.景区两表数据表的树.全国的行政区域到县已经3500多了,再加上景区会有几万个点,这棵选择树不论是在后台还是在前台构 ...

  2. Deepseek学习随笔(3)--- 高效提问技巧

    明确需求 在与 DeepSeek 互动时,明确需求是获取高质量回复的关键.以下是一些示例: 错误示例:帮我写点东西 这样模糊的指令无法让 DeepSeek 理解你的具体需求,生成的回复可能无法满足你的 ...

  3. CentOS 8 上安装和配置 nginx

    1.检查yum上的nginx版本 yum info nginx 2.安装nginx yum install nginx 安装过程有时会询问是否安装,输入y回车即可 3.将服务设置为每次开机启动 sud ...

  4. LCP 06. 拿硬币

    地址:https://leetcode-cn.com/problems/na-ying-bi/ <?php /** * Class Solution * 桌上有 n 堆力扣币,每堆的数量保存在数 ...

  5. Ubuntu Nvidia driver驱动安装及卸载

    前言 当前英伟达下载的驱动不再是 .run 的 shell文件,所以有了新的文档,如下 Ubuntu Nvidia driver驱动安装(新) 当然如果你有 shell 文件,也可以继续使用本文档安装 ...

  6. gin Http请求Body和Header的获取 request post form Query header

    gin Http请求Body和Header的获取 request post form Query header 请求参数 POST /post?id=1234&page=1 HTTP/1.1 ...

  7. 什么是单点登录?什么是SSO?什么是CAS?

    目录 单点登录简介 SSO&CAS是什么 单点登录适合什么场景 单点登录的三种实现方式 CAS的几个重要知识点 CAS的实现过程 单点登录简介 单点登录(SingleSignOn,SSO),就 ...

  8. docker swarm CA证书到期

    1.现象 在portain平台查看日志,发现一些节点日志无法查看报错为:Error grabbing logs: rpc error: code = Unknown desc = warning: i ...

  9. 修改 Proxmox VE 6.0 LVM Thin 为存储分区

    PVE 安装后默认将 60G 的 SSD 分为了 14G 和 26G 的两个分区,其中 25G 为 LVM Thin,用于ISO镜像存储的分区为 14G,明显不够用,传一个 WInServer2016 ...

  10. 解决 Docker 日志文件太大的问题

    Docker 在不重建容器的情况下,日志文件默认会一直追加,时间一长会逐渐占满服务器的硬盘的空间,内存消耗也会一直增加,本篇来了解一些控制日志文件的方法. 清理单个文件 运行时控制 全局配置 Dock ...