RISC-V汇编指令

写在最前面:这一篇是UC Berkeley的CS61C的笔记,根据我自己的理解进行学习记录,其中贴的一些图片来自于课程PPT。

了解汇编之前,我们需要先了解为什么需要汇编?以下是我的理解:

机器执行的命令都是些二进制的机器码,我们需要对机器进行编程需要记住这些机器码,这是对于程序员很不友好的,所以前人就用一些汇编指令取替代这些机器码,代码写完之后再使用编译器生成这些机器码,所以汇编是为了简化编程而创造出来的。

汇编代码一般使用.S结尾,表示source file;汇编翻译出的机器码用.o结尾,表示machine code object file;链接器链接生成的结果以.out结尾表示是最后的生成结果。

文中的rd是register destination的缩写,意为目标寄存器;rs是register source的缩写,意为源寄存器。


1 算数运算与逻辑运算指令

NumArithmetic / logicmeane.g.
1add加法运算指令add rd, rs1, rs2
2sub减法运算指令sub rd, rs1, rs2
3and与运算指令and rd, rs1, rs2
4or或运算指令or rd, rs1, rs2
5xor异或运算指令xor rd, rs1, rs2
6sllshift left logical 逻辑左移运算指令sll rd, rs1, rs2
7srlshift right logical 逻辑右移运算指令srl rd, rs1, rs2
8srashift right arithmetic 算数右移运算指令sra rd, rs1, rs2

这是一组是运算指令,1-2是算数运算指令,3-8是逻辑运算指令。

算数运算指令比较简单,这里以加法运算指令为例:

a = b + c;
add rd, rs1, rs2
->
# a = rd, b = rs1, c = rs2

add rd, rs1, rs2的意义就是将寄存器rs1的值加上寄存器rs2的值,最后存储到目标寄存器rd中。减法运算指令同理。

接下来看逻辑运算指令:

以逻辑左移指令为例:

sll x11, x12, x13   # x11 = x12 << x3

以上指令的意义是将x12左移x13位,存储到x11当中;右移运算符srl使用方式相同。

sra算数移位运算符指的是移位后,空出的bit用符号位填充:

1111 1111 1111 1111 1111 1111 1110 0111 = -25
srai x10, x10, 4
1111 1111 1111 1111 1111 1111 1111 1110 = -2

这里要注意,算数移位运算并不等于直接除以2

首先要注意的是RISC-V中是没有NOT指令的


2 immediate

NumImmediatemeane.g.
1addi加法运算指令addi rd, rs1, imm
2andi与运算指令andi rd, rs1, imm
3ori或运算指令ori rd, rs1, imm
4xori异或运算指令xori rd, rs1, imm
5sllishift left logical 逻辑左移运算指令slli rd, rs1, imm
6srlishift right logical 逻辑右移运算指令srli rd, rs1, imm
7sraishift right arithmetic 逻辑右移运算指令srai rd, rs1, imm

这一组指令可以看作是第一节的扩展,第一节中的指令是将两个寄存器中的值做运算,这一节中的指令同样是做算数运算或者是逻辑运算,不同的是这一组指令用于常数计算。

为什么要单独出一组常数计算的指令呢?这是因为常数相加非常常见,如果从内存加载一个常数,可能会消耗更多的时间,用更多的寄存器,直接用一组专用的指令可能会让执行速度变得更快。

addi为例:

a = b + 10;
addi rd, rs1, 10
->
# a = rd, b = rs1

addi rd, rs1, imm的意义就是将寄存器rs1的值加上常数imm,最后存储到目标寄存器rd中。

如果遇到a = b汇编应该怎么写呢?a = b可以看作是a = b + 0,但是这里我们不用addi,而是用add

addi rd, rs1, x0

这里的x0表示寄存器,该寄存器接地,保存的值始终为0。

接下来有一点要注意,这一组指令中并没有看到有subi,当我们要用到立即数减法时,编译器会帮我们转化为负数,再使用加法,这样做可以简化ALU单元的设计:

a = b - 9;
addi rd, rs1, -9

3 Load/Store

NumLoad/Storemeane.g.
1lwload word 加载四字节指令lw x10, 12(x15)
2swstore word 存储四字节指令
3lbload byte 加载一字节指令
4sbload byte 存储一字节指令
5lbuload byte unsigned 加载一字节无符号数

我们调用汇编指令add sub来做运算,但是运算所要的数据还在内存当中,我们要如何将这些数据从内存加载到寄存器呢?运算完成后如何将数据重新写到内存呢?这就是这组组汇编指令的所能完成的事情。

