跳到主要内容

基础整数指令集

RISC-V 基础整数指令集是所有 RISC-V 处理器必须实现的核心指令集。RV32I 是 32 位版本,RV64I 是 64 位版本。本章将详细介绍这些指令的格式和用法。

指令格式概述

RISC-V 指令采用规整的格式设计,基础指令集使用 6 种指令格式:

R 型格式 - 寄存器操作

R 型指令用于寄存器之间的操作,如加减运算、逻辑运算等。

|31    25|24  20|19  15|14    12|11   7|6      0|
| funct7 | rs2 | rs1 | funct3 | rd | opcode |
  • opcode:操作码,标识指令类型
  • rd:目标寄存器
  • funct3:功能码,区分同一类型的不同操作
  • rs1:源寄存器 1
  • rs2:源寄存器 2
  • funct7:扩展功能码

I 型格式 - 立即数操作

I 型指令用于寄存器与立即数的操作,如立即数加法、加载操作等。

|31          20|19  15|14    12|11   7|6      0|
| imm[11:0] | rs1 | funct3 | rd | opcode |
  • imm[11:0]:12 位立即数(有符号扩展)

S 型格式 - 存储操作

S 型指令用于将寄存器值存储到内存。

|31    25|24  20|19  15|14    12|11   7|6      0|
|imm[11:5]| rs2 | rs1 | funct3 |imm[4:0]| opcode |

立即数被分割为两部分存储。

B 型格式 - 分支操作

B 型指令用于条件分支跳转。

|31    25|24  20|19  15|14    12|11   7|6      0|
|imm[12|10:5]| rs2 | rs1 | funct3 |imm[4:1|11]| opcode |

立即数表示相对于当前 pc 的偏移量,以 2 字节为单位。

U 型格式 - 长立即数

U 型指令用于加载 20 位高位立即数。

|31                  12|11   7|6      0|
| imm[31:12] | rd | opcode |

J 型格式 - 跳转操作

J 型指令用于无条件跳转。

|31                12|11   7|6      0|
|imm[20|10:1|11|19:12]| rd | opcode |

立即数表示相对于当前 pc 的偏移量。

算术运算指令

加减运算

ADD - 加法

add rd, rs1, rs2    # rd = rs1 + rs2

将 rs1 和 rs2 的值相加,结果存入 rd。

add a0, a1, a2      # a0 = a1 + a2

ADDI - 立即数加法

addi rd, rs1, imm   # rd = rs1 + imm

将 rs1 与有符号立即数相加。这是最常用的指令之一。

addi sp, sp, -16    # 分配 16 字节栈空间
addi a0, a0, 1 # a0 自增 1
li a0, 100 # 伪指令,展开为 addi a0, x0, 100

SUB - 减法

sub rd, rs1, rs2    # rd = rs1 - rs2

将 rs1 减去 rs2,结果存入 rd。

sub a0, a1, a2      # a0 = a1 - a2
sub a0, a0, a1 # a0 = a0 - a1

移位运算

SLL - 逻辑左移

sll rd, rs1, rs2    # rd = rs1 << rs2[4:0]

将 rs1 左移 rs2 低 5 位指定的位数,低位补 0。

slli rd, rs1, imm   # rd = rs1 << imm(立即数版本)

左移 n 位相当于乘以 2^n。

slli a0, a0, 3      # a0 = a0 * 8

SRL - 逻辑右移

srl rd, rs1, rs2    # rd = rs1 >> rs2[4:0](高位补 0)
srli rd, rs1, imm # rd = rs1 >> imm

逻辑右移高位补 0,用于无符号数除法。

SRA - 算术右移

sra rd, rs1, rs2    # rd = rs1 >> rs2[4:0](高位符号扩展)
srai rd, rs1, imm # rd = rs1 >> imm(算术右移)

算术右移高位用符号位填充,用于有符号数除法。

srai a0, a0, 2      # a0 = a0 / 4(有符号除法)

比较运算

SLT - 小于比较(有符号)

slt rd, rs1, rs2    # rd = (rs1 < rs2) ? 1 : 0(有符号比较)
slti rd, rs1, imm # rd = (rs1 < imm) ? 1 : 0

比较有符号整数,结果为 0 或 1。

slt  a0, a1, a2     # 如果 a1 < a2(有符号),a0 = 1
slti a0, a1, 10 # 如果 a1 < 10,a0 = 1

SLTU - 小于比较(无符号)

sltu rd, rs1, rs2   # rd = (rs1 < rs2) ? 1 : 0(无符号比较)
sltiu rd, rs1, imm # rd = (rs1 < imm) ? 1 : 0(无符号)

比较无符号整数。一个常用技巧是检测值是否为 0:

