RISC-V汇编指令

news2024/11/23 6:31:20

写在最前面:这一篇是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, Exit
	add x10, x11, x12
Exit:

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

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

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

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

if (i == j)
	f = g + h;
else
	f = g - h;
->
	bne x13, x14, Else
	add x10, x11, x12
	j 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   # sum
	add x11, x0, x0   # i
	addi x13, x0, 20  # 
Loop:
	beq x11, x13, Done
	lw 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 函数参数到寄存器 a0
	jal mult			# 调用 mult 函数
	lw a1, 0(sp)		# 保存 mult 返回值到栈帧
	add a0, a0, a1		# 完成加法计算
	lw ra, 4(sp)		# 获取 ra
	addi 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.coloradmin.cn/o/784429.html

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

相关文章

六边形架构和分层架构的区别?

六边形架构和分层架构是什么&#xff1f; 六边形架构&#xff08;Hexagonal Architecture&#xff09;和分层架构&#xff08;Layered Architecture&#xff09;是两种常见的软件架构模式。六边形架构强调将核心业务逻辑与外部依赖解耦&#xff0c;通过接口与外部世界进行通信。…

hcip mgre与rip实验

要求&#xff1a; 1.toop搭建 2.IP地址规划 R1g 0/0/1 192.168.1.1 24 s 4/0/0 188.0.0.2 24 t&#xff1a;10.0.0.1 24R2 s 4/0/0 188.0.0.1 24 s 4/0/1 188.0.1.1 24 s 3/0/0 188.0.2.1 24 loop0 8.8.8.8 24 t&#xff1a;10.0.0.2 24 R3g…

本地 IDC 中的 K8s 集群如何以 Serverless 方式使用云上计算资源

作者&#xff1a;庄宇 在前一篇文章《应对突发流量&#xff0c;如何快速为自建 K8s 添加云上弹性能力》中&#xff0c;我们介绍了如何为 IDC 中 K8s 集群添加云上节点&#xff0c;应对业务流量的增长&#xff0c;通过多级弹性调度&#xff0c;灵活使用云上资源&#xff0c;并通…

Python自动化测试框架pytest的详解安装与运行

这篇文章主要为大家介绍了Python自动化测试框架pytest的简介以及安装与运行&#xff0c;有需要的朋友可以借鉴参考下希望能够有所帮助&#xff0c;祝大家多多进步 1. pytest的介绍 pytest是一个非常成熟的全功能的python测试工具&#xff0c;它主要有以下特征&#xff1a; 简…

二进制子集题解

样例输入&#xff1a; 13样例输入&#xff1a; 0 1 4 5 8 9 12 13思路分析&#xff1a; 这道题大体就是进制转换然后按位 d f s dfs dfs。进制转换比较好理解&#xff0c;不懂得可以自行 b d f s ( 百度优先搜索 ) bdfs(百度优先搜索) bdfs(百度优先搜索)一下。 代码&#…

【LeetCode】260.只出现一次的数字 III(找出单身狗)

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负。 目录 前言&#xff1a; 一只单身狗&#xff1a; 两只单身狗&#xff1a; 前言&#x…

HTTPS安全套接字层超文本传输协议

HTTPS安全套接字层超文本传输协议 HTTPS简介HTTPS和HTTP的主要区别客户端在使用HTTPS方式与Web服务器通信时的步骤SSL/TLS协议的加密&#xff08;握手&#xff09;过程为什么数据传输阶段使用对称加密HTTPS 的优点HTTPS 的缺点HTTPS 的优化证书优化会话复用 HTTPS简介 HTTP协议…

Keil系列教程07_Configuration(一)

1写在前面 本文带来的内容为“Configuration”&#xff0c;即配置。而这里的“配置”和前面“工程目标选项配置”是完全不同的两种配置。 工程目标选项配置是针对工程目标&#xff0c;保存在工程文件&#xff08;.uvprojx和.uvoptx&#xff09;。这里的Configuration是针对IDE…

在本地git仓库查看远端的URL

右键调出选项栏 选择git-远端&#xff0c;选择远端的内容就可以看到URL了

数据结构之时间复杂度