lw用于从某个地址加载数据,sw用于将数据存储到某个地址。接下来举例看看lw sw应该如何使用:

int A[100];
g = h + A[3];
->
lw x10, 12(x15)  	# x15表示数组A的地址
add x11, x12, x10 	# g = g + A[3]

我们首先要拿到数组A的地址,然后根据偏移量(以byte为单位)获取到需要读取的地址(这里要读取A[3],需要向后偏移12bytes),调用lw指令加载数据,最后完成计算。

如果我们要把计算得到的结果存储在A[10]中,要如何处理呢?

sw x11, 40(x15)

计算目标地址与基地址的偏移量,接着调用sw就好了。

使用lw sw时我们需要知道,每次读取或者写入都是以四字节为单位,32bit数刚好对应32bit寄存器,因此符号位在读取、写入过程中可以保留。

RISC-V还提供了加载、存储一个字节的指令lb wb,每次读取和写入都是一个字节,使用方法和lw sw类似。但是这里就会有问题了,当把一字节的数据从内存拷贝到寄存器时,这一字节的数据只占用了寄存器的8bit,那其他24bit(3byte)怎么办呢?都填0吗,有符号位要怎么办?

这里的做法是将符号位上的数填充到前面的3bytes里,这被称为符号扩展。

但是我们并不是每次都要做符号扩展,比如加载一个无符号数据就不需要扩展,所以还有一个指令lbu,用这个指令做加载就不会执行符号扩展,直接用0填充其他的三个字节。要注意,是没有sbu 的,这是因为从寄存器存储一字节到内存时,这一字节的最高位本身就是符号位了。


4 Branch

NumBranching/Jumpsmeane.g.
1beqbranch if equal 等于beq rs1, rs2, L1
2bnebranch if not equal 不等于
3bgebranch if greater than or equal 大于等于
4bltbranch if less than 小于
5bgeubge的unsigned版本
6bltublt的unsigned版本
7j(伪指令)jump 跳转j label

这一组是分支指令,上面的1-6是条件分支指令,需要通过比对值来控制代码执行流程,这一组指令的最后一个参数是跳转标签(Label);7-9是非条件分支指令,执行到这些命令时总是会跳转。接下来一起看看例子:

如果我们要判断两个值是相等然后再去执行对应操作,我们应该使用什么指令呢?

if (i == j)f = g + h;
->bne x13, x14, Exitadd x10, x11, x12
Exit:

可以看到我们用的时bnebne x13, x14, Exit的意思是如果不相等则跳转到Exit。为什么不用beq,而是要用一个相反的指令呢?我们尝试写一下:

	beq x13, x14, Branchj Exit
Branch:add x10, x11, x12
Exit:

从上面我们可以看到,如果条件不成立,跳过add指令会麻烦许多,所以判断时用相反的指令会更加简洁。

接下来再看一个if-else的例子:

if (i == j)f = g + h;
elsef = g - h;
->bne x13, x14, Elseadd x10, x11, x12j Exit
Else:sub x10, x11, x12
Exit:

这里有一点要注意,不能忘了退出指令;另外是没有ble 的,如果需要判断小于等于可以通过是否大于来判断。

接下来的例子更复杂一点,我们如何使用条件分支指令实现for / while 循环呢?

int A[20];
int sum = 0;
for (int i = 0; i < 20; i++)sum += A[i];
->add x9, x8, x0	  # x9=&A[0]add x10, x0, x0   # sumadd x11, x0, x0   # iaddi x13, x0, 20  # 
Loop:beq x11, x13, Donelw x12, 0(x9)		# A[i]add x10, x10, x12	# sum += A[i]addi x9, x9, 4		# &A[i+1]addi x11, x11, 1	# i++j Loop
Done:

5 Pseudo-instructions

伪指令指的是一些常用汇编指令的替代,例如:

mv rd, rs  =  addi rd, rs, 0
li rd, 13  =  addi rd, x0, 13
nop        =  addi x0, x0, 0
ret		   =  jr ra
j		   =  jal x0, Label

6 Function Call

这一组指令用于支持函数调用,了解指令前,先来了解程序是如何执行的。

