Skip to content

B 指令与条件后缀:让 CPU 学会做选择

分支与条件后缀封面

上一课的 CMP 是侦察兵——减一下、留好证据(标志位),然后退场。这一课,决策者终于登场:B 指令和它的条件后缀家族。有了它们,程序不再是从第一行跑到最后一行的直线,而是第一次拥有了"岔路"。

1. 从直线到岔路:什么是分支

到目前为止,我们写的所有程序都是"顺序流":PC(程序计数器)自顾自地每次 +4,一条接一条往下执行,谁也不拦它。而分支指令做的事情只有一件——按条件决定 PC 的下一个值,而不是无脑 +4。

图 1 · 顺序流与分支流对比:没有分支时 PC 无脑 +4;有了分支,CMP 出题、BEQ 判卷,程序走向岔路之一

图 1 · 顺序流 vs 分支流:程序第一次有了岔路

分支需要两个新角色配合:

  • 标签(label):cond1:cond2: 这样以冒号结尾的行,本身不占指令,只是给某个地址起了个名字
  • 跳转:b cond2 表示"下一条指令去 cond2 那里取"——汇编器会把标签换算成地址差,编进指令里
asm
    b cond2         @ 无条件跳到 cond2
cond1:
    mov r2, #1      @ 标签只是地址的名字,不产生指令
cond2:
    mov r3, #2

2. 无条件分支 B:直接改写 PC

BBranch,分支)是最朴素的一条:不问任何条件,直接跳。它本质上就是把 PC 改成目标标签的地址——流水线里的下一条指令,从标签处重新开始取。

注: 在反汇编工具(如 Ghidra)里,你可能会看到 BAL 这种写法。AL 是"总是"(ALways)的意思,BAL 和 B 是同一条指令的两种写法——"总是跳"就是"无条件跳"。

3. 条件后缀:BEQ 看 Z 行事

B 的真正威力在于后缀。把两个字母的条件码直接拼在 B 后面,它就长出了判断力:

  • beq:B + EQ(equal,相等)——Z=1 时才跳
  • bne:B + NE(not equal,不等)——Z=0 时才跳
  • blt:B + LT(less than,小于)——按 N 和 V 的组合判断

回忆上一课:cmp r0, r1 之后,两数相等时 Z=1。于是"相等则跳"的完整拼图就是:

asm
    cmp r0, r1      @ 侦察兵:相等则 Z=1
    beq cond1       @ 决策者:Z=1 才跳去 cond1

这就是 ARM 世界里 if 语句的最小单元。CMP 布置标志,条件分支看标志行事——上一课埋的伏笔,在这一课全部兑现。

4. 完整实验:BEQ 不跳,B 兜底

看视频里的单步实验(GDB + GEF),R0=4、R1=5,两数不相等:

图 2 · 单步执行轨迹:cmp 后 Z=0,beq 不跳;顺序执行到 b cond2 无条件跳转,cond1 的 mov r2,#1 被跳过

图 2 · 单步实验:BEQ 不跳,B 兜底跳(R0=4, R1=5)

asm
    mov r0, #4
    mov r1, #5
    cmp r0, r1      @ 45,Z=0
    beq cond1       @ Z=0,不跳,继续往下
    b cond2         @ 无条件跳:直接着陆 cond2
cond1:
    mov r2, #1      @ 被跳过,R2 始终是 0
cond2:
    mov r3, #2      @ 跳转着陆点,R3 = 2

调试器里单步走完,R2=0、R3=2——cond1 分支被整个跳过。如果把 R0 也改成 5,Z=1,BEQ 就会跳进 cond1,R2=1。同一份代码,标志位不同,走的路就不同。

5. 顺序穿透:最烂的 if 语句

视频作者毫不客气地把刚才的代码称为"世界上最烂的 if 语句"。为什么?因为它有个暗伤:汇编里没有"块"的概念,标签挡不住顺序执行。

假如 R0=R1,BEQ 跳进 cond1,mov r2, #1 执行完——然后呢?PC 继续 +4,顺势滑进了 cond2,mov r3, #2 也执行了!两个分支都跑了一遍,else 形同虚设。

易错点: 高级语言的 if-else 自带"互斥",汇编里互斥要靠你自己用跳转搭出来。忘记加"防穿透"分支,是条件分支新手最经典的 bug。

正确的拼装需要两条"胶水分支":

图 4 · if-else 的正确拼装:beq 条件跳去 if 体,b 兜底跳去 else 体,if 体末尾还要一条 b end 防穿透

图 4 · if-else 的正确拼装:两条「胶水分支」缺一不可

asm
    cmp r0, r1
    beq cond1       @ 胶水① 条件跳:相等才去 if 体
    b cond2         @ 胶水② 兜底跳:不等就去 else 体
cond1:
    mov r2, #1      @ if 体
    b end           @ 胶水③ 防穿透:干完就走
cond2:
    mov r3, #2      @ else 体
end:

三条 B 家族指令各司其职:一条管"选谁",一条管"躲开谁",一条管"收队"。看懂这个三段式,你就看懂了所有 if-else 的汇编骨架。

6. 条件后缀大家族

EQ 和 NE 只是开始。B 后面可以拼一整族后缀,全部围绕 CPSR 标志设计:

图 3 · 条件后缀速查表:EQ 看 Z=1,NE 看 Z=0,GT/LT/GE/LE 看 N 与 V 的组合,AL 总是跳

图 3 · 条件后缀速查表:B 的七十二变

后缀含义何时跳转
EQ相等Z = 1
NE不等Z = 0
GT大于Z = 0 且 N = V
LT小于N ≠ V
GE大于等于N = V
LE小于等于Z = 1 或 N ≠ V
AL总是无条件(通常省略)

注: GT/LT/GE/LE 看的是 N 和 V 的组合而不只是 N,是为了正确处理有符号溢出——这正是 V 标志存在的意义。好消息是你不用背组合规则,记住"后缀语义 = 高级语言的比较符"就够了。

有了这一族后缀,if (a > b)while (i != 0)for 循环……高级语言里的所有控制流,都能拆成 CMP + 条件分支的组合。接下来的课程就会把 C 语言的 if 和循环逐个翻译成汇编,看看编译器到底干了什么。

小结

  • B 指令:无条件改写 PC,跳到标签处;标签只是地址的名字,不产生指令
  • 条件后缀:拼在 B 后面形成 BEQ、BNE、BLT……按 CPSR 标志决定是否跳
  • 组合拳:CMP 布置标志 + 条件分支看标志行事,这是所有判断的汇编原型
  • 顺序穿透:标签挡不住顺序执行,if 体末尾必须加 b end 防穿透
  • 后缀家族:EQ/NE/GT/LT/GE/LE/AL,语义对应高级语言的比较符;AL 即无条件 B 本身
  • 下集预告:把 C 语言的 if 和循环丢进编译器,逆向看它们变成什么样的 CMP + B 组合

到这里,CPU 终于学会了做选择。数据会搬了(MOV/LDR/STR),会算了(ADD/SUB/MUL),会比了(CMP),现在还会跳了(B 家族)——图灵机的最后一块拼图就位。下一课见。

本章自测