RISC-V汇编指令

这篇具有很好参考价值的文章主要介绍了RISC-V汇编指令。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

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

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


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

Num Arithmetic / logic mean e.g.
1 add 加法运算指令 add rd, rs1, rs2
2 sub 减法运算指令 sub rd, rs1, rs2
3 and 与运算指令 and rd, rs1, rs2
4 or 或运算指令 or rd, rs1, rs2
5 xor 异或运算指令 xor rd, rs1, rs2
6 sll shift left logical 逻辑左移运算指令 sll rd, rs1, rs2
7 srl shift right logical 逻辑右移运算指令 srl rd, rs1, rs2
8 sra shift 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

Num Immediate mean e.g.
1 addi 加法运算指令 addi rd, rs1, imm
2 andi 与运算指令 andi rd, rs1, imm
3 ori 或运算指令 ori rd, rs1, imm
4 xori 异或运算指令 xori rd, rs1, imm
5 slli shift left logical 逻辑左移运算指令 slli rd, rs1, imm
6 srli shift right logical 逻辑右移运算指令 srli rd, rs1, imm
7 srai shift 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

Num Load/Store mean e.g.
1 lw load word 加载四字节指令 lw x10, 12(x15)
2 sw store word 存储四字节指令
3 lb load byte 加载一字节指令
4 sb load byte 存储一字节指令
5 lbu load 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

Num Branching/Jumps mean e.g.
1 beq branch if equal 等于 beq rs1, rs2, L1
2 bne branch if not equal 不等于
3 bge branch if greater than or equal 大于等于
4 blt branch if less than 小于
5 bgeu bge的unsigned版本
6 bltu blt的unsigned版本
7 j(伪指令) 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 相关指令

RISC-V汇编指令,计算机组成原理,risc-v,汇编

函数调用中的一些约定:

  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)保存寄存器
Num function call mean e.g.
1 jr jump register 跳转到寄存器 jr ra
1 jal jump and link 跳转并链接 jal rd, Label
2 jalr jump 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)的概念,每一次函数调用所用到的内存块被称为栈帧,栈帧里包含有返回指令的地址,传入参数的值,以及一些本地变量的值。
RISC-V汇编指令,计算机组成原理,risc-v,汇编

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

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

以下是寄存器列表,我们不需要非常了解每个寄存器的作用,但是需要了解寄存器中的值由谁来保存:RISC-V汇编指令,计算机组成原理,risc-v,汇编

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

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的内存布局:文章来源地址https://www.toymoban.com/news/detail-606345.html

  1. 栈空间起始于高位地址,并且向下增长,栈空间必须进行16-bytes对齐
  2. test segment在内存的最底部
  3. 静态数据段在文本段上面,有一个global pointer(gp)指向静态区
  4. 堆空间在静态区上面,从低地址向高地址增长
    RISC-V汇编指令,计算机组成原理,risc-v,汇编

到了这里,关于RISC-V汇编指令的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 从头开发一个RISC-V的操作系统(一)计算机系统漫游

    目标:通过这一个系列课程的学习,开发出一个简易的在RISC-V指令集架构上运行的操作系统。 这个系列的大部分文章和知识来自于:[完结] 循序渐进,学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春,以及相关的github地址。 在这个过程中,这个系列相当于是我的学习笔记,做

    2024年04月09日
    浏览(49)
  • 计算机系统基础(六)之RISC-V流水线设计——用ChatGPT辅助学习

    一、CPU的流水线通常被划分为五个部分,它们是: 取指令(Instruction Fetch):从内存中获取指令并将其放入指令寄存器中。 指令译码(Instruction Decode):将指令从指令寄存器中读取并解码成相应的操作。 执行指令(Execution):根据操作码执行指令,可能需要读取寄存器或内

    2024年02月03日
    浏览(42)
  • 计算机组成原理32位MIPS CPU设计实验(指令译码器电路设计 、时序发生器状态机设计、时序发生器输出函数、硬布线控制器)

    这次实验是32位MIPS CPU设计实验(单总线CPU-定长指令周期-3级时序),在头歌当中一共需要我们进行六道题的测试,分别为MIPS指令译码器设计,定长指令周期(时序发生FSM设计,时序发生器输出函数设计,硬布线控制器,单总线CPU设计),硬布线控制器组合逻辑单元。其中由于

    2024年02月02日
    浏览(42)
  • 计算机组成原理之计算机硬件发展和计算机系统的组成

    学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您: 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持,想组团高效学习… 想写博客但无从下手,急需写作干货注入能量… 热爱写作,愿意让自己成为更好

    2024年01月24日
    浏览(86)
  • 计算机组成原理 --- 计算机性能指标

    一.存储器的性能指标 1.MAR是地址寄存器,MDR是数据寄存器 2.MAR的位数能够体现最多存多少个地址,而每个地址就代表一个存储单元,所以MAR的位数能表示存储器中有多少个存储单元 3.MDR是数据寄存器,它的容纳极限 = 每个存储单元的容纳极限 --- 如果MDR的容纳极限小于存储单

    2023年04月08日
    浏览(87)
  • 计算机组成原理-计算机系统概述

    目录 一,基本组成  二、各部件工作原理 2.1存储器 2.2运算器  2.3控制器  2.4输入设备 2.5输出设备 一条指令的工作原理  三、计算机系统的层次结构  三种基本语言 四、计算机性能指标         “存储程序”的概念,指将指令以二进制代码的形式事先输入计算机的主存

    2024年02月05日
    浏览(95)
  • 【计算机组成与设计】Chisel取指和指令译码设计

    本次试验分为三个部分: 目录 设计译码电路 设计寄存器文件 实现一个32个字的指令存储器 输入位32bit的一个机器字,按照课本MIPS 指令格式,完成add、sub、lw、sw指令译码,其他指令一律译码成nop指令。输入信号名为Instr_word,对上述四条指令义译码输出信号名为add_op、sub_o

    2024年02月05日
    浏览(71)
  • 计算机组成原理(1)--计算机系统概论

    计算机系统由“硬件”和“软件”两大部分组成。 所谓“硬件”,是指计算机的实体部分,它由看得见摸得着的各种电子元器件,各类光、电、机 设备的实物组成,如主机、外部设备等。 所谓“软件”,它看不见摸不着,由人们事先编制的具有各类特殊功能的程序组成。(

    2024年01月16日
    浏览(61)
  • 计算机组成原理(一)计算机系统概论

    计算机组成原理这门课可以说是计算机专业最重要的基础,身为计算机专业非常重要,所以需要自己好好琢磨,不要应付考试。 计算机硬件系统的主要组成为五大部分,分别为存储器、运算器、控制器、输入设备和输出设备。 简述一下计算机的工作原理,假设要用计算机来

    2024年02月08日
    浏览(74)
  • 计算机组成原理

    作为还在学习的学生和不断进步的同事,学习计算机组成原理具有以下几个重要的好处:它可以帮助你深入理解计算机系统的工作原理,包括处理器、存储器、输入输出设备等组成部分之间的交互关系。这种深入理解可以提高你对计算机系统的整体把握能力,让你能够更好地

    2024年02月08日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包