1. RISC-V指令集概述

RISC-V指令集采用模块化,增量化设计理念

1.1 如何查看一个系统支持的指令集

cat /proc/cpuinfo

processor: 0
hart: 0
isa: rv64imafdcsu
mmu: sv48

2. 指令集扩展

从isa可知该系统支持的扩展指令集为 rv64imafdcsu:

  • 64位整型最小指令集合 (I)
  • 整型乘法和除法扩展指令 (M)
  • 原子操作 (A)
  • 单精度和双精度浮点数扩展指令 (F, D)
  • 压缩扩展指令 ©
  • 特权模式 (S)
  • 用户模式 (U)

3. 指令编码

RISC-V指令集采用模块化设计,支持多种指令编码方式:

3.1 编码方式对比

变长指令编码 (Variable-length Instruction Encoding):

  • 指令编码长度不固定
  • 空间不受限制
  • 芯片和编译器设计复杂

定长指令编码 (Fixed-length Instruction Encoding):

  • 指令编码长度固定
  • 芯片和编译器设计简单
  • 缺点:指令编码空间固定,受限

3.2 RISC-V指令编码特点

  • RISC-V CPU大部分采用32/16位固定编码
  • 更宽的指令编码的缺点:
    • Code size变大
    • I cache命中率会下降
  • RISC-V规范支持变长指令编码

3.3 指令长度编码格式

RISC-V支持多种指令长度,通过特定的位模式进行识别:

                         xxxxxxxxxxxxxxaa: 16位 (aa ≠ 11)
        xxxxxxxxxxxxxxxx xxxxxxxxxxxbbb11: 32位 (bbb ≠ 111)
...xxxx xxxxxxxxxxxxxxxx xxxxxxxxxx011111: 48位
...xxxx xxxxxxxxxxxxxxxx xxxxxxxxx0111111: 64位
...xxxx xxxxxxxxxxxxxxxx xnnnxxxxx1111111: (80+16*nnn)位 (nnn ≠ 111)
...xxxx xxxxxxxxxxxxxxxx x111xxxxx1111111: 保留用于≥192位
base+4     base+2              base

3.4 RISC-V指令编码格式详解

RISC-V指令编码格式是其模块化设计的重要体现。

3.4.1 指令宽度
  • 标准指令宽度: 32位。
  • 压缩指令宽度: 16位 (用于减小代码体积)。
  • 未来支持: 规范也支持64位指令,但目前尚未广泛应用。
3.4.2 指令编码目的

指令编码的目的是让CPU能够理解并执行特定的操作。
例如,让CPU执行加法运算:

add x3, x1, x2 // 表示 x1 + x2 = x3
3.4.3 指令Layout的关键元素

RISC-V指令的布局(layout)通常包含以下关键元素:

  • 指令分类 Opcode: 用于指示指令的基本操作类型。
  • 源操作数寄存器1 (rs1): 第一个源操作数的寄存器地址。
  • 源操作数寄存器2 (rs2): 第二个源操作数的寄存器地址。
  • 目标寄存器 (rd): 存储运算结果的寄存器地址。
  • 立即数 (Immediate): 指令中直接包含的常量值。
3.4.4 寄存器与指令分类的位分配
  • RISC-V架构支持32个通用寄存器。
  • 索引这32个寄存器需要5位(因为 2^5 = 32)。
  • 对于一个典型的三操作数指令(如 add x3, x1, x2),需要三个寄存器字段:源1 (rs1)、源2 (rs2) 和目标 (rd)。
  • 这三个字段共占用 5 + 5 + 5 = 15 位。
  • 在32位指令宽度中,剩余的 32 - 15 = 17 位可以用于指令分类(opcode)和其他功能码。
3.4.5 指令集设计考量

设计一套全新的指令集,其核心在于构思指令的布局(instruction layout),确保CPU和汇编器都能遵循这套统一的布局。

设计难点:
指令集设计是一门艺术,需要平衡芯片设计、编译器优化和操作系统支持等多个方面,以实现高效的整体性能。

3.5 RISC-V指令布局类型

RISC-V指令根据其布局特征,可以分为6种基本类型:

