从头开发一个RISC-V的操作系统(五)汇编语言编程

目标:通过这一个系列课程的学习,开发出一个简易的在RISC-V指令集架构上运行的操作系统。

前提

这个系列的大部分文章和知识来自于:[完结] 循序渐进,学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春,以及相关的github地址

在这个过程中,这个系列相当于是我的学习笔记,做个记录。

RISC-V汇编语言入门

能手写汇编代码的,在我的心目中,都是巨佬。只有明白底层硬件的人,才有可能去写汇编。

一个完整的RISC-V汇编程序有多条语句(statement)组成。一个典型的RISC-V汇编语句由3部分组成:
[label:] [operation] [comment]

  • label:任何以冒号结尾的标识符都被认为是一个标号。
  • operation可以有多种类型
    • instruction:直接对应二进制机器指令的字符串
    • pseudo-instruction:为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令
    • directive:通过类似指令的形式(以.开头),通知汇编器如何控制代码的产生,不对应具体的指令
    • macro:采用.macro/.endm自定义的宏
  • comment:注释,以#开始到当前行结束

RISC-V汇编指令总览

汇编指令操作对象

寄存器:在这个系列中,我们只有32个通用寄存器,x0~x31,这个在之前我们已经展示过了。在RISC-V中,Hart在执行算数逻辑运算时所操作的数据必须直接来自寄存器。

内存:Hart可以执行在寄存器和内存之间的数据读写操作;读写操作使用字节位基本单位进行寻址;RV32最多可以访问 2 32 2^{32} 232个字节的内存空间。

汇编指令编码格式

在这里插入图片描述
RV32由6种指令编码格式。每一条指令有32bits,funct7/funct3和opcode一起决定最终的指令类型。指令在内存中按照小端序排列。rs2,rs1,rd都是指的是寄存器(register),imm指的是立即数。
大端序:高字节存放在内存的低地址;小端序:低字节存放在内存的低地址。
下面这张表指出了opcode是如何与指令对应的。
在这里插入图片描述
opcode总共7bits,第0和第1位都是11,第2到第四位和第5到第6位不同,则代表着指令类型不同,使用addsub指令举例。
在这里插入图片描述
先看opcode0110011在表中对应着OP,add和sub都应该属于OP,然后funct3也相同,但是funct7不同,代表着它们一个是add,一个是sub。

这里介绍的只是一个概貌,更多信息还要自己学习。

add指令介绍

在这里插入图片描述
通过上面的图片就可以明白一条汇编指令是如何到二进制代码的。同时这一块视频中也有详细地讲解。

无符号数

我们知道有符号数在计算机中都是使用二进制补码的形式保存的,最高位为符号位,0代表正数,1代表负数。正数的补码不变,负数的补码=反码+1。

练习

这里我们带大家做一个add2的练习。
汇编源码为:

# Add
# Format:
#	ADD RD, RS1, RS2
# Description:
#	The contents of RS1 is added to the contents of RS2 and the result is 
#	placed in RD.

	.text			# Define beginning of text section
	.global	_start		# Define entry _start

_start:
	li x6, 1		# x6 = 1
	li x7, -2		# x7 = -2
	add x5, x6, x7		# x5 = x6 + x7

stop:
	j stop			# Infinite loop to stop execution

	.end			# End of file

make 以后,我们使用make code查看这个程序的二进制代码,然后逐行进行分析,如下:


test.elf:     file format elf32-littleriscv


Disassembly of section .text:

80000000 <_start>:

	.text			# Define beginning of text section
	.global	_start		# Define entry _start

_start:
	li x6, 1		# x6 = 1
80000000:	00100313          	li	t1,1
	li x7, -2		# x7 = -2
80000004:	ffe00393          	li	t2,-2
	add x5, x6, x7		# x5 = x6 + x7
80000008:	007302b3          	add	t0,t1,t2

8000000c <stop>:

stop:
	j stop			# Infinite loop to stop execution
8000000c:	0000006f          	j	8000000c <stop>

其他都没什么好说的,主要就是这个li指令以及add这个指令。add x5,x6,x7这个我们在上面展示过了,这里主要解释下li这个伪指令。
在这里插入图片描述
这里我们先从其他博客那里拿过来一个结论,
在这里插入图片描述