sltiu a0, a1, 1     # a0 = (a1 == 0) ? 1 : 0

逻辑运算指令

按位逻辑运算

AND - 按位与

and rd, rs1, rs2    # rd = rs1 & rs2
andi rd, rs1, imm # rd = rs1 & imm

用于提取特定位或清零特定位:

andi a0, a0, 0xFF   # 提取低 8 位

OR - 按位或

or rd, rs1, rs2     # rd = rs1 | rs2
ori rd, rs1, imm # rd = rs1 | imm

用于设置特定位:

ori a0, a0, 0x80    # 设置第 7 位

XOR - 按位异或

xor rd, rs1, rs2    # rd = rs1 ^ rs2
xori rd, rs1, imm # rd = rs1 ^ imm

用于翻转特定位:

xori a0, a0, 0xFF   # 翻转低 8 位

常用伪指令

RISC-V 定义了许多伪指令,它们在汇编时展开为真实指令:

mv   rd, rs         # 移动寄存器,展开为 addi rd, rs, 0
not rd, rs # 按位取反,展开为 xori rd, rs, -1
neg rd, rs # 取负,展开为 sub rd, x0, rs
seqz rd, rs # 等于零,展开为 sltiu rd, rs, 1
snez rd, rs # 不等于零,展开为 sltu rd, x0, rs
li rd, imm # 加载立即数,展开为 lui + addi

加载与存储指令

RISC-V 采用加载-存储架构,所有内存访问必须通过加载和存储指令完成。

加载指令

LB/LBU - 加载字节

lb  rd, offset(rs1)   # rd = SignExt(Mem[rs1 + offset][7:0])
lbu rd, offset(rs1) # rd = ZeroExt(Mem[rs1 + offset][7:0])

lb 进行符号扩展,lbu 进行零扩展。

lb  a0, 0(a1)         # 从 a1 指向的地址加载字节,符号扩展
lbu a0, 0(a1) # 零扩展加载

LH/LHU - 加载半字

lh  rd, offset(rs1)   # 加载 16 位半字,符号扩展
lhu rd, offset(rs1) # 加载 16 位半字,零扩展

LW - 加载字

lw rd, offset(rs1)    # 加载 32 位字
lw a0, 0(sp)          # 从栈顶加载一个字
lw a0, 12(a1) # 从 a1+12 地址加载一个字

存储指令

SB - 存储字节

sb rs2, offset(rs1)   # Mem[rs1 + offset][7:0] = rs2[7:0]

只存储最低 8 位。

sb a0, 0(a1)          # 将 a0 的低字节存储到 a1 指向的地址

SH - 存储半字

sh rs2, offset(rs1)   # 存储 16 位半字

SW - 存储字

sw rs2, offset(rs1)   # 存储 32 位字
sw a0, 0(sp)          # 将 a0 存储到栈
sw a0, 8(a1) # 将 a0 存储到 a1+8 地址

内存访问示例

# 数组元素访问
# 假设 a1 是数组基地址,a2 是索引,每个元素 4 字节
slli a3, a2, 2 # a3 = a2 * 4(计算偏移)
add a3, a1, a3 # a3 = 数组元素地址
lw a0, 0(a3) # 加载元素到 a0

# 结构体成员访问
# 假设 a1 是结构体指针,成员偏移为 8
lw a0, 8(a1) # 加载偏移为 8 的成员

分支指令

分支指令根据条件改变程序执行流程。

条件分支

BEQ - 相等分支

beq rs1, rs2, label   # 如果 rs1 == rs2,跳转到 label

BNE - 不相等分支

bne rs1, rs2, label   # 如果 rs1 != rs2,跳转到 label

BLT - 小于分支(有符号)

blt rs1, rs2, label   # 如果 rs1 < rs2(有符号),跳转

BGE - 大于等于分支(有符号)

bge rs1, rs2, label   # 如果 rs1 >= rs2(有符号),跳转

BLTU/BGEU - 无符号比较分支

bltu rs1, rs2, label  # 如果 rs1 < rs2(无符号),跳转
bgeu rs1, rs2, label # 如果 rs1 >= rs2(无符号),跳转

分支示例

if-else 结构

// C 代码
if (a == b) {
x = 1;
} else {
x = 2;
}
# RISC-V 汇编
bne a0, a1, else # 如果 a != b,跳转到 else
li a2, 1 # x = 1
j end # 跳过 else 分支
else:
li a2, 2 # x = 2
end:

循环结构

// C 代码
for (int i = 0; i < 10; i++) {
sum += i;
}
# RISC-V 汇编
li a0, 0 # sum = 0
li a1, 0 # i = 0
li a2, 10 # 循环上限
loop:
bge a1, a2, end # 如果 i >= 10,退出循环
add a0, a0, a1 # sum += i
addi a1, a1, 1 # i++
j loop
end:

