第2章:RISC-V基础指令集(RV32I)——寄存器模型、整数运算、逻辑运算与移位指令

好,咱们正式开始啃指令集这块硬骨头。RV32I是RISC-V最基础的指令集,说白了就是地基。你后面学到的乘除法、原子操作、浮点,全得在这上面盖楼。我个人习惯,学任何架构先看寄存器模型——寄存器就是CPU的“工作台”,指令就是“操作手册”。

2.1 寄存器模型:CPU的32个“临时工”

RV32I定义了32个通用寄存器,每个都是32位宽。编号从x0到x31。嗯,这里要注意,x0是个“铁公鸡”——你读它永远得到0,写它等于白写。我在项目中遇到过新手把目标寄存器写成x0,调试了半天发现数据丢了,其实就是被x0吞了。

寄存器虽然编号简单,但RISC-V规范给它们起了“别名”,方便阅读和编译。比如x1叫ra(返回地址),x2叫sp(栈指针)。你想想看,写汇编时用sp比用x2直观多了吧?

寄存器编号别名用途说明
x0zero硬连线为0,写入无效
x1ra返回地址(函数调用用)
x2sp栈指针
x3gp全局指针
x4tp线程指针
x5-x7t0-t2临时寄存器(调用者保存)
x8s0/fp保存寄存器/帧指针
x9s1保存寄存器
x10-x17a0-a7函数参数/返回值
x18-x27s2-s11保存寄存器(被调用者保存)
x28-x31t3-t6临时寄存器
我的小技巧: 写汇编时,临时寄存器(t0-t6)随便用,但函数调用前记得保存。保存寄存器(s0-s11)则相反——你用了就得负责恢复。我曾经因为忘记保存s0,导致上层函数崩溃,查了两天才找到bug。

2.2 整数运算指令:加减法,但不止加减法

整数运算指令分三类:加法、减法、以及“带立即数的加法”。指令格式很规整,都是opcode rd, rs1, rs2或者opcode rd, rs1, imm

先看加法:ADD rd, rs1, rs2。把rs1和rs2的值相加,结果存到rd。减法类似:SUB rd, rs1, rs2。注意,RISC-V没有“减法立即数”指令,你想减一个常数,得用ADDI rd, rs1, -常数。嗯,这里要记住,立即数是有符号的12位,范围-2048到2047。

举个例子:

# 计算 a = b + c
ADD x10, x11, x12    # x10 = x11 + x12

# 计算 a = b - 5
ADDI x10, x11, -5    # x10 = x11 - 5

# 计算 a = b + 100
ADDI x10, x11, 100   # x10 = x11 + 100
避坑指南: 我曾经在写地址计算时,用ADDI加一个大于2047的立即数,结果编译报错。后来才意识到,大常数需要先用LUI加载高20位,再用ADDI加低12位。这是RISC-V的“常数构造”套路,后面会细讲。

还有一条特殊的加法指令:LUI rd, imm。它把20位立即数加载到rd的高20位,低12位补0。配合ADDI,就能构造任意32位常数。比如加载0x12345678:

LUI x10, 0x12345      # x10 = 0x12345000
ADDI x10, x10, 0x678  # x10 = 0x12345678

2.3 逻辑运算指令:与、或、异或、取反

逻辑运算在底层很常用,比如位掩码、清零、置位。RV32I提供了AND、OR、XOR,以及它们的立即数版本ANDI、ORI、XORI。

指令格式和算术指令完全一样:

AND rd, rs1, rs2    # 按位与
OR  rd, rs1, rs2    # 按位或
XOR rd, rs1, rs2    # 按位异或

ANDI rd, rs1, imm   # 与立即数
ORI  rd, rs1, imm   # 或立即数
XORI rd, rs1, imm   # 异或立即数

你想想看,如果想清零某个寄存器的低8位,怎么做?用ANDI配合掩码0xFFFFFF00就行。如果想置位第3位,用ORI配合0x08。这些操作在控制GPIO、配置外设寄存器时天天用。

注意: 逻辑运算的立即数也是12位有符号数,但逻辑指令把它当作无符号位模式处理。比如ANDI x10, x11, -1,-1的12位表示是0xFFF,实际效果是保留低12位,高20位清零。我刚开始学的时候被这个坑过,以为ANDI -1是全保留,结果高20位丢了。

另外,RISC-V没有专门的“取反”指令。想取反,用XORI rd, rs1, -1。因为-1的二进制全是1,异或1就是取反。这个小技巧我在优化代码时经常用,比先加载常数再取反少一条指令。

2.4 移位指令:左移、右移(逻辑与算术)

移位指令分三种:逻辑左移(SLL)、逻辑右移(SRL)、算术右移(SRA)。每种都有立即数版本(SLLI、SRLI、SRAI)。

移位量由rs2的低5位(或立即数的低5位)决定,范围0-31。为什么是5位?因为32位寄存器只需要5位就能表示所有移位量。

SLL  rd, rs1, rs2    # 逻辑左移:左移,低位补0
SRL  rd, rs1, rs2    # 逻辑右移:右移,高位补0
SRA  rd, rs1, rs2    # 算术右移:右移,高位补符号位

SLLI rd, rs1, shamt  # 立即数左移
SRLI rd, rs1, shamt  # 立即数逻辑右移
SRAI rd, rs1, shamt  # 立即数算术右移

举个例子:

# 将x10左移4位(相当于乘以16)
SLLI x10, x10, 4

# 将x11逻辑右移8位(提取高24位)
SRLI x11, x11, 8

# 将x12算术右移2位(保持符号,相当于除以4并向下取整)
SRAI x12, x12, 2
我的经验: 算术右移和逻辑右移的区别,说白了就是高位补符号位还是补0。对有符号数做除法时,用算术右移;对无符号数做除法时,用逻辑右移。我曾经在写一个音频算法时,把有符号数用逻辑右移做了除法,结果负数全变成了正数,声音直接炸了。嗯,从那以后我每次用右移都会多看一眼符号。

移位指令在性能优化中很有用。比如你想快速乘以2的幂,用左移代替乘法;想提取位域,用左移+右移组合。我记得在优化一个CRC计算模块时,用移位指令代替了循环和分支,性能提升了3倍。

2.5 小结:RV32I运算指令的共性

总结一下,RV32I的整数运算、逻辑运算、移位指令有这些共同点:

  • 三操作数格式: rd, rs1, rs2(或立即数),没有隐式操作数
  • 立即数12位: 有符号,范围-2048到2047
  • 移位量5位: 范围0-31,由rs2低5位或立即数指定
  • 无状态位: RISC-V不设置标志位(不像x86的ZF、CF),条件判断靠比较指令
  • 无乘法除法: 乘除在RV32M扩展中,基础集只有加减和移位

我个人觉得,RISC-V这种规整的设计让硬件实现变得简单,也让编译器优化更轻松。你写汇编时,每条指令都清晰明了,没有隐藏的副作用。这种“干净”的设计哲学,正是RISC-V的魅力所在。

下一章我们会讲加载存储指令和条件分支,到时候这些运算指令就会派上大用场了。