如果你是学习汇编编程的新手,那么RISC-V是一个很好的起点。
RISC-V的设计理念侧重于简单性和模块化,使其成为那些初学汇编编程或者编码一般知识有限的人的绝佳选择。
如果你对汇编编程一无所知,或者对编码的了解非常有限,那么RISC-V可能是开始学习汇编语言的更好选择之一。当然,有更多的x86汇编教程可供参考,也有更多的人可以帮助你。但x86是一种庞大的体系结构,拥有超过1500个不同的指令。
相比之下,RISC-V的设计专门旨在易于教学,同时又足够实用,可以实际允许高性能微处理器的实现。
以下是一些强调其适合初学者的特点:
- 简单性: 与x86等复杂体系结构相比,RISC-V具有较少的指令集。减小的指令集使学习和理解汇编编程的基本概念更为容易。
- 模块化: RISC-V的模块化设计允许更为系统和逐步地学习汇编编程。指令集的组织结构清晰而逻辑,有助于更平稳地学习过程。
- 教学焦点: RISC-V的设计考虑到了教育的需要。其简单性和清晰性使其成为学术环境中的理想选择,其中的目标是教授计算机体系结构和汇编编程的基本原理。
- 实用设计: 尽管简单,RISC-V仍然是一种实用且功能强大的体系结构,适用于实现高性能微处理器。这使其成为初学者的绝佳选择,他们不仅想要学习基础知识,还希望在进步过程中深入探讨更高级的主题。
虽然x86汇编广泛使用,但其复杂性可能对初学者来说过于庞大。RISC-V为初学者提供了更易接近的汇编编程入门点,使初学者能够在深入研究更为复杂的体系结构之前理解核心概念。RISC-V的优点在于它是一种现代而简单的指令集,专为现代硬件现实而设计,例如较慢的主存储器、分支预测器的使用、超标量乱序执行单元等。
设置并开始学习RISC-V汇编语言
为了尽可能简化这个过程,我将使用康奈尔大学提供的在线RISC-V解释器。下列链接很重言哦,请放到书签中。
https://www.cs.cornell.edu/courses/cs3410/2019sp/riscv/interpreter/
你可以在下面看到一张截图。最左侧的橙色圆圈显示了一个简单的程序,只有一条指令,将寄存器x1和x2中的两个数相加,然后将结果存储在寄存器x3中。
寄存器是微处理器内部的内存单元。微处理器无法直接在存储器(RAM)中存储的数据上执行操作。在执行操作之前,它需要将数据加载到寄存器中。
在右侧,你可以看到一个寄存器列表。名为“Register”的第二列显示了寄存器的名称。
在第一列中,我们可以设置寄存器的初始值。在橙色圆圈中,你可以看到寄存器x1和x2的初始值分别设置为3和4。
ADD x3, x2, x1
点击绿色的“Run”按钮来运行程序。它会将寄存器x1和x2相加,并将结果存储在寄存器x3中。我已经运行了上述程序,所以你可以看到十进制一列显示了寄存器x3的值为7。
我们也可以复制粘贴一个较大的程序并尝试运行。我们看一个倒计时程序。
在下述程序中,输入被放置在寄存器x1中。通过编辑“Init Value”列,将数字(例如14)放入寄存器。我们从14开始往下数。
ADDI x2, x0, 1 loop: SUB x1, x1, x2 SW x1, 4(x0) BLT x0, x1, loop HLT
通过查看寄存器或存储器的十进制列,你可以看到倒计时的发生。倒计时发生在存储器地址0x04的第二行。
在运行之前,你应该将CPU设置为2HZ。这意味着每秒执行两条指令。如果CPU运行得更快,不容易看到发生的情况。之后,你可以点击绿色的“Run”按钮。在程序运行时,你可以看到它在下方的框中打印出正在执行的代码行。
既然你已经了解了如何插入程序、输入数据并运行程序的基础知识,让我们开始讨论RISC-V微处理器及其编程方式。
RISC-V寄存器的使用
RISC-V有32个通用寄存器,命名为x0到x31。第一个寄存器x0是特殊的,因为它始终为零。无论你将什么值复制到它,如果你尝试从中读取,它始终会产生零。这可能看起来有点奇怪,但实际上非常实用。有很多操作需要使用零这个数字。通过始终将一个寄存器设为零,你就不必将值0复制到它。对于那些熟悉Unix的人来说,它有点类似于/dev/null的作用。它是一个用于发送你想要丢弃数值的地方。
所有寄存器都有一个别名(括号中的名称),它有助于提醒代码编写者该寄存器的用途。对于x0寄存器的替代名称是zero。这意味着以下两行是等效的:
ADDI x2, x0 , 1 ADDI x2, zero, 1
后一种写法更易读,因为它提醒你寄存器正在被用于什么目的。
RSIC-V 寄存器说明
寄存器 | ABI 名称 | 说明 |
x0 | zero | 0值寄存器,硬编码为0,写入数据忽略,读取数据为0 |
x1 | ra | 用于返回地址(return address) |
x2 | sp | 用于栈指针(stack pointer) |
x3 | gp | 用于通用指针 (global pointer) |
x4 | tp | 用于线程指针 (thread pointer) |
x5 | t0 | 用于存放临时数据或者备用链接寄存器 |
x6~x7 | t1~t2 | 用于存放临时数据寄存器 |
x8 | s0/fp | 需要保存的寄存器或者帧指针寄存器 |
x9 | s1 | 需要保存的寄存器 |
x10~x11 | a0~a1 | 函数传递参数寄存器或者函数返回值寄存器 |
x12~x17 | a2~a7 | 函数传递参数寄存器 |
x18~x27 | s2-s11 | 需要保存的寄存器 |
x28~x31 | t3~t6 | 用于存放临时数据寄存器 |
ABI: 应用程序二进制接口,可以理解为寄存器别名,高级语言在生成汇编会用到。
这些命名与由高级语言编译器生成的RISC-V代码使用的约定相关。比如,你编写了一些C代码,如下所示:
int calculate(int x, int y) { int tmp = x * 10; return tmp + y; } int main() { int alpha = 4; int beta = calculate(2, 3); return alpha + beta; }
以上程序RISC-V汇编代码将如何执行?我们不讨论使用的指令,而只讨论寄存器。
在第7行,我们将值4存储在变量alpha中。
我们将需要一个寄存器来存储该值。
因为寄存器可能被在接下来的第8行调用的calculate函数使用,我们需要确保alpha被保留。我们如何做到这一点?在过去,汇编程序员通常会使用内存。特别是一个我们称之为栈的位置。然而,在那个时代,主存储器相对于CPU来说是很快的。而今天,与内存相比,CPU的速度非常快。当你把东西存储在内存中时,可能已经执行了数百条指令。因此,我们总是尽力避免对内存进行读写。
RISC-V采取的解决方案是制定一种约定。如果你将alpha的值放在s寄存器中的一个(s0到s6,对应x8、x9以及x18到x27),那么它将被保存。调用的子程序必须承诺不更改这些寄存器,或者如果更改了,要在返回前保存并恢复它们。
不是每个寄存器都可以是s寄存器,否则我们就不能使用任何寄存器而不保存它们。解决这个问题的方法是使用t寄存器,它们是临时的。
calculate函数内部的tmp变量可以存储在例如t0中,因为它不需要被保留。
接下来的问题是如何传递参数给函数(带参数返回值的子程序)?在x86 Intel处理器上经常使用的传统方法是将参数放在堆栈上的内存中。但是再次强调,不必要的内存访问是不好的。RISC-V的解决方案是使用a寄存器a0到a6来存储函数参数。x和y参数分别传递在a0和a1中。这对应于x10和x11。计算的结果返回在a0中。
当我们在第8行调用calculate时,我们希望能够返回并继续执行第9行。在这里的约定是将返回地址存储在ra寄存器中,它对应于x1。再次强调,如果你有来自CISC汇编代码(例如x86)的背景,你可能习惯于将这个放在堆栈上。
当然,最终我们会用完寄存器。对于我们如何将数据存储在内存中,也有相应的约定,但我在这里不会涉及到。
RISC-V 指令
在我们查看具体的指令之前,了解RISC-V指令的常见模式可能会很有用。如果你查看下面的代码,你会发现几乎所有的指令都有3个参数。
ADDI x2, x0, 1 loop: SUB x1, x1, x2 SW x1, 4(x0) BLT x0, x1, loop
这些参数在大多数指令中的使用方式非常相似。实际上,抛弃这个说法。在汇编代码中我们不称之为参数。参数是用于函数的。
汇编指令有操作数,而指令的类型是操作码。因此,下面的整行都是一条指令:
ADDI x2, x0, 1
虽然ADDI部分是操作码,而x2、x0和1是操作数。更准确地说,ADDI是一个助记符,是实际数值操作码的一个别名,在机器代码中使用。
不管怎样,典型的RISC-V指令遵循这种格式:
OPCODE rd, rs1, rs2
从编程角度来看,我们可以将其理解为:
rd = f(rs1, rs2)
操作码指定在两个源寄存器rs1和rs2上执行的某个函数f,并生成一个结果,该结果存储在目标寄存器rd中。对于ADD和SUB指令,通常写成:
rd ← rs1 + rs2 rd ← rs1 - rs2
在RISC-V汇编中,注释以井号#开头,就像在脚本语言中一样
ADDI x2, x0, 1 # x2 ← x0 + 1 BLT x0, x1, loop # IF x0 < x1 GOTO loop
RISC-V 指令集架构
RISC-V 指令有以下特点:
- 完全开放
- 指令简单
- 模块化设计,易于扩展
名称 | 类别 | 说明 |
RV32I | 基础指令 | 整数指令:包含算法、分支、逻辑、访存指令,有32个32位寄存器。能寻址32位地址空间 |
RV64I | 基础指令 | 整数指令:包含算法、分支、逻辑、访存指令,有32个64位寄存器。能寻址64位地址空间 |
RV128I | 基础指令 | 整数指令:包含算法、分支、逻辑、访存指令,有32个128位寄存器。能寻址128位地址空间 |
RV32E | 基础指令 | 与RV32I一样,只不过只使用前16个(0~15)32位寄存器 |
M | 扩展指令 | 包含乘法、除法、取模求余指令 |
F | 扩展指令 | 单精度浮点指令 |
D | 扩展指令 | 双精度浮点指令 |
Q | 扩展指令 | 四倍精度浮点指令 |
A | 扩展指令 | 原子操作指令:比如比较并交换,读改写等指令 |
C | 扩展指令 | 压缩指令:单指令长度为16位,主要用于改善程序大小 |
P | 扩展指令 | 单指令多数据(Packed-SIMD)指令 |
B | 扩展指令 | 位操作指令 |
H | 扩展指令 | 支持(Hypervisor)管理指令 |
J | 扩展指令 | 支持动态翻译语言指令 |
L | 扩展指令 | 十进制浮点指令 |
N | 扩展指令 | 用户中断指令 |
G | 通用指令 | 包含I、M、A、F、D 指令 |
名称 | 类别 | 说明 |
RV32I | 基础指令 | 整数指令:包含算法、分支、逻辑、访存指令,有32个32位寄存器。能寻址32位地址空间 |
RV64I | 基础指令 | 整数指令:包含算法、分支、逻辑、访存指令,有32个64位寄存器。能寻址64位地址空间 |
RV128I | 基础指令 | 整数指令:包含算法、分支、逻辑、访存指令,有32个128位寄存器。能寻址128位地址空间 |
RV32E | 基础指令 | 与RV32I一样,只不过只使用前16个(0~15)32位寄存器 |
M | 扩展指令 | 包含乘法、除法、取模求余指令 |
F | 扩展指令 | 单精度浮点指令 |
D | 扩展指令 | 双精度浮点指令 |
Q | 扩展指令 | 四倍精度浮点指令 |
A | 扩展指令 | 原子操作指令:比如比较并交换,读改写等指令 |
C | 扩展指令 | 压缩指令:单指令长度为16位,主要用于改善程序大小 |
P | 扩展指令 | 单指令多数据(Packed-SIMD)指令 |
B | 扩展指令 | 位操作指令 |
H | 扩展指令 | 支持(Hypervisor)管理指令 |
J | 扩展指令 | 支持动态翻译语言指令 |
L | 扩展指令 | 十进制浮点指令 |
N | 扩展指令 | 用户中断指令 |
G | 通用指令 | 包含I、M、A、F、D 指令 |
要满足现在操作系统和应用程序的基本运行,RV32G指令集或者RV64G指令集就够了。
RV32G和RV64G指令集只有寄存器位宽和寻址大小不同。这些指令按照功能可以分为如下几类:
- 整数运算指令:算术、逻辑、比较等基础运算功能。
- 分支转移指令:实现条件转移、无条件转移操作
- 加载存储指令:实现字节、半字(half word)、字(word)、双字(RV64I)的加载,存储操作,采用的都是寄存器相对寻址方式
- 控制与状态寄存器访问指令:实现对系统控制与系统状态寄存器的原子读-写、原子读-修改、原子读-清零等操作
- 系统调用指令:实现系统调用功能。
- 原子指令:用于各种同步锁
- 单双浮点指令:实现浮点运算操作
从上表我们可以看到,RISC-V 指令集具有模块化特点。这就允许我们根据自己的需求,选择一个基础指令集,加上若干个扩展指令集灵活搭配,就可以得到我们想要的指令集架构,进而根据这样的指令架构,设计出贴合我们需求的CPU.
作为初学者,我们了解RISC-V 的核心即可。它的最核心部分是一个基础指令集,叫做RV32I.
RV32I 包含的指令是固定不变的,这为编译器设计人员,操作系统开发人员和汇编语言程序员提供了稳定的基础框架。
RV32I 指令集
如果你习惯于使用高级语言,那么函数不太可能接受固定数量的参数。然而,汇编代码指令是以32位的固定宽度格式编码的。为了使CPU内部的解码器更容易确定指令的功能,将指令的不同部分放在固定的位置是有帮助的。
下面的图表显示了用于编码指令的32位字中不同位的用途。如果你看一下这个图表,你会发现RISC-V的设计者们尽力使事情规范化,以便为解码器提供便利。
- opcode :指令操作码
- imm:代码立即数
- func3和funct7:代表指令对应的功能
- rs1:源寄存器1
- rs2:源寄存器2
- rd:目标寄存器(RSIC-V 一个指令可以提供三个寄存器操作)
六种指令格式作用如下:
序号 | 指令类型 | 作用 |
1 | R 型指令 ( Register) |
用于寄存器和寄存器操作 |
2 | I 型指令 (Immediate) |
用于短立即数和内存载入指令load操作 |
3 | S 型指令 (Store) |
用于内存存储store操作 |
4 | B 型指令 (Branch) |
用于有条件跳转操作 |
5 | U 型指令 (Upper Immediate) |
用于长立即数操作 |
6 | J 型指令 (Jump) |
用于无条件跳转操作 |
指令集模块是一款CPU架构的主要组成部分,是CPU 和 上层软件交互的核心,也是cpu主要功能体现。
RISC-V 规范只定义了CPU 需要包含的基础整型操作指令:
-
整型的储存 -
加载 -
加减 -
逻辑 -
移位 -
分支 -
等。
其他指令为可选指令或者用户扩展指令。比如:
-
乘 -
除 -
取模 -
单精度浮点 -
双精度浮点 -
压缩 -
原子指令 -
等。
扩展指令是芯片工程师根据需求自定义。
所以 RISC-V 采用的是模块化的指令集,易于扩展、组装。它适用于不同的应用场景,可以降低 CPU 实现成本。
寄存器
qwe