到目前为止我们的程序都是一条道走到黑——顺序执行。真实的程序充满岔路:if 判断、while 循环、出错重试。这一讲的六条分支指令,就是让 CPU 学会「看情况办事」的全部工具。
六条分支指令
分支 = 比较两个寄存器,成立就跳到标签处,不成立继续往下走:
| 指令 | 语义 | 记法 |
|---|---|---|
beq rs1, rs2, label |
相等则跳 | == |
bne rs1, rs2, label |
不等则跳 | != |
blt rs1, rs2, label |
有符号小于 | < |
bge rs1, rs2, label |
有符号大于等于 | >= |
bltu / bgeu |
无符号版 | <、>= |
注意三个细节:
- 没有 bltu 的「大于」「小于等于」版——交换两个操作数就是它:
a > b等价于blt b, a。六条指令不多一条。 - 有符号(blt/bge)和无符号(bltu/bgeu)是两条不同指令。第 02 讲 slt/sltu 的分野在这里同样适用:拿无符号指令去比「看起来是负数」的值,会得到完全相反的结论。
- 没有无符号相等?不需要——相等判断不分符号。
RISC-V 没有 jmp
乍看指令表你会找不到无条件跳转 jmp。真相是:跳转早已藏在 jal 里——jal zero, label(跳过去,返回地址丢进 x0 = 不需要返回)。因为它太常用,汇编器给了个伪指令别名 j label。伪指令的完整家族第 07 讲盘点。
if 的汇编形状
C 代码:
if (a0 == 0) { a1 = 1; }
汇编(注意判断条件常常要反过来写——不成立就跳过 if 体):
bne a0, zero, skip # a0 != 0 就跳过
addi a1, zero, 1 # if 体
skip:
循环的两个模板
while 型(先判断再干活):
loop:
beq t0, t1, done # 条件不满足就退出
# ... 循环体 ...
j loop
done:
for 型(计数循环:1 加到 100):
addi t0, zero, 1 # i = 1
addi t1, zero, 0 # sum = 0
for:
bgt t0, t2, done # i > 100 退出(bgt 是 bge 换位的伪指令)
add t1, t1, t0 # sum += i
addi t0, t0, 1 # i++
j for
done:
跑完 t1 = 5050。高斯小时候口算的题,CPU 做了 100 次循环——但它只需 3 纳秒。
跳转目标其实是「偏移量」
汇编里写标签很直观,但机器码里标签会被换算成相对当前 PC 的偏移,编码进指令的立即数字段(12 位,±4KB 范围,且必须是 2 的倍数)。所以:
- 分支只能在小范围内跳(跨太远要借寄存器中转,编译器比你急)
- 第 08 讲拆指令编码时,你会看到这个立即数在 32 位里排列顺序是打乱的——那是为了硬件复用,先知道有这回事
动手验证
把「1 加到 100」的代码敲进模拟器,单步执行,观察 t0 递增、t1 累积。然后做个小挑战:改成「只加偶数」(提示:andi t3, t0, 1 判断奇偶——第 06 讲正好讲位操作)。
免费下载 HI RISC-V App,关卡判题帮你验证对错。
本讲三句话
- 六条分支指令
beq/bne/blt/bge/bltu/bgeu,比较两个寄存器决定跳不跳 - 无条件跳转是伪指令
j,本质是jal zero, label - 分支立即数 = 相对 PC 的偏移,范围 ±4KB——循环体别写太大
下一讲是重头戏:函数调用。ra、栈帧、调用约定——汇编的成人礼。