RISC-V入门(基础概念+汇编部分) 基于 汪辰老师的视频笔记
RISC-V入门 基于 汪辰老师的视频笔记
RISC-V入门
[完结] 循序渐进,学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春
参考
RISC-V ISA 基本介绍
历史简介
自由(Free)与开放(Open)
-
RISC-V 念作 “risk-five”,代表着Berkeley所研发的第五代精简指令集。
-
该项目2010年始于加州大学伯克利(Berkeley)分校,希望选择一款 ISA 用于科研与教学。经过前期多年的的研究和选型,最终决定放弃使用现成的 X86 和 ARM 等 ISA,而是自己从头研发一款:
- X86:太复杂,IP问题。
- ARM:一样的复杂,而且在2010年之前还不支持64位,以及同样的IP问题。
-
主要研发人员
- Andrew Waterman,Yunsup Lee,David Patterson,Krste Asanovic
RISC-V 究竟是什么
-
一款高质量,免许可证,开放的RISC ISA。
-
一套由非盈利的RISC-V基金会维护的标准:https://riscv.org/ 。
-
适用于所有类型的计算机系统:从微控制器到超级计算机。
-
RISC-V不是一家公司,也不是一款 CPU 实现。
发展现状
特点
- 简单
- 清晰的分层设计
- 模块化
- 稳定
- 社区化
RISC-V ISA规范一览
RISC-V ISA
ISA 命名格式:RV [###] [abc…xyz]
- RV:用于标识 RISC-V体系架构的前缀,既 RISC-V 的缩写。
- [###] :{32, 64, 128} 用于标识处理器的字宽,也就是处理器的寄存器的宽度(单位为bit)。
- [abc…xyz] :标识该处理器支持的指令集模块集合。
- 例子:RV32IMA,RV64GC
模块化的 ISA
-
增量式 ISA :计算机体系结构的传统方法,同一个体系架构下的新一代处理器不仅实现了新的 ISA 扩展,还必须实现过去的所有扩展,目的是为了保持向后的二进制兼容性。典型的,以 80X86 为代表。
-
模块化 ISA:由 1 个基本整数指令集 + 多个可选的扩展指令集组成。基础指令集是固定的,永远不会改变。
RISC ISA = 1个基本整数指令集 + 多个可选的扩展指令集
-
基本整数(Integer)指令集
- 唯一强制要求实现的基础指令集,其他指令集都是可选的扩展模块。
-
扩展模块指令集:
- RISC-V 允许在实现中以可选的形式实现其他标准化和非标准化的指令集扩展。
- 特定组合“IMAFD”被称为“通用(General)”组合,用英文字母G表示。
-
例子:
- RV32I:最基本的 RISC-V 实现。
- RV32IMAC:32位实现,支持 Integer + Multiply + Atomic + Compressed 。
- RV64GC:64位实现,支持 IMAFDC 。
通用寄存器(General Purpose Register)
- RISC-V 的 Unprivileged Specification 定义了32个通用寄存器,以及一个PC
- 对 RV32I/RV64I/RV128I 都一样。
- 如果实现支持 F/D 扩展则需要额外支持32个 浮点(Float Point)寄存器。
- RV32E 将32个通用寄存器缩减为16个。
- 寄存器的宽度由 ISA 指定
- RV32的寄存器宽度为 32 位,RV64的寄存器宽度为64位,依次类推。
- 每个寄存器具体编程时有特定的用途以及各自的别名。由RISC-V Application Binary Interface(ABI)定义。
Hart
-
HART = HARdware Thread
- 一个处理器有多个执行流(硬件线程),跟多核的效果有点像。
特权级别(Privileged Level)
-
RISC-V 的 Privileged Specification 定义了三个特权级别(privilege level)
-
Machine 级别是最高的级别,所有的实现都需要支持。
-
可选的 Debug 级别(用于调试CPU)
Control and Status Registers(CSR)
- 不同的特权级别下时分别对应各自的一套 Register(CSR),用于控制(Control)和获取相应 Level 下的处理器工作状态。
- 高级别的特权级别下可以访问低级别的 CSR ,譬如Machine Level 下可以访问 Supervisor/User Level 的CSR,以此类推,但反之不可以。
- RISC-V 定义了专门用于操作 CSR 的指令(【参考1】中定义的 “Zicsr” 扩展)。
- RISC-V 定义了特定的指令可以用于在不同特权级别之间进行切换(【参考1】中定义的 ECALL/EBREAK)。
内存管理与保护
-
物理内存保护(Physical Memory Protection,PMP)
- 允许M模式指定U模式可以访问的内存地址。
- 支持 R/W/X,以及 Lock。
-
虚拟内存(Virtual Memory)
- 需要支持 Supervisor Level
- 用于实现高级的操作系统特性(Unix/Linux)
- 多种映射方式 Sv32/Sv39/Sv48
异常和中断
- 异常(Exception):“an unusal condition occurring at run time associated with an instruction in the current RISC-V hart”,执行完异常后,回到出现异常的那条指令,再次运行那条指令,然后继续往下运行。
- 中断(Interrupe):“an external asynchronous event that may cause a RISC-V hart to experience an unexpected transfer of control”,执行完中断后,回到中断出现的后一条语句,继续往下执行。
编译与链接
GCC 介绍
简介
GCC(GNU Compiler Collection)
- https://gcc.gnu.org/
- 由 GNU 开发的,遵循 GPL 许可证发行的编译器套件。
- 支持 C/C++、Objective-C、Fortran、Ada 和 Go 语言等多种语言前端,已被移植到多种计算机体系架构上,如 x86、ARM、RISC-V 等。
- GCC的初衷是为 GNU 操作系统专门编写一款编译器,现以被大多数“Unix-like”操作系统(如Linux、BSD、MacOS等)采纳为标准编译器。
命令格式
gcc [options] [filenames]
常用选项 | 含义 |
---|---|
-E | 只做预处理 |
-c | 只编译不链接,生成目标文件“.o” |
-S | 生成汇编代码 |
-o file | 把输出生成到由 file 指定文件名的文件中 |
-g | 在输出的文件中加入支持调试的信息 |
-v | 显示输出详细的命令执行过程信息 |
主要执行步骤
- 编译(cc1,这里针对 C 语言,不同的语言由自己的编译器):编译器完成“预处理” 和 “编译”。
- “预处理”指处理源文件中以“#”开头的预处理指令,譬如 #include、#define等;
- “编译” 则针对预处理的结果进行一系列的词法分析、语法分析、语义分析、优化后生成汇编指令,存放在 .o 为后缀的目标文件中。
- 汇编(as):汇编器将汇编语言代码转换为机器(CPU)可以执行的指令。
- 链接(ld):链接器将汇编器生成的目标文件和一些标准库(譬如 libc)文件组合,形成最终可执行的应用程序。
涉及的文件类型
- .c:C源文件
- .cc/.cxx/.cpp:C++ 源文件
- .s/.S:汇编语言源文件,大S包含预处理程序,小s不包含预处理的汇编
- .h:头(header)文件
- .o:目标(object)文件
- .a/.so:编译后的静态库(archive)文件和共享库(shared object)文件
- a.out:可执行文件
针对多个源文件的处理
ELF 介绍
简介
-
ELF(Executable Linkble Format)是一种Unix-like系统上的二进制文件格式标准。
-
ELF标准中定义的采用 ELF 格式的文件分为4类:
ELF文件类型 说明 实例 可重定位文件(Relocatable File) 内容包含了代码和数据,可以被链接成可执行文件或共享目标文件。 Linux上的 .o 文件 可执行文件(Executable File) 可以直接执行的程序 Linux上的 a.out 共享目标文件(Shared Object File) 内容包含了代码和数据,可以作为链接器的输入,在链接阶段和其他的 Relocatable File 或者 Shared Object File 一起链接成新的 Object File;或者在运行阶段,作为动态连接器的输入,和Executable File 结合,作为进程的一部分来运行。 Linux上的 .so 核心转储文件(Core Dump File) 进程意外终止时,系统可以将该进程的部分内容和终止时的状态信息保存到该文件中以供调试分析。 Linux 上的 core 文件
文件格式
ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而执行一个ELF文件时,在加载器(Loader)看来它是由Program Header Table描述的一系列Segment的集合
左边是从汇编器和链接器的视角来看这个文件,开头的ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,Section Header Table中保存了所有Section的描述信息。右边是从加载器的视角来看这个文件,开头是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加载过程中没有用到,所以是可有可无的。注意Section Header Table和Program Header Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。
文件处理相关工具:Binutils
https://www.gnu.org/software/binutils/
- ar:归档文件,将多个文件打包成一个大文件。
- as:被 gcc 调用,输出汇编文件,输出目标文件供应链接收器 ld 连接。
- ld:GNU 链接器。被 gcc 调用,它把目标文件和各种库文件结合在一起,重定位数据,并链接付好引用。
- objcopy:执行文件格式转换。
- objdump:显示 ELF 文件 的信息。
- -S,反汇编(gcc -g -c 源文件.c --> 生成带调试信息的 .o 文件)
- readelf:显示更多 ELF 格式文件的信息(包括 DWARF 调试信息)。
- -SW,显示Section header table
- -h,显示文件头信息
- …
嵌入式开发
什么是嵌入式开发
嵌入式开发是一种比较综合性的技术,它不单指纯粹的软件开发技术,也不单是一种硬件配置技术;他是在特定的硬件环境下针对某款硬件进行开发,是一种系统级别的与硬件结合比较紧密的软件开发技术。
交叉编译
-
参与编译和运行的机器根据其角色可以分成以下三类:
- 构建(build)系统:生成编译器可执行程序的计算机。
- 主机(host)系统:运行编译器可执行程序,编译链接应用程序的计算机系统。
- 目标(target)系统:运行应用程序的计算机系统。
-
根据 build/host/target 的不同组合我们可以得到如下的编译方式分类:
- 本地(native)编译:build == host == target
- 交叉(cross)编译:build == host != target
-
交叉(cross)编译:build == host != target
-
GNU 交叉编译工具链(Toolchain)
- 命名格式:arch-vendor-os1-[os2-]XXX
- 例子:
- x86_64-linux-gnu-gcc
- riscv64-unknown-elf-gcc
- riscv64-unknown-elf-objdump
- riscv64-unknown-linux-gnu-gcc
- riscv64-linux-gnu-gcc
调试器 GDB
https://www.gnu.org/software/gdb/
- GDB(GDB:The GNU Project Debugger),GNU项目调试器,用于查看另一个程序在执行过程中正在执行的操作,或该程序奔溃时正在执行的操作。
- 被调试的程序可能与 GDB 在同一台计算机上执行,也可能在另一台计算机(远程)上或者在模拟器上执行。
- GDB支持调试多种语言:譬如:Assembly,C,Go,Rust,…
GDB 基本调试流程
- 重新编译程序并在编译选项中加入“-g”
- $ gcc -g test.c
- 运行 gdb 和程序
- $ gdb a.out
- 设置断点
- (gdb) b 6
- 运行程序
- (gdb) r
- 程序暂停在断点处,执行查看
- (gdb) p xxx
- 继续、单步或者恢复程序运行
- (gdb) s/n/c
模拟器 QEMU
https://www.qemu.org/
- QEMU 是一套由(Fabrice Bellard)编写的以 GPL 许可证分发源码的计算机系统模拟软件,在 GNU/Linux 平台上使用广泛。
- 支持多种体系架构。譬如:IA-32(x86),AMD 64,MIPS 32/64,RISC-V 32/64 等等。
- QEMU 有两种主要运作模式:
- User mode:直接运行应用程序。
- System mode:模拟整个计算机系统,包括中央处理器及其他周围设备。
Qemu 的安装与使用
- 安装
- Ubuntu 上 apt install
- 源码编译安装
- qemu-system-riscv32 … -kernel ./test.elf
- qemu-system-riscv32 … -kernel ./test.elf -s -S
- -s:“-gdb tcp::1234”的缩写,启动 gdbserver 并在 1234端口号上监听客户端。
- -S:在启动时停止CPU(只有到在客户端键入 ‘c’ 才会开始执行)
项目构建工具 Make
https://www.gnu.org/software/make/
- make 是什么
- make是一种自动化工程管理工具。
- Makefile
- 配合make,用于描述构建工程过程中所管理的对象以及如何构造工程的过程。
- make 如何找到 Makefile
- 隐式查找:当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件
- 显式查找:-f
MakeFile 的构成
-
MakeFile 由一条或者多条规则(rule)组成。
-
每条规则由三要素构成:
- target:目标,可以是 obj 文件也可以是可执行文件。
- prerequisites:生成 target 所需要的依赖。
- command:为了生成 target 需要执行的命令,可以有多条。
-
一个简单的 Makefile 规则如下:
-
Makfile中其他元素介绍
-
缺省规则
-
伪规则
-
注释:行注释,以“#”开头
-
make 的运行
RISC-V汇编
RISC-V汇编语言 入门
汇编语言概念简介
- 汇编语言(Assembly Language)是一种低级语言
- 汇编语言的缺点:
- 难读
- 难写
- 难移植
- 汇编语言的优点
- 灵活
- 强大
- 汇编语言的应用场景
- 需要直接访问底层硬件的地方
- 需要对性能执行极致优化的地方
汇编语言语法介绍(GNU版本)
-
一个完整的RISC-V汇编程序有多条 语句(statement)组成
-
一个典型的RSIC-V汇编 语句 由3部分组成:
[label:] [operation] [comment]
-
label(标号):GNU汇编中,任何以冒号结尾的标识符都被认为是一个标号。
-
operation 可以有以下多种类型:
-
instruction(指令):直接 对应二进制机器指令的字符串。
-
pseudo-instruction(伪指令):为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令(instruction)。
-
directive(指令/伪操作):通过类似指令的形式(以 “ . ”开头),通知汇编器如何控制代码的产生等,不对应具体的指令。
-
macro:采用.macro/.endm自定义的宏。
.macro do_nothing # 宏的开头,定义了一个名为do_nothing的宏 nop # 宏的内容 .endm # 宏的结尾
-
-
comment(注释):常用方法,“#”开始到当前行结束。
-
汇编常见伪操作(directive)
指示符 | 作用 |
---|---|
.text | 代码段,之后跟的符号都在.text内 |
.data | 数据段,之后跟的符号都在.data内 |
.bss | 未初始化数据段,之后跟的符号都在.bss中 |
.section .foo | 自定义段,之后跟的符号都在.foo段中,.foo段名可以做修改 |
.align n | 按2的n次幂字节对齐 |
.balign n | 按n字节对齐 |
.globl sym | 声明sym未全局符号,其它文件可以访问 |
.string “str” | 将字符串str放入内存 |
.byte b1,…,bn | 在内存中连续存储n个单字节 |
.half w1,…,wn | 在内存中连续存储n个半字(2字节) |
.word w1,…,wn | 在内存中连续存储n个字(4字节) |
.dword w1,…,wn | 在内存中连续存储n个双字(8字节) |
.float f1,…,fn | 在内存中连续存储n个单精度浮点数 |
.double d1,…,dn | 在内存中连续存储n个双精度浮点数 |
.option rvc | 使用压缩指令(risc-v c) |
.option norvc | 不压缩指令 |
.option relax | 允许链接器松弛(linker relaxation,链接时多次扫描代码,尽可能将跳转两条指令替换为一条) |
.option norelax | 不允许链接松弛 |
.option pic | 与位置无关代码段 |
.option nopic | 与位置有关代码段 |
.option push | 将所有.option设置存入栈 |
.option pop | 从栈中弹出上次存入的.option设置 |
RISC-V 汇编指令 总览
RISC-V 汇编指令 操作对象
- 寄存器:
- 32个通用寄存器,x0~x31(注意:本章节课程仅涉及RCV32I的通用寄存器组);
- x0只读,读出来永远是0。其他的可读可写;
- 另外还有一个pc寄存器,常规下不可访问,用于保存当前指令的地址。
- 在RISC-V中,Hart(硬件线程,处理器执行的最小单元)在执行算数逻辑运算是操作的数据必须直接来自寄存器。
- 32个通用寄存器,x0~x31(注意:本章节课程仅涉及RCV32I的通用寄存器组);
- 内存:
- Hart可以执行在寄存器和内存之间的数据读写操作;
- 读写操作使用字节(Byte)为基本单位进行寻址;
- RV32可以访问最多2^32个字节的内存空间。
RISC-V 汇编指令 编码格式
6种指令格式(format)
- R-type:(Register),每条指令中有3个fields,用于指定3个寄存器参数。
- I-type:(Immediate),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度12bits)。
- S-type:(Store),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为12bits,但fields的组织方式不同于I-type)。
- B-tyep:(Branch),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为12bits,但取值为2的倍数)。
- U-type:(Upper),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为20bits,用于表示一个立即数的高20位)。
- J-type:(Jump),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为20bits)
RV32/64G 的 opcode 分类
riscv-spec-20191213.ptf 第147页
基础概念
- 指令长度:ILEN1=32bits(RV32I)
- 指令对齐:IALIGN=32bits(RV32I)
- 32个bit划分成不同的 “域(field)”。如,R-type分成了6个域。
- funct3/funct7 和 opcode 一起决定最终的指令类型
- 指令在内存中按照 小端序 排列
小端序概念
我的总结
-
小端序是相对大端序来的,他们共同的特征是:从内存的低地址依次向高地址读取和写入。
-
区别在于,对于0x12345678(下面是4字节对齐):
-
大端模式,即低地址存放高字节数:
-
小端模式,即低地址存储低字节数:
-
汪辰老师的讲述
-
主机字节序(HBO - Host Byte Order)
-
一个多字节整数在计算机内存中存储的字节顺序称为主机字节序(HBO - Host Byte Order,或者叫本地字节序)
-
不同类型CPU的HBO不同,这与CPU的设计有关。分为大端序(Bit-Endian)和小端序(Little-Endian)。
-
主机字节序(大端序 vs 小端序)
RISC-V 汇编指令分类
RISC-V 汇编伪指令一览
riscv-spec-20191213.ptf 第157页
RISC-V汇编 指令 详解
算数运算指令
基本概念
无符号数 v.s. 有符号数
- 有符号数在计算机中表示:二进制补码(two’s complement),表示一个负数,取反加一(反过来也是取反加一)
- 符号扩展(Sign extension) v.s. 零扩展(Zero extension)
部分指令介绍
-
ADD
语法 ADD RD, RS1, RS2 例子 add x5, x6, x7 x5 = x6 + x7 - 编码格式:R-type
- opcode(7): 0110011 (OP)
- funct3取值000; funct7取值0000000
- rs1(5): 第一个 operand(“source register 1”)
- rs2(6): 第二个 operand(“source register 2”)
- rd(5): “destination register” 用于存放求和的结果
- 编码格式:R-type
-
SUB(Substract)
语法 SUB RD, RS1, RS2 例子 sub x5, x6, x7 x5 = x6 - x7 - 编码格式:R-type
- opcode(7): 0110011 (OP)
- funct3取值000; funct7取值0100000
- rs1(5): 第一个 operand(“source register 1”)
- rs2(6): 第二个 operand(“source register 2”)
- rd(5): “destination register” 用于存放相减的结果
- 编码格式:R-type
-
ADDI(ADD Immediate)
语法 addi RD, RS1, IMM 例子 addi x5, x6, -2 x5 = x6 + (-2) -
编码格式:I-type
- opcode(7): 0010011 (OP-IMM)。
- funct3: 000,和 opcode 一起决定最终的指令类型。
- rs1(5): 第一个 operand(“source register 1”)。
- imm(12): “immediate”,立即数,代替了 R-type 的第三个寄存器参数和 func7。
- 立即数占12位。
- 在参与算数运算前该 立即数 会被“符号扩展”为一个32位的数。
- 这个立即数可以表达的数值范围为:[-2^11, +2^11),既[-2048, 2047)。
- rd(5): “destination register” 用于存放求和的结果 。
-
注意:RISC-V ISA并没有提供 SUBI 指令
- 因为可以用 加上 一个负的立即数 表示
- x5 = x6 + (-4)
-
ADDI的局限性
给一个寄存器赋值的数值范围只有:[-2048, 2047)。如果要赋值一个大数(32位)怎么办?
√ 解决思路:自己构造一个。具体做法:
- 先引入一个新的命令(LUI)设置高20位,存放 rs1
- 然后符用现有的 ADDI 命令补上剩余的低 12 位即可
-
-
LUI(Load Upper Immediate)
语法 LUI RD, IMM 例子 lui x5, 0x12345 x5 = 0x12345 << 12 -
LUI 指令采用 U-type:
- opcode(7): 0110111 (LUI)。
- rd(5): “destination register” 用于存放结果。
- imm(20): “immediate”,立即数。
-
LUI 指令会构造一个 32bits 的立即数,这个立即数的高 20 位对应指令中的 imm ,低 12 位清零。这个立即数作为结果存放在 RD 中。
-
练习:
利用 LUI + ADDI 来为寄存器加载一个大数
0x12345678
lui x1, 0x12345 # x1 = 0x12345000 addi x1, x1, 0x678 # x1 = x1 + 0x678
0x12345fff
# 错误写法 # addi 中的 0xfff # 会被符号扩展成一个32位的数字 0xffffffff lui x1, 0x12345 # x1 = 0x12345000 addi x1, x1, 0xfff # x1 = x1 + 0xfff # 正确写法 # 借位写法 给0xfff加上1给到x1, # 再给x1减去1 # 相当于借的那一位1位 0x1000 - 1 = 0xfff lui x1, 0x12346 addi x1, x1, -1
上面的写法虽然可以实现,但是还是过于麻烦,而且对于0x12345fff 可以用借位的写法,但是对于0xffff ffff就借不了位了,这个时候我们可以用 li 伪指令解决。
-
-
LI(Load Immediate)
语法 LI RD, IMM 例子 li x5, 0x12345678 x5 = 0x12345678 - LI 是一个伪指令(pseudo-instruction)
- 汇编器会根据 IMM 的实际情况自动生成正确的真实指令(instruction)
-
AUIPC
语法 AUIPC RD, IMM 例子 auipc x5, 0x12345 x5 = 0x12345 << 12 + pc - AUIPC 指令采用 U-type
- 和 LUI 指令类似, AUIPC 指令也会构造一个 32 bits 的立即数,这个立即数的高 20 位对应指令中的 imm,低12位清零。但和 LUI 不同的是,AUIPC 会将这个立即数和 PC 值相加,将相加的结果存放在 RD 中。
-
LA(Load Address)
语法 LA RD, LABEL 例子 la x5, foo - LA 是一个伪指令(pseudo-instruction)。
- 具体编程时给出需要加载的 label,编译器会根据实际情况利用 auipc 和其他指令自动生成正确的指令序列。
- 常用于加载一个函数或者变量的地址。
算数指令总结
指令 | 语法 | 描述 | 例子 |
---|---|---|---|
ADD | ADD RD, RS1, RS2 | RS1和rS2的值相加,结果保存到RD | add x5, x6, x7 |
SUB | SUB RD, RS1, RS2 | RS1的值减去RS2的值,结果保存到RD | sub x5, x6, x7 |
ADDI | ADDI RD, RS1, IMM | RS1的值和IMM相加,结果保存到RD | addi x5, x6, 100 |
LUI | LUI RD, IMM | 构造一个32位的数,高20位存放IMM,低12位清零。结果保存到RD | lui x5, 0x12345 |
AUIPC | AUIPC RD, IMM | 构造一个32位的数,高20位保存到IMM,低12位清零。结果和PC相加后保存到RD | auipc x5, 0x12345 |
基于算数运算指令实现的其他伪指令
伪指令 | 语法 | 等价指令 | 指令描述 | 例子 |
---|---|---|---|---|
LI | LI RD, IMM | LUI和ADDI的组合 | 将立即数 IMM 加载到 RD 中 | li x5, 0x12345678 |
LA | LA RD, LABEL | AUIPC 和 ADDI 的组合 | 为RD加载一个地址值 | la x5, label |
NEG | NEG RD, RS | SUB RD, x0, RS | 对RS中的值取反并将结果存放在RD中 | neg x5, x6 |
MV | MV RD, RS | ADDI RD, RS, 0 | 将RS中的值拷贝到RD中 | mv x5, x6 |
NOP | NOP | ADDI x0, x0, 0 | 什么也不做 | nop |
逻辑运算指令(Logical instruction)
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
AND | R-type | AND RD, RS1, RS2 | RD = RS1 & RS2 | and x5, x6, x7 |
OR | R-type | OR RD, RS1, RS2 | RD = RS1 | RS2 | or x5, x6, x7 |
XOR | R-type | XOR RD, RS1, RS2 | RD = RS1 ^ RS2 | xor x5, x6, x7 |
ANDI | I-type | ANDI RD, RS1, IMM | RD = RS1 & IMM | andi x5, x6, 20 |
ORI | I-type | ORI RD, RS1, IMM | RD = RS1 | IMM | or x5, x6, 20 |
XORI | I-type | XORI RD, RS1, IMM | RD = RS1 ^ IMM | xor x5, x6, 20 |
- 所有的逻辑指令都是按位操作
- XOR(eXclusive OR,“异或”):两个 bit 值 不同(异)则取值为 1(达到类似取 1为OR的效果);如果 两个 bit相同则取值为 0。
- 取反就异或上二进制表示全是 1 的值
伪指令 | 语法 | 等价指令 | 描述 | 例子 |
---|---|---|---|---|
NOT | NOT RD, RS | XORI RD, RS, -1 | 对 RS 的值按位取反,结果存放在 RD 中 | not x5, x6 |
移位运算指令(Shifting Instruction)
逻辑移位
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
SLL | R-type | SLL RD, RS1, RS2 | 逻辑左移(Shift Left Logical)RD = RS1 << RS2 | sll x5, x6, x7 |
SRL | R-type | SRL RD, RS1, RS2 | 逻辑右移(Shift Right Logical)RD = RS1 >> RS2 | srl x5, x6, x7 |
SLLI | I-type | SLLI RD, RS1, IMM | 逻辑左移立即数(Shift Left Logical Immediate)RD = RS1 << IMM | slli x5, x6, 3 |
SRLI | I-type | SRLI RD, RS1, IMM | 逻辑右移立即数(Shift Right Logical Immediate)RD = RS1 >> IMM | srli x5, x6, 3 |
无论是逻辑左移还是逻辑右移,补足的都是 0
算数移位
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
SRA | R-type | SRA RD, RS1, RS2 | 算数右移(Shift Right Arithmetic) | sra x5, x6, x7 |
SRAI | I-type | SRAI RD, RS1, RS2 | 算数右移立即数(Shift Right Arithmetic Immediate) | srai x5, x6, 3 |
- 算数右移按照符号位值补足。
- 对于算数移位,为什么没有算数左移呢,因为 左移符号位都被移走了,符号位没有意义。
内存读写指令(Load and Store Instruction)
- 内存读指令:Load,将数据从内存读入寄存器
- 内存写指令:Store,将数据从寄存器写出到内存
内存读(Load)
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
LB | I-type | LB RD, IMM(RS1) | Load Byte,从内存中读取一个8bits的数据到RD中,内存地址 = RS1 + IMM,数据在保存到RD之前会执行sign-extended。 | lb x5, 40(x6) |
LBU | I-type | LBU RD, IMM(RS1) | Load Byte Unsigned,从内存中读取一个8bits的数据到RD中,内存地址 = RS1 + IMM,数据在保存到RD之前会执行zero-extended。 | lbu x5, 40(x6) |
LH | I-type | LH RD, IMM(RS1) | Load Halfword,从内存中读取一个16bits的数据到RD中,内存地址 = RS1 + IMM,数据在保存到RD之前会执行sign-extended。 | lh x5, 40(x6) |
LHU | I-type | LHU RD, IMM(RS1) | Load Halfword Unsigned,从内存中读取一个16bits的数据到RD中,内存地址 = RS1 + IMM,数据在保存到RD之前会执行zero-extended。 | lhu x5, 40(x6) |
LW | I-type | LW RD, IMM(RS1) | Load Word,从内存中读取一个32bits的数据到RD中,内存地址 = RS1 + IMM。 | lw x5, 40(x6) |
注意:IMM给出的偏移量范围是[-2048, 2047]。
内存写(Store)
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
SB | S-type | SB RS2, IMM(RS1) | Store Byte,将RS2寄存器中低8bits的数据写出到内存,内存地址 = RS1 + IMM。 | sb x5, 40(x6) |
SH | S-type | SH RS2, IMM(RS1) | Store Halfword,将RS2寄存器中低16bits的数据写出到内存,内存地址 = RS1 + IMM。 | sh x5, 40(x6) |
SW | S-type | SW RS2, IMM(RS1) | Store Word,将RS2寄存器中32bits的数据写出到内存,内存地址 = RS1 + IMM。 | sw x5, 40(x6) |
注意:IMM给出的偏移量范围是[-2048, 2047]。
- 为上面对于load要区分无符号方式和有符号方式,而store不区分?
条件分支指令
指令 | 格式 | 语法 | 描述 | 例子 |
---|---|---|---|---|
BEQ | B-type | BEQ RS1, RS2, IMM | Branch if EQual。比较RS1和RS2的值,如果相等,则执行路径跳转到一个新的地址。 | beq x5, x6, 100 |
BNE | B-type | BNE RS1, RS2, IMM | Branch if Not EQual。比较RS1和RS2的值,如果不相等,则执行路径跳转到一个新的地址。 | bne x5, x6, 100 |
BLT | B-type | BLT RS1, RS2, IMM | Branch if Less Than。按照有符号方式比较RS1和RS2的值,如果RS1 < RS2,则执行路径跳转到一个新的地址。 | blt x5, x6, 100 |
BLTU | B-type | BLTU RS1, RS2, IMM | Branch if Less Than(Unsigned)。按照无符号方式比较RS1和RS2的值,如果RS1 < RS2,则执行路径跳转到一个新的地址。 | bltu x5, x6, 100 |
BGE | B-type | BGE RS1, RS2, IMM | Branch if Greater Than。按照有符号方式比较RS1和RS2的值,如果RS1 >= RS2,则执行路径跳转到一个新的地址。 | bge x5, x6, 100 |
BGEU | B-type | BGEU RS1, RS2, IMM | Branch if Greater Than(Unsigned)。按照无符号方式比较RS1和RS2的值,如果RS1 >= RS2,则执行路径跳转到一个新的地址。 | bgeu x5, x6, 100 |
- 跳转的目标地址计算方法:先将 IMM x 2,符号扩展后和PC值相加得到最终的目标地址,所以跳转范围是以PC为基准,+/- 4KB 左右([-4096, 4094])
- 具体编程时,不会直接写 IMM,而是用标号代替,交由连接器来最终决定IMM的值。
伪指令 | 语法 | 等价指令 | 描述 |
---|---|---|---|
BLE | BLE RS, RT, OFFSET | BGE RT, RS, OFFSET | Branch if Less & EQual,有符号方式比较,如果 RS <= RT,跳转到 OFFSTET |
BLEU | BLEU RS, RT, OFFSET | BGEU RT, RS, OFFSET | Branch if Less or EQual Unsigned,无符号方式比较,如果 RS <= RT,跳转到 OFFSTET |
BGT | BGT RS, RT, OFFSET | BLT RT, RS, OFFSET | Branch if Greater Than,有符号方式比较,如果 RS > RT,跳转到 OFFSTET |
BGTU | BGTU RS, RT, OFFSET | BLTU RT, RS, OFFSET | Branch if Greater Than Unsigned,无符号方式比较,如果 RS > RT,跳转到 OFFSTET |
BEQZ | BEQZ RS, OFFSET | BEQ RS, x0, OFFSET | Branch if EQual Zero,如果 RS == 0,跳转到OFFSET |
BNEZ | BNEZ RS, OFFSET | BNE RS, x0, OFFSET | Branch if Not EQual Zero,如果 RS != 0,跳转到OFFSET |
BLTZ | BLTZ RS, OFFSET | BLT RS, x0, OFFSET | Branch if Less Than Zero,如果 RS < 0,跳转到OFFSET |
BLEZ | BLEZ RS, OFFSET | BGE x0, RS, OFFSET | Branch if Less or EQual Zero,如果 RS <= 0,跳转到OFFSET |
BGTZ | BGTZ RS, OFFSET | BLT x0, RS, OFFSET | Branch if Greater Than Zero,如果 RS > 0,跳转到OFFSET |
BGEZ | BGEZ RS, OFFSET | BGE RS, x0, OFFSET | Branch if Greater or EQual Zero,如果 RS <= 0,跳转到OFFSET |
无条件跳转指令(Unconditional Jump Instruction)
-
JAL(Jump And Link)
语法 JAL RD, LABEL 例子 jal x1, label -
JAL指令使用J-type编码格式。
-
JAL指令用于调用子过程(subroutine/function)。
-
子过程的地址计算方式:首先对20bits宽的IMM x 2后进行sign-extended,然后将符号扩展后的值和PC的值相加。因此该函数跳转的范围是以PC为基准,上下~+/- 1MB。
-
JAL指令的下一条指令的地址写入RD,保存返回地址。
-
实际编程时,用label给出跳转的目标,具体IMM值由编译器和链接器最终负责生成。
-
该函数跳转的范围是以PC为基准,上下~+/- 1MB。
-
如何解决更远距离的跳转?
AUIPC x6, IMM-20 JALR x1, x6, IMM-12
-
-
-
JALR(Jump And Link Register)
语法 JALR RD, IMM(RS1) 例子 jalr x0, 0(x5) - JALR指令使用I-type编码格式。
- JALR指令用于调用子过程(subroutine/function)。
- 子过程的地址计算方法:首先对12bits宽的IMM进行sign-extended,然后将符号扩展后的值和RS1的值相加,得到最终的结果后将其最低位设置为0(确保地址按2字节对齐)。因此该函数跳转地址的范围是以RS1为基准,上下 ~+/- 2KB。
- JALR指令的下一条指令的地址写入RD,保存为返回地址。
如果跳转不需要返回,可以利用 x0 代题 JAL 和 JALR 中的 RD。
-
伪指令
伪指令 语法 等价指令 例子 J J OFFSET JAL x0, OFFSET j leap JR JR RS JARL x0, 0(RS) jr x2
RISC-V指令寻址模式总结
所谓寻址模式指的是指令中定位操作数(oprand)或者地址的方式。
寻址模式 | 解释 | 例子 |
---|---|---|
立即数寻址 | 操作数是指令本身的一部分。 | addi x5, x6, 20 |
寄存器寻址 | 操作数存放在寄存器中,指令中指定访问的寄存器从而获取该操作数。 | add x5, x6, x7 |
基址寻址 | 操作数在内存中,指令通过指定寄存器(基址base)和立即数(偏移offset),通过base+offset的方式获得操作数在内存中的地址从而获取该操作数。 | sw x5, 40(x6) |
PC 相对地址 | 在指令中通过PC和指令中的立即数相加获得目的地址的值。 | beq x5, x6, 100 |
RISC-V汇编 函数 调用约定
函数调用过程概述
汇编编程时为何需要制定函数调用约定
- 函数跳转时使用的寄存器,不同的人可能用不一样的,自己编程时不会去在意用哪个寄存器,但是多人协作时,别人不知道你的调用函数用的寄存器,这会导致一些不便,于是约定一个共同的函数调用寄存器,可以更好的有助于团队,以及其他人容易的调用你的代码。
函数调用过程中的编程约定
有关寄存器的编程约定
寄存器 | ABI名(编程用名) | 用途约定 | 谁负责在函数调用过程中维护这些寄存器 |
---|---|---|---|
x0 | zero | 读取时总为0,写入时不起任何效果 | N/A |
x1 | ra | 存放函数返回地址的值(return address) | Caller |
x2 | sp | 存放栈指针(stack pointer) | Callee |
x5-x7, x28-x31 | t0-t2, t3-t6 | 临时(temporaries)寄存器,Callee可能会使用这些寄存器,所以Callee不保证这些寄存器中的值在函数调用过程中保持不变,这意味着对于Caller来说,如果需要的话,Caller需要自己在调用Callee之前保存临时寄存器中的值。 | Caller |
x8, x9, x18-x27 | s0, s1, s2-s11 | 保存(saved)寄存器,Callee需要保证这些寄存器的值在函数返回后仍然维持函数调用之前的原值,所以一旦Callee在自己的函数中会用到这些寄存器,则需要在栈中备份并在退出函数时进行恢复。 | Callee |
x10, x11 | a0, a1 | 参数(argument)寄存器,用于在函数调用过程中保存第一个和第二个参数,以及在函数返回时传递返回值。 | Caller |
x12-x17 | a2-a7 | 参数(argument)寄存器,如果函数调用时需要传递更多的参数,则可以用这些寄存器,但注意用于传递参数的寄存器最多只有8个(a0-a7),如果还有更多的参数则需要利用栈。 | Caller |
函数跳转和返回指令的编程约定
伪指令 | 等价指令 | 描述 | 例子 |
---|---|---|---|
jal offset | jal x1, offset | 跳转 offset 制定位置,返回地址保存在 x1(ra) | jal foo |
jalr rs | jalr x1, 0(rs) | 跳转到 rs 中值所指定的位置,返回地址保存在 x1(ra) | jarl s1 |
j offset | jal x0, offset | 跳转到 offset 指定位置,不保存返回地址。 | j loop |
jr rs | jalr x0, 0(rs) | 跳转到 rs 中值所指定的位置,不保存返回地址。 | jr s1 |
call offset | auipc x1, offset[31:12]+offset[11] jalr x1, oofset[11:0](x1) | 长跳转调用函数 | call foo |
tail offset | auipc x6, offset[31:12]+offset[11] jalr x0, offset[11:0](x6) | 长跳转尾调用 | tail foo |
ret | jalr x0, 0(x1) | 从 Callee 返回 | ret |
实现被调用函数的编程约定
函数起始部分(Prologue) |
---|
减少 sp 的值,根据本函数中使用 saved 寄存器的情况以及 local 变量的多少开辟栈空间。 |
将 saved 寄存器的值保存到栈中。 |
如果函数中还会调用其他的函数,则将 ra 寄存器的值保存到栈中。 |
函数退出部分(Epilogue) |
---|
从栈中恢复 saved 寄存器 |
如果需要的话,从栈中恢复 ra 寄存器 |
增加 sp 的值,恢复到进入函数之前的状态 |
调用 ret 返回 |
RISC-V汇编 与 C 混合编程
RISC-V 汇编调用 C 函数
- 遵守ABI(Abstract Binary Interface)的规定
- 数据类型的大小,布局和对齐
- 函数调用约定(Calling Convention)
- 系统调用约定
- …
- RISC-V 函数调用约定规定:
- 函数参数采用寄存器 a0~a7 传递
- 函数返回值采用寄存器 a0 和 a1 传递
C函数中嵌入 RISC-V 汇编
asm [volatile](
“汇编指令”
: 输出操作数列表(可选)
: 输入操作数列表(可选)
: 可能影响的寄存器或者存储器(可选)
)
- 汇编指令用双引号括起来,多条指令至今啊用 “ ; ” 或者 “ \n ” 分隔
- “输出的操作数列表”和“输入的操作数列表”用于将需要操作的C变量和汇编指令的操作数对应起来,多个操作数之间用 “ , ” 分隔。
- “可能影响的寄存器或者存储器” 用于告知编译器当前嵌入的汇编语句可能修改的寄存器或者内存,方便编译器执行优化。
- [volatile]:加了这个表示里面的代码不需要优化了,可以省略不写。
例子:
- 这里的 [sum]“r”(a),意思是 sum 绑定变量 a,以 r(寄存器)的形式,若是 m,则是堆栈的形式。
- 第二个例子中,0,1,2是按顺序绑定的。
更多见【参考三】
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)