上一讲我们认识了寄存器工作台,也写了第一条 addi。这一讲把算术做完:加减法的规则、溢出的真相,以及 RISC-V 最「离经叛道」的设计——没有 flags 寄存器,那比较怎么办?

加与减:三条指令的事

指令 形式 语义
add add rd, rs1, rs2 rd = rs1 + rs2
addi addi rd, rs1, imm rd = rs1 + imm(±2048)
sub sub rd, rs1, rs2 rd = rs1 − rs2

三个要点:

  • 没有 subi。减常数 = 加负数:addi t0, t0, -1。指令少一条,译码电路省一套。
  • 立即数永远挂在 addi 上,寄存器加寄存器才用 add。
  • sub 只有寄存器形式——想减立即数,老老实实 addi 负值。

溢出:不报错,环绕

32 位有符号数的范围是 -2147483648 到 2147483647。加过头了会怎样?RISC-V 的选择是安静地环绕,不报错、不设标志:

addi t0, zero, -1           # t0 = 0xFFFFFFFF = -1
addi t1, zero, 1
add   t2, t0, t1            # t2 = 0,溢出位被自然丢掉

这不是偷懒。环绕语义(补码运算天然成立)让编译器生成的代码更少、流水线更简单;真需要检测溢出的场合,由编译器用高位比较显式实现。对比 x86 的 into 溢出陷阱指令——RISC-V 连这种「保险丝」都不留。

没有 flags,比较怎么办

x86 做完 cmp 后,结果悄悄存进 flags 寄存器,下一条 jg/je 去读它。RISC-V 把这套全扔了:没有 flags,比较结果直接写进寄存器——

指令 形式 语义
slt slt rd, rs1, rs2 rs1 < rs2(有符号)?rd=1 : rd=0
slti slti rd, rs1, imm 同上,比立即数
sltu / sltiu 同上 无符号比较

slt = set less than。想判断 a0 < 10:

slti t0, a0, 10        # t0 = (a0 < 10) ? 1 : 0

得到 0/1 后,配合第 04 讲的分支指令就能做出决定。这个设计的好处马上显现:

  1. 乱序执行不再被 flags 拖后腿——flags 是全局共享状态,一条比较指令会污染后面所有分支;把结果写进普通寄存器,就没有这个依赖。
  2. 比较结果可以当普通值用:当 0/1 参与算术、存进内存、当数组下标,都天经地义。

两个经典小例子

绝对值(有符号):

    sub  t0, zero, a0     # t0 = 0 - a0
    slti t1, a0, 0        # a0 < 0 ?
    beq  t1, zero, done   # 非负就跳过(下一讲细讲分支)
    mv   a0, t0           # 负数取反
done:

求最大值:

    slt  t0, a0, a1       # a0 < a1 ?
    add  t2, zero, a0
    beq  t0, zero, done   # a0 >= a1,a0 就是大的
    add  t2, zero, a1     # 否则取 a1
done:

乘除法去哪了

细心的你会发现 RV32I 只有加减。乘除法在 M 扩展里(mul/div/rem 等 8 条)——大多数教学场景用不到,App 的进阶关卡里会见到它们。这也是「模块化指令集」的思路:地基越小越好,能力按需拼装。

动手验证

在模拟器里试着让一个寄存器溢出:从 0x7FFFFFFF(最大正数)开始 addi 1,看看它如何变成 0x80000000(最小负数)。再玩玩 sltu:用无符号视角比较 -1 和 1,结果是反直觉的——0xFFFFFFFF 无符号下是 42 亿,比 1 大得多。

免费下载 HI RISC-V App,亲手试一遍。

本讲三句话

  1. add/addi/sub 三条打天下;没有 subi,减常数就是加负数
  2. 溢出安静环绕,不报错不设标志——补码运算的天然结果
  3. 没有 flags:slt 系列把比较结果写进寄存器,又快又自由

下一讲离开寄存器,去内存里拿数据——load 与 store 登场。