3. RISC-V基础指令集(RV32I):寄存器模型、算术运算指令、逻辑运算指令、移位指令、比较指令

好,咱们今天聊聊RV32I的核心指令集。这部分是RISC-V的基石,说白了,你后面写的所有复杂程序,最终都会拆解成这些基础操作。我刚开始接触RISC-V时,第一感觉就是——干净。没有历史包袱,每条指令都设计得恰到好处。

3.1 寄存器模型:32个通用寄存器

RV32I定义了32个通用寄存器,每个都是32位宽。编号从x0到x31。嗯,这里有个特殊的存在——x0,它永远返回0。你往里面写啥都没用,读出来永远是0。这个设计很巧妙,我当年做MIPS时也有类似设计,但RISC-V把它用得更彻底。

寄存器约定速记表

寄存器ABI名称用途描述
x0zero硬连线0,写入无效
x1ra返回地址
x2sp栈指针
x3gp全局指针
x4tp线程指针
x5-x7t0-t2临时寄存器
x8s0/fp保存寄存器/帧指针
x9s1保存寄存器
x10-x17a0-a7函数参数/返回值
x18-x27s2-s11保存寄存器
x28-x31t3-t6临时寄存器

我个人习惯把x0叫做"黑洞寄存器"。为什么?因为数据进去就没了,但读取时永远给你0。这个特性在做清零操作时特别方便。比如你想把某个寄存器清0,直接 add x5, x0, x0 就行。

我的小技巧:写汇编时,尽量多用临时寄存器(t0-t6)做中间计算,把保存寄存器(s0-s11)留给需要跨函数调用的数据。这样能减少栈操作,提升性能。

3.2 算术运算指令

算术指令分为两类:立即数型和寄存器型。咱们先看加法。

// 寄存器型加法
ADD rd, rs1, rs2      // rd = rs1 + rs2

// 立即数型加法
ADDI rd, rs1, imm12   // rd = rs1 + 符号扩展的12位立即数

减法只有寄存器型:SUB rd, rs1, rs2,结果是 rd = rs1 - rs2。为什么没有SUBI?因为ADDI配合负数就能实现减法功能。你想想看,ADDI rd, rs1, -5 不就是减5吗?

乘法和除法不在RV32I里,它们在M扩展中。但RV32I提供了乘法的高位结果获取指令——MULH、MULHU、MULHSU。不过这些属于可选扩展,咱们先不展开。

注意溢出问题:RISC-V的算术指令不会主动检测溢出。我曾经在项目中遇到过一个问题:两个大数相加,结果超过了32位范围,但硬件没有报错,只是悄悄截断了高位。所以做安全关键系统时,一定要自己检查溢出。

3.3 逻辑运算指令

逻辑运算包括与、或、异或、取反。同样分为立即数型和寄存器型。

// 与运算
AND rd, rs1, rs2      // rd = rs1 & rs2
ANDI rd, rs1, imm12   // rd = rs1 & 符号扩展的imm12

// 或运算
OR rd, rs1, rs2       // rd = rs1 | rs2
ORI rd, rs1, imm12    // rd = rs1 | 符号扩展的imm12

// 异或运算
XOR rd, rs1, rs2      // rd = rs1 ^ rs2
XORI rd, rs1, imm12   // rd = rs1 ^ 符号扩展的imm12

取反操作呢?没有专门的NOT指令。但你可以用 XORI rd, rs1, -1 来实现。因为-1的二进制全是1,异或1就是取反。这个技巧我在做位操作时经常用。

我记得有一次调试一个通信协议解析模块,需要快速提取某些位域。用ANDI配合移位,几行代码就搞定了。如果换成C语言,编译器可能生成多条指令,但手写汇编可以精确控制每条指令的用途。

3.4 移位指令

移位指令有三种:逻辑左移、逻辑右移、算术右移。每种都有立即数和寄存器两种形式。

// 逻辑左移
SLLI rd, rs1, shamt5  // rd = rs1 << shamt5
SLL rd, rs1, rs2      // rd = rs1 << rs2[4:0]

// 逻辑右移
SRLI rd, rs1, shamt5  // rd = rs1 >> shamt5 (高位补0)
SRL rd, rs1, rs2      // rd = rs1 >> rs2[4:0] (高位补0)

// 算术右移
SRAI rd, rs1, shamt5  // rd = rs1 >> shamt5 (高位补符号位)
SRA rd, rs1, rs2      // rd = rs1 >> rs2[4:0] (高位补符号位)

这里有个细节:移位量只取低5位。为什么?因为32位寄存器,移位超过31位就没意义了。这个设计减少了硬件开销,也避免了奇怪的边界情况。

算术右移 vs 逻辑右移:算术右移保留符号位,适合有符号数除法(向下取整)。逻辑右移补0,适合无符号数。我见过有人用逻辑右移处理有符号负数,结果符号位丢失,数据全乱了。切记:有符号数用SRA,无符号数用SRL。

3.5 比较指令

比较指令设置目标寄存器为0或1。RV32I提供了四种比较:小于(有符号)、小于(无符号)、等于、不等于(通过组合实现)。

// 有符号小于
SLT rd, rs1, rs2      // rd = (rs1 < rs2) ? 1 : 0 (有符号比较)
SLTI rd, rs1, imm12   // rd = (rs1 < imm12) ? 1 : 0

// 无符号小于
SLTU rd, rs1, rs2     // rd = (rs1 < rs2) ? 1 : 0 (无符号比较)
SLTIU rd, rs1, imm12  // rd = (rs1 < imm12) ? 1 : 0

等于和不等于是怎么实现的?用XOR配合SLT。比如判断两个数是否相等:XOR rd, rs1, rs2,如果结果为0则相等。然后可以用 SLTIU rd, rd, 1 把结果转为0/1。

为什么没有直接的BEQ、BNE?其实有的,它们属于分支指令,咱们后面会讲。这里比较指令的结果是0或1,可以用于条件赋值、循环控制等场景。

避坑指南:我曾经在写一个排序算法时,用SLT比较两个无符号数,结果负数被当作很小的数处理了。后来才发现SLT是有符号比较,应该用SLTU。这个bug查了我整整一个下午。所以,比较前一定要想清楚数据类型是有符号还是无符号。

3.6 知识体系总览

下面这张图展示了RV32I基础指令集的核心结构。你可以看到,所有操作都围绕32个通用寄存器展开,指令格式统一,操作类型清晰。

RV32I基础指令集知识体系 32个通用寄存器 (x0-x31) 算术运算 ADD, ADDI, SUB LUI, AUIPC 逻辑运算 AND, ANDI, OR, ORI XOR, XORI 移位运算 SLL, SLLI, SRL SRLI, SRA, SRAI 比较运算 SLT, SLTI, SLTU SLTIU 指令格式统一:R型、I型、U型 所有指令长度固定为32位,操作码(OPCODE)决定指令类别 寄存器操作数位置固定:rs1, rs2, rd 在指令中的位置不变

这张图把RV32I的基础指令分成了四大类。你注意看,所有指令都围绕寄存器操作,没有直接访问内存的指令(除了加载存储,那是下一章的内容)。这种设计让指令解码变得非常简单,硬件实现也更容易。

好了,这一章的内容就到这里。记住:寄存器模型是基础,算术逻辑移位比较是工具。把这些指令练熟了,后面写复杂程序时才能得心应手。