Menu Close

RISC-V汇编入门

如果你是学习汇编编程的新手,那么RISC-V是一个很好的起点。

构建基于RISC-V的个人电脑的主板
构建基于RISC-V的个人电脑的主板

RISC-V的设计理念侧重于简单性和模块化,使其成为那些初学汇编编程或者编码一般知识有限的人的绝佳选择。

如果你对汇编编程一无所知,或者对编码的了解非常有限,那么RISC-V可能是开始学习汇编语言的更好选择之一。当然,有更多的x86汇编教程可供参考,也有更多的人可以帮助你。但x86是一种庞大的体系结构,拥有超过1500个不同的指令。

相比之下,RISC-V的设计专门旨在易于教学,同时又足够实用,可以实际允许高性能微处理器的实现。

以下是一些强调其适合初学者的特点:

  1. 简单性: 与x86等复杂体系结构相比,RISC-V具有较少的指令集。减小的指令集使学习和理解汇编编程的基本概念更为容易。
  2. 模块化: RISC-V的模块化设计允许更为系统和逐步地学习汇编编程。指令集的组织结构清晰而逻辑,有助于更平稳地学习过程。
  3. 教学焦点: RISC-V的设计考虑到了教育的需要。其简单性和清晰性使其成为学术环境中的理想选择,其中的目标是教授计算机体系结构和汇编编程的基本原理。
  4. 实用设计: 尽管简单,RISC-V仍然是一种实用且功能强大的体系结构,适用于实现高性能微处理器。这使其成为初学者的绝佳选择,他们不仅想要学习基础知识,还希望在进步过程中深入探讨更高级的主题。

虽然x86汇编广泛使用,但其复杂性可能对初学者来说过于庞大。RISC-V为初学者提供了更易接近的汇编编程入门点,使初学者能够在深入研究更为复杂的体系结构之前理解核心概念。RISC-V的优点在于它是一种现代而简单的指令集,专为现代硬件现实而设计,例如较慢的主存储器、分支预测器的使用、超标量乱序执行单元等。

设置并开始学习RISC-V汇编语言

为了尽可能简化这个过程,我将使用康奈尔大学提供的在线RISC-V解释器。下列链接很重言哦,请放到书签中。

https://www.cs.cornell.edu/courses/cs3410/2019sp/riscv/interpreter/

你可以在下面看到一张截图。最左侧的橙色圆圈显示了一个简单的程序,只有一条指令,将寄存器x1和x2中的两个数相加,然后将结果存储在寄存器x3中。

RISC-V解释器
RISC-V解释器

寄存器是微处理器内部的内存单元。微处理器无法直接在存储器(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”按钮。在程序运行时,你可以看到它在下方的框中打印出正在执行的代码行。

要再次运行程序,你需要点击蓝色的“Reset”按钮。

既然你已经了解了如何插入程序、输入数据并运行程序的基础知识,让我们开始讨论RISC-V微处理器及其编程方式。

RISC-V寄存器的使用

RISC-V有32个通用寄存器,命名为x0到x31。第一个寄存器x0是特殊的,因为它始终为零。无论你将什么值复制到它,如果你尝试从中读取,它始终会产生零。这可能看起来有点奇怪,但实际上非常实用。有很多操作需要使用零这个数字。通过始终将一个寄存器设为零,你就不必将值0复制到它。对于那些熟悉Unix的人来说,它有点类似于/dev/null的作用。它是一个用于发送你想要丢弃数值的地方。

所有寄存器都有一个别名(括号中的名称),它有助于提醒代码编写者该寄存器的用途。对于x0寄存器的替代名称是zero。这意味着以下两行是等效的:

ADDI x2, x0  , 1
ADDI x2, zero, 1

后一种写法更易读,因为它提醒你寄存器正在被用于什么目的。

register (寄存器)
register (寄存器)
RIsc-V指令集
RISC-V指令集

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的设计者们尽力使事情规范化,以便为解码器提供便利。

RISC-V 指令集
RISC-V 指令集
  • opcode :指令操作码
  • imm:代码立即数
  • func3和funct7:代表指令对应的功能
  • rs1:源寄存器1
  • rs2:源寄存器2
  • rd:目标寄存器(RSIC-V 一个指令可以提供三个寄存器操作)
RSIC-V共六种指令格式
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 实现成本。

 RISC-V 采用的是模块化的指令集
RISC-V 采用的是模块化的指令集

寄存器

 

 

qwe

除教程外,本网站大部分文章来自互联网,如果有内容冒犯到你,请联系我们删除!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Leave the field below empty!

Posted in RISC-V教程

Related Posts