3.5.1 指令类型说明
类型 名称 用途 说明
R类型 Register-to-Register 寄存器与寄存器算术指令 如:add x1, x2, x3
I类型 Register-to-Immediate 寄存器与立即数算术指令或加载指令 如:addi x1, x2, 100
S类型 Store 存储指令 如:sw x1, 8(x2)
B类型 Branch 条件跳转指令 如:beq x1, x2, label
U类型 Upper Immediate 长立即数操作指令 如:lui x1, 0x12345
J类型 Jump 无条件跳转指令 如:jal x1, label
3.5.2 指令布局字段说明
字段名 位范围 用途 说明
opcode [6:0] 操作码字段 用于指令的分类
funct3 [14:12] 功能码字段 与操作码字段结合定义指令操作功能
funct7 [31:25] 功能码字段 与操作码字段结合定义指令操作功能
rd [11:7] 目标寄存器编号 存储运算结果的寄存器
rs1 [19:15] 源操作寄存器1编号 第一个源操作数寄存器
rs2 [24:20] 源操作寄存器2编号 第二个源操作数寄存器
imm 可变 立即数 带符号扩展的立即数值
3.5.3 32位指令布局图

指令格式
指令编码整齐,rs1,rs2,rd都在固定位置,便于RTL实现,减少选择器。
立即数均是有符号数,加载到寄存器中需要进行符号扩展。

  • 例如12位有符号数0x823,对应的二进制是 0b1000 0010 0011,最高位为1代表它是一个负数
  • 计算过程:取反加1得到 0b0111 1101 1101 = 2013,所以0x823 = -2013

RV64指令集的数据和地址宽度为64位,为什么指令编码只有32位?

  • 基于寄存器加载和存储的架构设计,所有的数据处理都是在通用寄存器中完成的
  • 使用5bit可以索引32个通用寄存器
  • 使用32位指令编码比64位指令编码有利于提高代码密度,更能提高指令cache命中概率。

RISC-V约定下面两种情况为非法指令:

  • 低16位全是0,有可能跳转到刚初始化的内存区域
  • 32位全为1,可能跳转到没有编程的存储器设备,断开的内存总线,出错的设备等
  • 方便程序员提前捕捉错误

3.6 RISC-V汇编指令书写说明

RV64指令集中常用的符号说明如下:

3.6.1 寄存器符号
  • rd: 表示目标寄存器,可以从x0~x31通用寄存器中选择。
  • rs1: 表示源寄存器1,可以从x0~x31通用寄存器中选择。
  • rs2: 表示源寄存器2,可以从x0~x31通用寄存器中选择。
3.6.2 寻址模式符号
  • (): 通常用来表示寻址模式。
    • (a0) 表示以a0寄存器的值为基地址进行寻址。
    • 这个前面还可以加offset,表示偏移,例如8(a0),表示以a0寄存器的值为基地址,然后偏移8个字节进行寻址。
3.6.3 其他符号
  • {}: 表示可选项。
  • imm: 表示立即数。

4. 加载指令格式说明

RISC-V的加载指令(Load Instruction)格式如下所示,它定义了如何从内存中读取数据并存入寄存器。

4.1 指令汇编语法示例

l{d|w|h|b}{u} rd, offset(rs1)

4.2 指令位字段图

位范围 31-20 19-15 14-12 11-7 6-0
字段 imm[11:0] rs1 funct3 rd opcode
位数 12 5 3 5 7
说明 offset[11:0] base width dest LOAD

4.3 符号和字段解释

  • {d|w|h|b}: 表示加载的数据宽度。
    • d: 双字 (Double-word)
    • w: 字 (Word)
    • h: 半字 (Half-word)
    • b: 字节 (Byte)
  • {u}: 是可选项。如果存在,表示加载的数据为无符号数,即采用零扩展方式(Zero-extended)。如果不存在,表示加载的数据为有符号数,即采用符号扩展方式(Sign-extended)。
  • rd: 表示目标寄存器,数据将加载到此寄存器中。
  • rs1: 表示源地址寄存器,其值作为基地址。
  • (rs1): 表示以 rs1 寄存器的值为基地址进行寻址,通常简称为 rs1 地址。
  • offset: 表示以源寄存器 rs1 的值为基地址的偏移量。offset 是一个12位有符号数,其取值范围为 [-2048 ~ 2047]

5. 存储指令格式说明

RISC-V的存储指令(Store Instruction)格式如下所示,它定义了如何将寄存器中的数据写入内存。

5.1 指令汇编语法示例

s{d|w|h|b} rs2, offset(rs1)

5.2 指令位字段图

