4. RV32I基础整数指令集(上):算术运算、逻辑运算与移位指令

各位同学,今天我们正式进入RV32I指令集的核心地带。说实话,这部分内容就像CPU的“呼吸”和“心跳”——没有它们,处理器就是个死物。我个人习惯把RV32I的指令分成几大类来理解,今天我们先啃下最基础的三块:算术运算、逻辑运算和移位指令。

4.1 算术运算指令:ADD、SUB、ADDI

先看加法。RISC-V的加法指令非常纯粹,就是ADD rd, rs1, rs2,把rs1和rs2的值相加,结果存到rd。没有标志位,没有条件码。嗯,这里要注意:x86和ARM的加法会改状态寄存器,但RISC-V不干这事。为什么?为了简化硬件设计,减少指令间的隐式依赖。

我在项目中遇到过一个问题:有同事从ARM转过来做RISC-V,习惯性地想检查加法溢出标志位,结果发现根本没有。他当时就懵了。其实RISC-V的哲学是——溢出检测你自己用软件做,别让硬件背锅。

核心要点:ADD和SUB都是寄存器-寄存器操作,不涉及内存。操作数宽度固定为32位,结果也只保留低32位。

减法指令SUB rd, rs1, rs2,其实就是rs1 - rs2。硬件实现上,它和ADD共享一套加法器,只是把rs2取反再加1。说白了,减法就是加法的“变装”。

再来看立即数加法ADDI rd, rs1, immediate。这个指令太常用了,加载地址、调整栈指针、循环计数……几乎每个程序里都有它。立即数是12位有符号数,范围-2048到2047。你想想看,如果立即数不够用怎么办?RISC-V的解决方案是:用LUI加载高20位,再用ADDI补低12位。这是RISC-V的经典组合拳。

指令 格式 功能 我的一点经验
ADD ADD rd, rs1, rs2 rd = rs1 + rs2 最常用的寄存器加法
SUB SUB rd, rs1, rs2 rd = rs1 - rs2 注意操作数顺序,别搞反了
ADDI ADDI rd, rs1, imm rd = rs1 + 符号扩展(imm) 立即数范围有限,大常数需分两步

避坑指南:我曾经在写启动代码时,用ADDI给栈指针赋值,结果立即数不够大,栈指针指向了错误地址。后来我养成了习惯:大常数一定用LUI+ADDI组合,或者直接用LI伪指令。

4.2 逻辑运算指令:AND、OR、XOR

逻辑运算指令,说白了就是按位操作。AND用于清零某些位,OR用于置位,XOR用于翻转。这三个指令都有对应的立即数版本:ANDI、ORI、XORI。

我记得有一次调试外设驱动,需要修改某个控制寄存器的特定位。用ANDI清零,再用ORI置位,两行代码搞定。如果换成其他架构,可能还要读-改-写三步走。RISC-V的简洁在这里体现得很充分。

# 示例:将x5寄存器的bit3清零,bit1置位
andi x5, x5, ~(1 << 3)   # 清零bit3
ori  x5, x5, (1 << 1)    # 置位bit1

这里有个细节:ANDI、ORI、XORI的立即数也是12位有符号数。但逻辑运算通常只需要低12位,所以符号扩展不会影响结果——除非你操作的是最高位。嗯,这种情况很少见,但如果你遇到了,记得用LUI先加载高20位。

个人技巧:我习惯用XORI来快速翻转寄存器的某些位,比如切换GPIO的电平状态。一条指令,干净利落。

4.3 移位指令:SLL、SRL、SRA

移位指令分三种:逻辑左移(SLL)、逻辑右移(SRL)、算术右移(SRA)。移位量由rs2的低5位决定,范围0-31。为什么是5位?因为32位寄存器只需要5位就能表示所有移位量。

你想想看,如果移位量超过31会怎样?RISC-V的规定是:只取低5位,高位忽略。也就是说,SLL x1, x2, x3等价于左移x3[4:0]位。这和x86的SHL指令行为不同,x86会检查移位量是否超过操作数宽度。我曾经在移植代码时踩过这个坑——x86上移位32位会清零,但RISC-V上移位32位等于不移位(因为32的低5位是0)。

关键区别:

  • SLL:左移,低位补0。相当于乘以2的n次方。
  • SRL:逻辑右移,高位补0。相当于无符号数除以2的n次方。
  • SRA:算术右移,高位补符号位。相当于有符号数除以2的n次方(向负无穷取整)。
# 示例:假设x5 = 0x80000001(最高位为1)
slli x6, x5, 1    # x6 = 0x00000002,左移1位,低位补0
srli x7, x5, 1    # x7 = 0x40000000,逻辑右移,高位补0
srai x8, x5, 1    # x8 = 0xC0000000,算术右移,高位补符号位1

这里要注意:SLLI、SRLI、SRAI是立即数版本的移位指令,移位量直接编码在指令中,范围0-31。我个人习惯在编译优化时,把乘除法替换成移位指令——如果除数是2的幂,用SRAI代替除法,性能提升非常明显。

避坑指南:我曾经在写除法算法时,用SRLI处理无符号数,用SRAI处理有符号数。结果有一次把有符号负数当无符号数右移了,高位补0,导致结果完全错误。记住:有符号数右移一定要用SRAI,无符号数用SRLI。

4.4 知识体系总览

为了让你更直观地理解这三类指令的关系,我画了一张图。你可以看到,算术运算、逻辑运算、移位指令构成了RV32I指令集的“三驾马车”,它们各自独立又相互配合。

RV32I基础整数指令集(上) 算术运算指令 ADD rd, rs1, rs2 SUB rd, rs1, rs2 ADDI rd, rs1, imm 特点:无标志位 立即数12位有符号 溢出由软件处理 逻辑运算指令 AND rd, rs1, rs2 OR rd, rs1, rs2 XOR rd, rs1, rs2 按位操作 有立即数版本 常用于位域操作 移位指令 SLL rd, rs1, rs2 SRL rd, rs1, rs2 SRA rd, rs1, rs2 移位量取低5位 有立即数版本 SRA保留符号位 三类指令均使用R-type或I-type格式,操作数宽度固定为32位

这张图把今天讲的内容串起来了。你可以看到,算术运算、逻辑运算、移位指令各自独立,但都遵循RISC-V的简洁设计哲学——没有冗余的标志位,没有隐式的副作用。每条指令只做一件事,而且做得干净利落。

好了,今天的内容就到这里。记住:ADD/SUB/ADDI是算术运算的基石,AND/OR/XOR是位操作的利器,SLL/SRL/SRA是移位和乘除法的加速器。这三类指令加起来不到20条,但你已经能写出大部分基础程序了。下次我们继续聊比较指令和分支指令,那才是真正让程序“活”起来的东西。


公众号:蓝海资料掘金营,微信deep3321