我们的汇编代码经过编译器翻译后会生成二进制的目标文件,目标文件中的数据就是一条一条的指令。程序执行时会将这些指令一条一条加载到内存中对应的程序区,所以这些指令也是有对应的地址的。CPU中有一个特殊的寄存器Program Counter(PC)程序计数器,里面存储的是下一条指令的地址,一条程序执行完成,PC会更新其保存的地址(默认是增加4字节来指向下一条指令,因为RISC-V中的所有指令都是32bits)。PC中的地址更新时也会有其他情况比如说上面的j指令,或者这一节将会了解的函数调用相关指令,PC的地址将会更新到指定内存地址。

6.1 相关指令

在这里插入图片描述

函数调用中的一些约定:

  1. 函数调用过程中使用a0-a7(x10-x17)(argument register)这8个寄存器来传递参数,其中两个a0-a1用于返回参数;
  2. 寄存器x1ra(return address register)用于回到控制原点,即回到函数调用的地方;
  3. s0-s1(x8-x9),s2-s11(x18-x27)(saved register)保存寄存器
Numfunction callmeane.g.
1jrjump register 跳转到寄存器jr ra
1jaljump and link 跳转并链接jal rd, Label
2jalrjump and link register 跳转并链接jalr rd, rs, imm

jump and link表示:跳转到某个地址,并且函数调用的下一条指令的地址保存到ra

我们先来看一个函数执行的汇编代码示例:

...
sum(a, b);
...int sum(int x, int y) {return x + y;
}
->
#address (decimal)
1000	mv a0, s0	# x = a
1004	mv a1, s1	# y = b
1008 	addi ra, zero, 1016		# ra = 1016
1012	j sum
1016	...
...
2000 	sum: add a0, a0, a1
2004	jr ra

从上面的例子我们可以发现,函数体在内存中的地址和主程序可能会离得比较远,函数执行时有如下步骤:

  1. 拷贝参数
  2. 保存函数执行完成后的地址到ra
  3. 跳转到函数并执行
  4. 执行完成后跳转到ra

这里用到一条新的指令jr,跳转到某个寄存器。为什么这边不用j来跳转呢?因为j跳转需要很多标签,如果函数返回要加标签,那么可能到处都是这些标签了。

每次使用jr跳转时,需要在函数执行前将控制点记录到ra中,这可能会有些许麻烦,RISC-V为我们提供了jal指令来帮助我们做保存返回地址的工作,示例如下:

1008 	addi ra, zero, 1016		# ra = 1016
1012	j sum
->
1008	jal sum

由于返回函数调用点非常常用,所以用ret这个伪指令代替jr ra

jal命令如果我们不需要返回地址则将他保存到x0jal x0, Label,并且用伪指令j来替代。

6.2 关于函数调用的一些知识

6.1节中我们初步了解了函数调用,接下来我们再通过一些示例来理解函数调用。我们先总结下CPU进行函数调用时需要经历的6个步骤:

  1. 将参数放到函数可以获取到的地方(寄存器);
  2. 将控制点交给函数(jal);
  3. 获取函数需要的存储资源;
  4. 执行函数;
  5. 将函数返回值放到调用者可以获取的地方,释放本地存储;
  6. 将控制点还给调用者(ret)。

这里有一个问题:当CPU进行函数调用时,寄存器会被用来存储函数中的变量,原来寄存器中的值存应该如何存储呢?函数调用结束时这些值应该如何恢复呢?

存储这些值需要一块内存,函数调用前将存储寄存器中旧的值存到内存中,函数调用结束后从内存中恢复这些值并且删除掉他们。

这块内存被设计为栈结构(stack: last in first out (LIFO)),为了找到这块内存,需要有一个寄存器指向这块地址,这个寄存器x2被称为栈指针(sp: stack pointer)。

约定栈指针从高地址到低地址增长:push动作减小栈指针的值,pop增加栈指针的值。

接下来要了解栈帧(stack frame)的概念,每一次函数调用所用到的内存块被称为栈帧,栈帧里包含有返回指令的地址,传入参数的值,以及一些本地变量的值。
在这里插入图片描述

在嵌套函数调用中,我们常称调用函数伪CalleR,称被调用函数为CalleE。当被调用函数执行时,调用函数需要知道哪些寄存器的值被改变了,哪些寄存器的值没有被改变。为了减少从内存存储或者加载数据的次数,寄存器被分成两类:

  1. 在函数调用期间值可以保留的寄存器:Caller只能依赖这些没有修改的寄存器,例如sp、gp、tp;
  2. 函数调用期间值不能保留的寄存器:例如参数寄存器a0-a7,ra,临时寄存器(temporary)t0-t6。

以下是寄存器列表,我们不需要非常了解每个寄存器的作用,但是需要了解寄存器中的值由谁来保存:在这里插入图片描述