位范围 31-25 24-20 19-15 14-12 11-7 6-0
字段 imm[11:5] rs2 rs1 funct3 imm[4:0] opcode
位数 7 5 5 3 5 7
说明 offset[11:5] src base width offset[4:0] STORE

5.3 符号和字段解释

  • {d|w|h|b}: 表示存储的数据宽度。
    • d: 双字 (Double-word)
    • w: 字 (Word)
    • h: 半字 (Half-word)
    • b: 字节 (Byte)
  • rs1: 表示源地址寄存器,其值作为基地址。
  • (rs1): 表示以 rs1 寄存器的值为基地址进行寻址,简称 rs1 地址。
  • rs2: 源寄存器2,其值将被存储到内存中。
  • offset: 表示以源寄存器 rs1 的值为基地址的偏移量。offset 是一个12位有符号数,取值范围为 [-2048 ~ 2047]

示例:

.section .text
.global main
main:
	li a0, 0x80200000
	sd a0, (a0)
	li a1, 16
	lw t0, (a0)
	ld t1, (a0)
	lui t0, 0x8034f
	lui t1, 0x400
	nop

在这里插入图片描述

  • 首先查看该地址处的值,通过x指令可看到0x80200000地址处8个字节为0x00000000 80200000
  • lw t0, (a0) 加载了4个字节,由于最高位为1,会将最高位扩展至64位,t0中的值为0xffffffff 80200000
  • ld t1, (a0) 加载了两个字,结果为 0x80200000
  • lui t0, 0x8034f 将0x8034f左移12位,最高位为1,扩展符号后结果为0xffffffff8034f000
  • lui t1, 0x400 将0x400左移12位,最高位为0,扩展符号后结果为0x00000000400000

6. PC相对寻址

6.1 PC(程序计数器)

PC: 程序计数器 (Program Counter, PC) 用来指示下一条指令的地址。

6.2 AUIPC指令

RISC-V指令集提供了1条PC相对寻址的指令: AUIPC

指令格式: auipc rd, imm

操作说明:

  • imm立即数左移12位并符号扩展到64位后得到一个新的立即数
  • 这个新的立即数是一个有符号的立即数
  • 然后加上当前PC值,存储到rd寄存器中

寻址范围: 这条指令能寻址的范围为基于当前PC偏移±2GB

计算公式: rd = PC + Signed (imm << 12)

AUIPC指令寻址范围示意图:

PC-2GB ←—————————————— PC ——————————————→ PC+2GB
  • 32位有符号数的范围即相对PC的寻址范围为±2GB,并且跳转的offset为4096的整数倍

6.3 LUI指令

LUI指令将一个立即数左移12位后得到一个32位立即数,并且最高位符号扩展到64位,LUI指令的imm为20位,范围为0~0xFFFFF

指令格式: lui rd, imm

6.4 示例

示例1:AUIPC和LUI指令执行

问题:假设当前PC值为0x80200000,分别执行如下指令,那么a5和a6寄存器的值是多少?

auipc a5, 0x2
lui   a6, 0x2

解答

  • a5寄存器的值PC + sign_extend(0x2 << 12) = 0x80200000 + 0x2000 = 0x80202000
  • a6寄存器的值0x2 << 12 = 0x2000

分析

  • auipc指令将立即数左移12位并符号扩展后与PC相加
  • lui指令将立即数左移12位后加载到寄存器的高20位
示例2:使用AUIPC进行PC相对寻址加载数据

问题:假设当前PC值为0x80200000,想加载0x80202008地址的数据,那用auipc指令怎么写?

背景知识

  • AUIPC指令通常和ADDI联合使用来实现32位PC相对寻址
  • AUIPC指令可以寻址与被访问地址4KB对齐的地方,即被访问地址的高20位部分
  • ADDI指令可以在[-2048 ~ 2047]范围内寻址,即被访问地址的低12位部分

解决方案1:使用AUIPC和ADDI

auipc a5, 2
addi a0, a5, 8
ld t0, (a0)

计算过程

  • auipc a5, 2a5 = PC + (2 << 12) = 0x80200000 + 0x2000 = 0x80202000
  • addi a0, a5, 8a0 = 0x80202000 + 8 = 0x80202008
  • ld t0, (a0):从地址0x80202008加载数据到t0

解决方案2:使用AUIPC和立即偏移

auipc a5, 2
ld t0, 8(a5)

计算过程

  • auipc a5, 2a5 = 0x80202000
  • ld t0, 8(a5):从地址a5 + 8 = 0x80202008加载数据到t0

