上一讲我们认识了寄存器工作台,也写了第一条 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 讲的分支指令就能做出决定。这个设计的好处马上显现:
- 乱序执行不再被 flags 拖后腿——flags 是全局共享状态,一条比较指令会污染后面所有分支;把结果写进普通寄存器,就没有这个依赖。
- 比较结果可以当普通值用:当 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,亲手试一遍。
本讲三句话
add/addi/sub三条打天下;没有subi,减常数就是加负数- 溢出安静环绕,不报错不设标志——补码运算的天然结果
- 没有 flags:
slt系列把比较结果写进寄存器,又快又自由
下一讲离开寄存器,去内存里拿数据——load 与 store 登场。