条件码设置指令: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 本章知识结构图

下面这张图把条件码设置的两种路径和对应的指令串起来了,方便你对照记忆:

条件码设置指令体系 条件码 NZCV 设置 隐式设置(S 后缀指令) 显式设置(专用指令) ADDS SUBS ANDS CMP CMN TST 关键区别 • 隐式设置:计算 + 更新标志位,适合循环计数、累加后判断 • 显式设置:只更新标志位,不写寄存器,适合纯比较场景 • 性能提示:显式设置指令通常比带 S 后缀的指令更清晰 • 避坑:ADD/SUB 不加 S 不更新 NZCV,条件分支会失效

4.6 实战建议

最后,我根据多年经验给你几条实在的建议:

  1. 纯比较用 CMP/TST:不要用 SUBS/ANDS 替代,代码意图更明确,编译器也更容易优化。
  2. 循环计数用 SUBS:需要同时减一和检查标志位时,SUBS 一步到位。
  3. 位测试用 TST:检查特定位时,TST 比 AND 加 CMP 少一条指令。
  4. 注意 CMN 的语义:它做的是加法比较,别和 CMP 搞混了。

一句话总结:CMP 做减法比较,CMN 做加法比较,TST 做位与测试。它们只改标志位,不改寄存器。需要同时算数和改标志位时,用带 S 后缀的指令。