6.5 计算高20位和低12位

如果知道了当前PC值和目标地址B,那如何计算AUIPC和ADDI指令的参数呢?

下图展示了PC、目标地址B以及中间计算值的关系:

PC <-------------- Offset -------------->
|--------------------------------------------------------------------|
|       ...  |               |          |               |     ...    |
|--------------------------------------------------------------------|
              <---- 4KB ---> A <-lo12-> B <---- 4KB ---> 

关键定义:

  • B: 目标地址 (Target Address)
  • offset: 地址B与当前PC值的偏移量。即 offset = B - PC
  • A: 地址B与4KB对齐的地方。在计算中,A 可以理解为 PC + (hi20 << 12)
  • lo12: 地址A与地址B的偏移量,即 lo12 = B - A。这是一个有符号的12位数值,取值范围为 [-2048 ~ 2047]

计算步骤:
为了计算AUIPC指令的 hi20 立即数和ADDI指令的 lo12 立即数,我们可以按照以下步骤进行:

  1. 计算总偏移量 offset:
    offset = B - PC

  2. 计算AUIPC的 hi20 立即数:
    hi20 = (offset + 0x800) >> 12

    • 这里的 0x800 (即2048) 是为了进行四舍五入,确保 lo12 落在 [-2048, 2047] 的范围内。
  3. 计算ADDI的 lo12 立即数:
    lo12 = offset - (hi20 << 12)

    • 这个 lo12 值就是 B - (PC + (hi20 << 12)),它表示从 PC + (hi20 << 12) (即图中的 A 点) 到目标地址 B 的精确偏移。

这个计算方法对于理解链接重定位非常重要。

6.6 与PC相关的加载和存储伪指令

在编写汇编代码时,当前PC值是未知的,计算hi20lo12的过程通常留给链接器在重定位时完成。RISC-V定义了几种常用的伪指令,这些伪指令都是基于AUIPC指令的。

表 3.4 与PC相关的加载和存储伪指令
伪指令 指令组合 说明
la rd, symbol (非PIC) auipc rd, delta[31:12] + delta[11]
addi rd, rd, delta[11:0]
加载符号的绝对地址。这里delta = symbol - pc
la rd, symbol (PIC) auipc rd, delta[31:12] + delta[11]
l{w/d} rd, rd, delta[11:0]
加载符号的绝对地址。这里delta = GOT[symbol] - pc,表示从全局偏移表加载
lla rd, symbol auipc rd, delta[31:12] + delta[11]
addi rd, rd, delta[11:0]
加载符号的本地地址。这里delta = symbol - pc
l{b|h|w|d} rd, symbol auipc rd, delta[31:12] + delta[11]
l{b|h|w|d} rd, delta[11:0](rd)
将符号的内容加载到寄存器rd
s{b|h|w|d} rd, symbol, rt auipc rt, delta[31:12] + delta[11]
s{b|h|w|d} rd, delta[11:0](rt)
将寄存器rd的内容存储到符号中。这里rt是用于地址计算的临时寄存器
li rd, imm 根据情况扩展为多条指令 将立即数imm加载到寄存器rd
伪指令说明

1. 地址加载伪指令

  • la (Load Address): 加载符号的地址到寄存器
    • 非PIC版本: 直接计算符号地址,适用于静态链接
    • PIC版本: 通过全局偏移表(GOT)加载,适用于位置无关代码

2. 本地地址加载伪指令

  • lla (Load Local Address): 加载本地符号的地址,总是使用PC相对寻址

3. 内容加载/存储伪指令

  • l{b|h|w|d}: 加载字节/半字/字/双字内容
  • s{b|h|w|d}: 存储字节/半字/字/双字内容
  • 这些伪指令自动处理地址计算,简化了编程

4. 立即数加载伪指令

  • li (Load Immediate): 根据立即数大小自动选择最优的指令组合
使用示例
# 加载符号地址
la t0, global_var        # 加载全局变量地址到t0

# 加载符号内容
lw t1, global_var       # 加载全局变量的值到t1

# 存储到符号
sw t2, global_var       # 将t2的值存储到全局变量

# 加载立即数
li t3, 0x12345678       # 加载32位立即数到t3

7. 非对齐访问

在RISC-V中没有强制约束不对齐访问,让CPU设计者做出选择:

  1. 由硬件实现非对齐访问。
  2. 遇到非对齐访问时触发异常由软件处理。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