基础整数指令集
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 汇编程序了。下一章将介绍标准扩展指令集。