li伪指令把一个立即数imm加载到rd寄存器中。当imm在 [ − 2 11 , 2 11 − 1 ] [-2^{11} , 2^{11-1}] [211,2111]范围内(也就是[-2048~2047))的时候,li被转化成下面这条实际指令:

addi rd, x0,imm #rd=imm+0
x0是一个特殊的寄存器,值为0且永远不会改变

所以add x7, x0, -2 对应的二进制为:111111111110 00000 000 00111 0010011,前面的111111111110代表了-2,它是以二进制补码存储的;00000代表了寄存器x0,000是funct,00111代表了寄存器x7,最后的0010011则是opcode。和我们输出的hex一样。

那么当立即数imm不在这个范围,但在32位有符号数的范围内(也就是[-2147482648~-2048)以及(+2047~+2147482647])的时候,一条addi指令显然是不够了。 这时候就需要lui指令。

假设我们有这样的一条语句add x7, -3000,那么它对应的二进制是多少呢?先看它的二进制,如下图:
在这里插入图片描述
在立即数为-3000的情况下,一条li伪指令被分为了两条汇编指令luiaddiaddi我们已经在上面介绍过了,下面给出lui指令的说明。
在这里插入图片描述
看这个似乎有点懵,我们直接说怎么将-3000写入到x7寄存器的。
-3000的32位二进制反码为1111 1111 1111 1111 1111 0100 0100 1000,先取第12到第31位(通过右移12位就可以得到第12到第31位),也就是1111 1111 1111 1111 1111共20位,构成lui这条指令1111 1111 1111 1111 1111 00111 0110111 = fffff3b7。对应的操作就是:将20位左移12位,并将低12位置0,写入到x7中。
再取-3000的第0到第11位0100 0100 1000加到x7寄存器中(x7 = x7 + imm[0:11]),对应的二进制指令就是0100 0100 1000 00111 000 00111 0010011 = 44838393,可以看到,和程序的结果一样。这样大家阶对li伪指令有了进一步的了解。

参考链接

  1. https://zhuanlan.zhihu.com/p/367085156
### RISC-V 汇编指令详解 #### 基础概念 RISC-V架构支持多种类型的指令集扩展,这些指令集可以被分类为基础指令和拓展指令。对于汇编编程而言,了解不同种类的基础指令及其语法结构至关重要[^1]。 #### 寄存器命名约定 在编写RISC-V汇编代码时,会频繁使用到各种寄存器。通常情况下,`x0-x31`代表通用目的寄存器;而浮点运算则涉及到名为`f0-f31`的系列特殊用途寄存器。此外还有PC指针以及条件码等辅助性组件参与其中。 #### 数据传送类指令 用于加载立即数至寄存器或将内存中的数据传送到指定位置: - `li rd, imm`: 将立即数值`imm`装载进目标寄存器`rd` ```assembly li t0, 42 # Load immediate value 42 into register t0 ``` - `lw rd, offset(rs1)`: 加载字节宽度的数据项自基址加偏移量处并存储于给定的目标寄存器 ```assembly lw a0, 8(s0) # Load word from address s0 + 8 to argument register a0 ``` #### 控制流转移语句 实现程序执行路径的选择与循环控制等功能: - `beq rs1, rs2, label`: 如果两个源操作数相等,则跳转到标签所在的位置 ```assembly beq t0, zero, end_loop # Branch if equal between registers t0 and zero (r0), goto 'end_loop' ``` - `jal ra, func_call`: 调用函数并将返回地址保存起来以便后续恢复调用者环境 ```assembly jal ra, my_function # Jump And Link - call function named 'my_function', save return addr in reg ra ``` #### 算术逻辑单元(ALU)操作符 完成基本算术计算及位级处理任务: - `add rd, rs1, rs2`: 对应参数rs1加上rs2的结果送入结果寄存器rd内 ```assembly add sp, sp, -16 # Adjust stack pointer by subtracting 16 bytes ``` - `xor rd, rs1, rs2`: 执行按位异或运算后赋值给接收端 ```assembly xor t1, t2, t3 # Perform bitwise XOR on contents of regs t2 & t3; store outcome at dest reg t1 ``` 通过上述几类典型实例可以看出,在掌握好相应规则的基础上,利用简洁明了的助记符形式表达复杂机器级别动作正是汇编语言的魅力所在。同时值得注意的是,随着硬件平台差异性的存在,某些特定功能可能仅适用于具备对应特性的CPU型号之上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值