跳转指令

无条件跳转

JAL - 跳转并链接

jal rd, label     # rd = pc + 4; pc = label

保存返回地址并跳转。通常用于函数调用。

jal ra, function  # 调用函数,返回地址存入 ra

JALR - 间接跳转

jalr rd, offset(rs1)   # rd = pc + 4; pc = rs1 + offset

用于返回或间接调用:

jalr x0, 0(ra)    # 返回(伪指令 ret)
jalr ra, 0(a0) # 间接调用,目标地址在 a0 中

常用伪指令

j   label         # 无条件跳转,展开为 jal x0, label
jr rs # 跳转到寄存器地址,展开为 jalr x0, 0(rs)
ret # 函数返回,展开为 jalr x0, 0(ra)
call function # 函数调用,展开为 auipc + jalr

高位立即数指令

LUI - 加载高位立即数

lui rd, imm       # rd = imm << 12

将 20 位立即数左移 12 位后加载到寄存器。用于加载 32 位常数的高 20 位。

# 加载 0x12345000
lui a0, 0x12345 # a0 = 0x12345000

AUIPC - 加载高位 pc 相对地址

auipc rd, imm     # rd = pc + (imm << 12)

计算 pc 相对地址,常与 jalr 配合实现长距离跳转。

# 调用远距离函数
auipc ra, %pcrel_hi(function)
jalr ra, %pcrel_lo(function)(ra)

加载立即数

加载 32 位立即数通常需要两条指令:

# 加载 0x12345678
lui a0, 0x12345 # a0 = 0x12345000
addi a0, a0, 0x678 # a0 = 0x12345678

汇编器提供的 li 伪指令会自动选择最优方式:

li a0, 0x12345678       # 自动展开为 lui + addi
li a0, 100 # 展开为 addi a0, x0, 100
li a0, 0x1000 # 展开为 lui a0, 0x1

RV64I 扩展指令

RV64I 在 RV32I 基础上增加了处理 64 位数据的指令。

字操作指令

ADDIW/SUBIW - 32 位立即数加减

addiw rd, rs1, imm   # rd = SignExt((rs1 + imm)[31:0])

将 32 位结果符号扩展到 64 位。

SLLIW/SRLIW/SRAIW - 32 位移位

slliw rd, rs1, imm   # rd = SignExt((rs1 << imm)[31:0])
srliw rd, rs1, imm # rd = SignExt((rs1 >> imm)[31:0])
sraiw rd, rs1, imm # rd = SignExt((rs1 >>> imm)[31:0])

双字操作指令

LD/SD - 加载/存储双字

ld rd, offset(rs1)   # 加载 64 位双字
sd rs2, offset(rs1) # 存储 64 位双字

ADDW/SUBW/SLLW/SRLW/SRAW - 32 位寄存器操作

addw rd, rs1, rs2    # rd = SignExt((rs1 + rs2)[31:0])
subw rd, rs1, rs2 # rd = SignExt((rs1 - rs2)[31:0])
sllw rd, rs1, rs2 # rd = SignExt((rs1 << rs2)[31:0])

这些指令在 64 位处理器上处理 32 位数据时非常有用。

指令编码示例

理解指令编码有助于调试和深入理解处理器工作原理。

ADD 指令编码

add a0, a1, a2
# 展开为:add x10, x11, x12

R 型格式:
funct7 = 0x00 (0000000)
rs2 = 12 (01100)
rs1 = 11 (01011)
funct3 = 0 (000)
rd = 10 (01010)
opcode = 0x33 (0110011)

二进制:0000000 01100 01011 000 01010 0110011
十六进制:0x00C58533

ADDI 指令编码

addi a0, a1, 100

I 型格式:
imm = 100 (00001100100)
rs1 = 11 (01011)
funct3 = 0 (000)
rd = 10 (01010)
opcode = 0x13 (0010011)

二进制:00001100100 01011 000 01010 0010011
十六进制:0x06458513

小结

本章介绍了 RISC-V 基础整数指令集的核心内容:

  • 6 种指令格式:R、I、S、B、U、J
  • 算术运算:ADD、SUB、ADDI、移位、比较
  • 逻辑运算:AND、OR、XOR
  • 内存访问:LB、LH、LW、SB、SH、SW
  • 控制流:条件分支(BEQ、BNE、BLT、BGE)和跳转(JAL、JALR)
  • 高位立即数:LUI、AUIPC
  • RV64I 扩展指令

掌握这些基础指令后,就可以编写简单的 RISC-V 汇编程序了。下一章将介绍标准扩展指令集。