目录 一、时间复杂度的概念 二、大O的渐进表示法 三、常见时间复杂度计算举例 一、时间复杂度的概念 时间复杂度的定义&#xff1a;在计算机科学中&#xff0c;算法的时间复杂度是一个函数&#xff0c;它定量描述了该算法的运行时间。一个算法执行所耗费的时间&#xff0c;从…

OpenCV图像处理-图像分割-MeanShift

MeanShift 1. 基本概念2.代码示例 1. 基本概念 MeanShift严格说来并不是用来对图像进行分割的&#xff0c;而是在色彩层面的平滑滤波。它会中和色彩分布相近的颜色&#xff0c;平滑色彩细节&#xff0c;侵蚀掉面积较小的的颜色区域&#xff0c;它以图像上任意一点P为圆心&…

架构重构实践心得

一、前言 大多数的技术研发都对重构有所了解&#xff0c;而每个研发又都有自己的理解。从代码重构到架构重构&#xff0c;我参与了携程大型全链路重构项目&#xff0c;积累了一点经验心得&#xff0c;在此抛砖引玉和大家分享。 二、重构的定义 重构是指在不改变外部行为的情…

MySQL数据库(十)

目录 一、Java的数据库编程&#xff1a;JDBC 1.1JDBC工作原理 二、Java具体连接数据库 2.1准备过程 2.2代码连接数据库 一、Java的数据库编程&#xff1a;JDBC JDBC&#xff0c;即Java Database Connectivity&#xff0c;java数据库连接。是一种用于执行SQL语句的Java API&…

华为认证HCIA-HCIP-HCIEdatacom题库解析+机构视频+实验

题库包含有2023年最新HCIA-datacom题库、HCIP-datacom题库&#xff0c;HCIE-datacom题库&#xff0c; 云计算HCIA&#xff0c;HCIP题库&#xff0c;云服务HCIA&#xff0c;HCIP题库&#xff0c;华为存储HCIP题库&#xff0c;华为安全HCIP题库 &#xff0c;学习笔记&#xff0c;…

webrtc QOS笔记 Nack机制浅析

nack源码浅析 Video Nack 机制概述 nack的机制非常简洁,收到非连续的packet seq 会将丢包的seq插入自身nack_list缓存, 之后立即发送一次那组丢包的seq重传请求, 之后如果超时仍然没有收到重传回来的seq, 就通过定时任务继续发送. nack 三个缓存list nack_list_ : 用于记录已丢…

10分钟内入门 ArcGIS Pro

本文来源&#xff1a;GIS荟 大家好&#xff0c;这篇文章大概会花费你10分钟的时间&#xff0c;带你入门 ArcGIS Pro 的使用&#xff0c;不过前提是你有 ArcMap 使用经验。 我将从工程文件组织方式、软件界面、常用功能、编辑器、制图这5个维度给大家介绍。 演示使用的 ArcGI…

【SQL应知应会】表分区(一)• Oracle版

欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 本文收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习&#xff0c;有基础也有进阶&#xff0c;有MySQL也有Oracle 分区表 • Oracle版 前言一、分区表1.什么是表分区…

《电脑城的衰退:时代变迁中的背影》

随着科技的不断进步和电子商务的兴起&#xff0c;电脑城这个曾经火爆的地方正逐渐走向衰退甚至面临消失。对于这一变迁&#xff0c;我认为既有利也有弊。 首先&#xff0c;电脑城的衰退带来了一定的便利。传统的电脑城通常拥有大量的实体店铺&#xff0c;买家必须亲自前往选择…

Qt/C++音视频开发49-多级连保存和推流设计(同时保存到多个文件/推流到多个平台)

一、前言 近期遇到个用户需要多级联的保存和推流&#xff0c;在ffmpegsave多线程保存类中实现这个功能&#xff0c;越简单越好&#xff0c;就是在推流的同时&#xff0c;能够开启自动转储功能&#xff0c;一边推流的同时一边录像保存到本地视频文件。最初设想的一个方案是new两…

LeetCode515. 在每个树行中找最大值

515. 在每个树行中找最大值 文章目录 [515. 在每个树行中找最大值](https://leetcode.cn/problems/find-largest-value-in-each-tree-row/)一、题目二、题解 一、题目 给定一棵二叉树的根节点 root &#xff0c;请找出该二叉树中每一层的最大值。 示例1&#xff1a; 输入: ro…