文章目录
- 前提
- RISC-V汇编语言入门
- RISC-V汇编指令总览
- 汇编指令操作对象
- 汇编指令编码格式
- add指令介绍
- 无符号数
- 练习
- 参考链接
目标:通过这一个系列课程的学习,开发出一个简易的在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位不同,则代表着指令类型不同,使用add
和sub
指令举例。
先看opcode
,0110011
在表中对应着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,211−1]范围内(也就是[-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
伪指令被分为了两条汇编指令lui
和addi
,addi
我们已经在上面介绍过了,下面给出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
伪指令有了进一步的了解。
参考链接
- https://zhuanlan.zhihu.com/p/367085156