条件码设置指令:CMP、CMN、TST 与条件码的隐式/显式设置
各位好,欢迎来到第四章。这一章我们聊聊条件码的设置。说实话,很多工程师写 ARM64 汇编时,条件分支写得飞起,但一问到条件码到底是怎么来的,就有点含糊了。我个人觉得,这块如果不吃透,后面做性能调优很容易踩坑。
ARM64 里,条件码(NZCV)不是凭空产生的。它要么由专门的指令设置,要么由算术逻辑指令隐式附带。咱们今天就把这两条路都走一遍。
4.1 CMP 比较指令
CMP 是使用频率最高的条件码设置指令。它的本质是什么?说白了,就是一次不保存结果的减法。
CMP Xn, Xm // 计算 Xn - Xm,更新 NZCV,丢弃结果
CMP Xn, #imm // 计算 Xn - imm,更新 NZCV,丢弃结果
我在项目中见过有人用 SUBS 然后丢弃结果来实现比较,虽然功能一样,但代码可读性差很多。CMP 就是专门干这个的,别绕弯子。
核心理解:CMP 等价于 SUBS XZR, Xn, Xm。它只影响标志位,不写寄存器。
举个例子,判断两个数是否相等:
LDR X0, =value1
LDR X1, =value2
CMP X0, X1
B.EQ equal_label // 如果 X0 == X1,跳转
这里 CMP 执行后,如果 X0 和 X1 相等,Z 标志位被置 1。B.EQ 检查的就是 Z=1。
4.2 CMN 负比较指令
CMN 是 CMP 的镜像操作。它计算的是 Xn + Xm,然后更新标志位。你可能会问,为什么需要这个?
嗯,这里有个实际场景。比如你要判断一个数是不是负数,用 CMN 配合 #0 就能做到:
CMN X0, #0 // 计算 X0 + 0,实际上就是判断 X0 是否为 0
// 但更常见的用法是 CMN X0, X1 判断 X0 + X1 是否为负
我个人习惯是:当需要检查两个数相加的结果时,用 CMN 比用 ADDS 再丢弃结果更清晰。我曾经在调试一个网络协议栈时,看到前辈用 CMN 做边界检查,当时觉得挺巧妙,后来自己也用上了。
| 指令 | 操作 | 典型用途 |
|---|---|---|
| CMP | Xn - Xm | 大小比较、相等判断 |
| CMN | Xn + Xm | 负数检测、补码比较 |
4.3 TST 位测试指令
TST 是位测试的利器。它执行按位与操作,同样不保存结果,只更新标志位。
TST Xn, Xm // 计算 Xn & Xm,更新 NZCV
TST Xn, #imm // 计算 Xn & imm,更新 NZCV
为什么需要 TST?你想想看,如果我想知道一个寄存器的第 3 位是不是 1,用 AND 然后检查结果当然可以,但 TST 更直接:
TST X0, #0x8 // 测试第 3 位
B.NE bit_set // 如果第 3 位为 1,跳转
这里 TST 执行后,如果第 3 位为 1,结果非零,Z 标志位清零,B.NE 触发跳转。
小技巧:TST 配合 B.EQ/B.NE 是做位掩码检查的标准模式。我在做设备驱动时,经常用这个组合检查中断状态寄存器的特定位。
4.4 条件码的隐式设置与显式设置
这块是很多人的知识盲区。ARM64 里,条件码的设置有两种方式:
- 隐式设置:算术逻辑指令加
S后缀,比如 ADDS、SUBS、ANDS。这些指令在计算结果的同时,顺手更新了 NZCV。 - 显式设置:专门的条件码设置指令,比如 CMP、CMN、TST。它们只更新标志位,不写目标寄存器。
我见过不少新手在循环里用 ADD 而不是 ADDS,然后发现条件分支总是不对。为什么?因为 ADD 不更新标志位!
避坑指南:我曾经调试过一个死循环,查了半天发现是用了 ADD 做循环计数,但后面跟的 B.LT 永远检查不到正确的标志位。记住:不加 S 后缀的算术指令不更新 NZCV。
来看一个对比:
// 错误写法:ADD 不更新标志位
MOV X0, #10
loop:
ADD X0, X0, #-1 // X0 减 1,但 NZCV 不变
CBNZ X0, loop // 这里用 CBNZ 没问题,但如果用 B.GT 就错了
// 正确写法:SUBS 更新标志位
MOV X0, #10
loop:
SUBS X0, X0, #1 // X0 减 1,同时更新 NZCV
B.GT loop // 当 X0 > 0 时继续循环
这里有个重要的设计哲学:ARM64 把「计算」和「比较」解耦了。你想算数就用 ADD/SUB,想比较就用 CMP/TST。只有当你明确需要同时做这两件事时,才用带 S 后缀的指令。
4.5 本章知识结构图
下面这张图把条件码设置的两种路径和对应的指令串起来了,方便你对照记忆:
4.6 实战建议
最后,我根据多年经验给你几条实在的建议:
- 纯比较用 CMP/TST:不要用 SUBS/ANDS 替代,代码意图更明确,编译器也更容易优化。
- 循环计数用 SUBS:需要同时减一和检查标志位时,SUBS 一步到位。
- 位测试用 TST:检查特定位时,TST 比 AND 加 CMP 少一条指令。
- 注意 CMN 的语义:它做的是加法比较,别和 CMP 搞混了。
一句话总结:CMP 做减法比较,CMN 做加法比较,TST 做位与测试。它们只改标志位,不改寄存器。需要同时算数和改标志位时,用带 S 后缀的指令。