接下来看一个嵌套调用的例子:

int sumSquare(int x, int y) {return mult(x, x) + y;
}
->
sumSquare:addi sp, sp, -8		# 先给stack开辟空间sw ra, 4(sp)		# 存储 sumSquare ra(return address)sw a1, 0(sp)		# 存储 y 到栈帧mv a1, a0			# 创建 mult 函数参数到寄存器 a0jal mult			# 调用 mult 函数lw a1, 0(sp)		# 保存 mult 返回值到栈帧add a0, a0, a1		# 完成加法计算lw ra, 4(sp)		# 获取 raaddi sp, sp, 8		# 恢复栈指针jr ra				# 返回 sumSquare 调用中
mult:...

我们的程序在运行时,变量会存在于三种内存空间中:

  1. static:只会被声明一次的变量,其生命周期一直到程序终止
  2. heap:通过动态内存分配(malloc)声明的变量
  3. stack:程序执行期间所用到的空间,寄存器可以存储值到这块空间中

接下来了解下内存布局:RV32 和 RV64、RV128的内存布局不一样,这里了解RV32的内存布局:

  1. 栈空间起始于高位地址,并且向下增长,栈空间必须进行16-bytes对齐
  2. test segment在内存的最底部
  3. 静态数据段在文本段上面,有一个global pointer(gp)指向静态区
  4. 堆空间在静态区上面,从低地址向高地址增长
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/7732.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于jeecg-boot的任务甘特图显示

更多功能看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 基于项目的任务显…

SpringCloud-Alibaba之Sentinel熔断与限流

一、下载安装运行 http://localhost:8080进行访问 登录账号和密码均为sentinel 二、创建工程&#xff0c;并注册到nacos服务中心 依赖spring-cloud-starter-alibaba-nacos-discovery,spring-cloud-starter-alibaba-sentinel sentine-datasource-nacos (持久化)配置文件 se…

python:基于GeoPandas和GeoViews库将GEDI激光高程数据映射到交互式地图

作者:CSDN @ _养乐多_ 本文将介绍 GEDI(Global Ecosystem Dynamics Investigation)激光雷达数据某数据点波形数据提取,并绘制图表,添加其他图表元素并使图表具有交互性。 在本文中,我们将探索如何打开、读取和处理GEDI数据,并利用地理信息处理库GeoPandas和地理空间数…

使用langchain与你自己的数据对话(二):向量存储与嵌入

之前我以前完成了“使用langchain与你自己的数据对话(一)&#xff1a;文档加载与切割”这篇博客&#xff0c;没有阅读的朋友可以先阅读一下&#xff0c;今天我们来继续讲解deepleaning.AI的在线课程“LangChain: Chat with Your Data”的第三门课&#xff1a;向量存储与嵌入。 …

抖音seo账号矩阵系统源码如何开发布局?

目录 一、 抖音SEO账号矩阵系统源码的开发布局步骤如下&#xff1a; 二。 开发部署源码 三、 开发部署功能设计 1. 短视频AI智能创作 2. 托管式账号管理: 3. 数据分析 4. 智能营销获客 四。 抖音seo源码开发部署交付技术文档包含 五。 开发代码展示&#xff1a; 一、 抖…

vuejs源码之模版编译原理

之前我们说过虚拟dom&#xff0c;也就是虚拟dom拿到vnode后所做的事情&#xff0c;而模版编译是如何让虚拟dom拿到vnode。 模版编译的目标就是生成渲染函数&#xff0c;而渲染函数的作用是每次执行它&#xff0c;它就会使用当前最新的状态生成一份新的vnode&#xff0c;然后用…

Idea 开启 lombook 注解插件处理器

Idea 开启 lombook 注解插件处理器 方便编译器识别 勾选 Enable annotation processing

线性DP--BOX

还没学&#xff0c;等学完再仔细写。 #include<bits/stdc.h> using namespace std; typedef long long ll; ll a[1000010]; ll vis[1000010]; ll f[1000010][3]; int main() {ll n,m;cin>>n;for(int i1;i<n;i){cin>>a[i];}for(int i1;i<n;i){cin>&g…

【Python机器学习】实验01 Numpy以及可视化回顾

文章目录 一、Numpy的基础知识实验1 生成由随机数组成的三通道图片&#xff0c;分别显示每个维度图片&#xff0c;并将三个通道的像素四周进行填充&#xff0c;分别从上下左右各填充若干数据。 二、Numpy的线性代数运算实验2 请准备一张图片&#xff0c;按照上面的过程进行矩阵…

