本篇文章的内容
- 一、RISC-V汇编语言简介
- 1.1 RISC-V 汇编语言的基本格式
- 1.2 RISC-V 汇编指令操作对象
- 1.3 RISC-V 汇编指令编码格式
- 1.4 RISC-V 汇编指令分类
- 二、RISC-V汇编语言详解
- 2.1 add 加法指令
- 2.2 sub 减法指令
本系列是博主参考B站课程学习开发一个RISC-V的操作系统的学习笔记,计划从RISC-V的底层汇编指令学起,结合C语言,在Ubuntu 20.04上开发一个简易的操作系统。一个目的是通过实践操作学习和了解什么是操作系统,第二个目的是为之后学习RISC-V的集成电路设计打下一定基础。本系列持续不定期更新,分享出来和大家一同交流进步。
博主是微电子科学与工程专业的学生,对软件和操作系统难免有理解不到位的地方。如有谬误敬请不吝告知,不胜感激。
参考课程及文章:
【Bilibili】[完结] 循序渐进,学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春
一、RISC-V汇编语言简介
汇编语言(Assembly Language)是一种“低级”语言。不同的架构的汇编语言是不同的,因为底层的寄存器的个数和功能不同。例如x86的机器语言在RISC-V的机器上是无法运行的,但是使用高级语言时完全不需要考虑底层的机器语言。我们使用不同的编译器将高级语言翻译成不同的机器语言,来完成对内存和指令的管理和优化。而正是由于这一点,使用汇编语言时完全不需要考虑不同编译器的影响,这也是汇编语言的灵活性的体现。本课程学习和使用的汇编语言是RISC-V汇编语言,其依赖于RISC-V独有的GNU汇编器,所以将RISC-V的汇编程序移植到其他架构的处理器上也是无法运行的。
- 汇编语言的缺点:难读、难写、难移植
- 汇编语言的优点:灵活、强大
- 汇编语言的应用场景
- 需要直接访问底层硬件的地方
- 需要对性能执行极致优化的地方
1.1 RISC-V 汇编语言的基本格式
一个完整的RISC-V汇编程序有多条语句(statement)组成,汇编文件一般由.s
结尾(不包含预处理语句,是纯粹的汇编语句)。一条典型的RISC-V汇编语句由3部分组成,分为标签、操作和注释:
[label:] [operation] [comment]
- label(标号): GNU汇编中,任何以冒号结尾的标识符都被认为是一个标号。标号相当于给一个指令所在的地址起的一个名字。
- operation 可以有以下多种类型:
- instruction(指令): 直接对应二进制机器指令的字符串,例如
add
。 - pseudo-instruction(伪指令): 一些指令的组合。它并不对应二进制机器指令,只是为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令(instructions),方便程序的使用,例如
li
。在理解和做法上与自定义的函数类似。 - directive(指示/伪操作): 通过类似指令的形式(以“.”开头),通知汇编器如何控制代码的产生等,不对应具体的RISC-V指令,由汇编器自身定义,例如
.text
、.global
、.end
、.macro
、.endm
等。在理解和应用上类似C语言中的#define
语句。 - macro(宏):采用指示/伪操作
.macro
/.endm
自定义的宏,汇编器碰到宏时会自动将宏替换成对应定义的内容。
- instruction(指令): 直接对应二进制机器指令的字符串,例如
- comment(注释): 常用方式,
#
开始到当前行结束,也有些汇编器定义;
或//
开头的注释格式。
下面是一个简单的RISC-V的汇编语言程序:
# fitst RISC-V Assemble Sample
.macro do_nothing
nop
nop
.endm
.text
.global _start
_start:
li x6, 5 # x6 = 5
li x7, 4 # x7 = 4
add x5, x6, x7 # x5 = x6 + x7
do_nothing
stop:
j stop # jump to stop
.end
1.2 RISC-V 汇编指令操作对象
- 寄存器
- 32个寄存器,x0~x31(本节课只设计RV32I的通用寄存器组)
- 在RISC-V中,Hart在执行算数逻辑运算时所操作的数据必须直接来自寄存器(Hart可以认为是处理器执行指令的最小单元,类似传统的CPU概念,但是与CPU不同),如果数据存在内存中,必须首先将要操作的数据转移到寄存器中,Hart才能对数据进行处理,计算之后的结果也只能存在寄存器中,之后根据需要再转移到外部的内存空间中。
上图中,XLEN
表示机器的字长,RV32I的ISA对应的XLEN
对应的就是32位。x0
寄存器很特殊,其中的值恒为0,且不允许写操作,读出的值恒为0。pc
即指针计数器寄存器在RISC-V中是禁止被访问的。获取程序当前运行的位置需要用特殊的方法实现。
- 内存
- Hart可以执行在寄存器和内存之间的数据读写操作;
- 读写操作时,使用字节(Byte)为基本单位进行寻址;
- RV32可以访问最多 2 32 2^{32} 232个字节的内存空间。
1.3 RISC-V 汇编指令编码格式
做底层开发的时候,经常需要进行逐字节对照查阅指令。可以根据手册(RISC-V非特权指令集)查阅每个指令的含义。
-
指令长度:在RISC-V汇编指令的规范中,所有的指令的长度都是固定的(在非压缩的情况下讨论)。RV32I中指令长度
ILEN1
= 32bit。 -
指令对齐:指一条指令开始的地址,
IALIGN
= 32bit表示指令开始的地址一定是4的倍数,程序加载的时候一定从这样一个指令开始的地方开始加载。 -
每个32bit构成一个域。
-
funct3/funct7
和opcode
共同决定了最终的指令类型。以add
指令为例,它对应的opcode
为0110011,通过查阅下标可以发现它是一条OP类型的指令。
-
指令在程序中按小端序(Little-Endian)排列(底字节存入低地址,高字节存入高地址)。不同的CPU规定的主机字节序不同。
-
RISC-V中有六种不同的指令格式(format):
- R-type:(Register),每条指令中有三个 fields,用于指定 3 个寄存器参数(选择的寄存器编号)
- I-type: (Immediate),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为 12 bits)。
- S-type: (Store),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为 12 bits,但 fields 的组织方式不同于 I-type)
- B-type: (Branch),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为 12bits,但取值为 2 的倍数)。
- U-type: (Upper),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为 20bits,用于表示一个立即数的高 20 位)
- J-type: (Jump),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为 20bits)
1.4 RISC-V 汇编指令分类
RISC-V中常用的汇编指令如下表所示:
伪指令很多,一条伪指令具体执行哪些代码可以在 RISC-V非特权指令集手册 中的 RISC-V Assembly Programmer’s Handbook 章节查阅到。
二、RISC-V汇编语言详解
2.1 add 加法指令
- 编码格式:R-type
opcode
: 0110011funct3
= 000,funct7
= 0000000
# 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
Makefile操作须知
在仓库代码中的common.mk
文件中有如下的一段代码,它的意思是启动虚拟机qemu的系统模式(汇编直接编译出的代码是不能在操作系统的裸机上直接运行的,即不能在用户user模式下运行),参数-nographic
表示不启动图形界面,-smp 1
表示只启动一个Hart,-machine virt
表示启动virt的机器类型。QEMU = qemu-system-riscv32 QFLAGS = -nographic -smp 1 -machine virt -bios none
运行指令
make debug
时实际运行的是如下的代码。-s
表示在目标机(模拟机)中启动gdbserver,二者建立网络链接(在这个环境中实际上是内部网络的回环),-S
表示启动调试模式后程序暂停运行。-x ${GDBINIT}
表示启动一个gdb的调试脚本,自动运行gdb的相关指令,脚本的内容可以在文件gdbinit
文件中找到.PHONY : debug debug: all @echo "Press Ctrl-C and then input 'quit' to exit GDB and QEMU" @echo "-------------------------------------------------------" @${QEMU} ${QFLAGS} -kernel ${EXEC}.elf -s -S & @${GDB} ${EXEC}.elf -q -x ${GDBINIT}
编译运行后,发现test.bin
文件只有16个字节,对应程序中确实只有四条指令。test.elf
文件中除了指令本身之外还有很多调试相关的内容。
输入命令make hex
可以看到test.bin
中的数据,可以根据这些数据将所有指令反汇编出来(也可以使用make code
命令自动进行反汇编):
使用make debug
命令对程序进行调试,在gdb页面中,输入si
(step instruction)单步运行程序(makefile中已经将断点设置在.start
处了)。可以看到,x6,x7,x5的值会依次变化。
如果要退出qemu中的gdb,一定要先 Ctrl+C,之后再输入
quit
和确认y
进行退出,否则会遇到qemu的后台程序杀不死的问题,导致下次启动错误。
2.2 sub 减法指令
- 编码格式:R-type
opcode
: 0110011funct3
= 000,funct7
= 0100000
# Substract
# Format:
# SUB RD, RS1, RS2
# Description:
# The contents of RS2 is subtracted from the contents of RS1 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
sub x5, x6, x7 # x5 = x6 - x7
stop:
j stop # Infinite loop to stop execution
.end # End of file
原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、机器学习方面的学习笔记。