C#月数计算器(主要用于社保、医保缴费月数计算)

1、为什么做这个&#xff1f; 工作中&#xff0c;经常需要计算参保人社保、医保缴费月数&#xff0c;之前都是在Excel中写一个DATEDIF公式&#xff0c;修改单元格中的日期&#xff0c;计算间隔的月数&#xff0c;公式如下&#xff1a; DATEDIF(起始日期, 终止日期, 返回类型) …

【wxWidgets】剪贴板和拖放操作

【wxWidgets】剪贴板和拖放操作 使用剪贴板传输数据时应用程序间的一种交互方式 剪贴板和拖放操作在wxWidgets中共享了一些类来实现数据的传输 数据对象 wxDataObject类时剪贴板操作和拖放操作的核心&#xff0c;该类实例代表了拖放操作中鼠标拖拽的事物和剪贴板中拷贝和粘贴…

Stephen Wolfram:一次只添加一个词

It’s Just Adding One Word at a Time 一次只添加一个词 That ChatGPT can automatically generate something that reads even superficially like human-written text is remarkable, and unexpected. But how does it do it? And why does it work? My purpose here is t…

mysql进阶1——proxysql中间件

文章目录 一、基本了解二、安装部署三、proxysql管理配置3.1 内置库3.1.1 main库表3.1.2 stats库表3.1.3 monitor库 3.2 常用管理变量3.2.1 添加管理用户3.2.2 添加普通用户3.2.3 修改监听套接字 四、多层配置系统4.1 系统结构4.2 修改变量加载配置4.3 启动加载流程 一、基本了…

单机和集群以及分布式的浅析

假设一个大系统分为A、B、C、D、E五个模块&#xff0c;也可以认为是五个基本的服务&#xff0c;该系统靠这五个模块协同工作&#xff0c;共同为用户提供服务。 单机 单机&#xff1a;显然&#xff0c;单机表名该系统完完全全的部署在该台机器上&#xff0c;拥有完整的服务&am…

集成学习——Boosting算法:Adaboost、GBDT、XGBOOST和lightGBM的简要原理和区别

1、Boosting算法 Boosting算法是通过串联的方式&#xff0c;将一组弱学习器提升为强学习器算法。它的工作机制如下&#xff1a; &#xff08;1&#xff09;用初始训练集训练出一个基学习器&#xff1b; &#xff08;2&#xff09;依据基学习器的表现对训练样本分布进行调整&…

opencv 图像距离变换 distanceTransform

图像距离变换&#xff1a;计算图像中每一个非零点距离离自己最近的零点的距离&#xff0c;然后通过二值化0与非0绘制图像。 #include "iostream" #include "opencv2/opencv.hpp" using namespace std; using namespace cv;int main() {Mat img, dst, dst…

洛必达法则和分部积分的应用之计算数学期望EX--概率论浙大版填坑记

如下图所示&#xff0c;概率论与数理统计浙大第四版有如下例题&#xff1a; 简单说就是&#xff1a;已知两个相互独立工作电子装置寿命的概率密度函数&#xff0c;将二者串联成整机&#xff0c;求整机寿命的数学期望。 这个题目解答中的微积分部分可谓是相当的坑爹&#xff0c;…

vue/cli 自定义配置

vue/cli 自定义配置 1、更改默认的端口号8080 只需要更改vue.config.js文件 1、更改默认的端口号8080 只需要更改vue.config.js文件

脑电信号处理与特征提取——4.脑电信号的预处理及数据分析要点(彭微微)

目录 四、脑电信号的预处理及数据分析要点 4.1 脑电基础知识回顾 4.2 伪迹 4.3 EEG预处理 4.3.1 滤波 4.3.2 重参考 4.3.3 分段和基线校正 4.3.4 坏段剔除 4.3.5 坏导剔除/插值 4.3.6 独立成分分析ICA 4.4 事件相关电位&#xff08;ERPs&#xff09; 4.4.1 如何获…

什么是UE像素流送,像素流推流是什么原理?

游戏开发者通常在运行游戏逻辑时会将游戏渲染到屏幕的同一台设备上来运行虚幻引擎应用&#xff0c;多人联网游戏可能会在应用程序的多个实例之间分发部分游戏逻辑&#xff0c;但每个单独的实例仍然会为自己的玩家在本地渲染游戏。即使是使用 HTML5 部署选项创建可以在 Web 浏览…