ARM64 条件码基础:NZCV 标志位与条件执行
各位好,欢迎来到《ARM64 条件码分支控制与性能调优与调试实战》的第一章。我是你们的讲师,一个在 ARM 架构上摸爬滚打多年的工程师。
咱们今天聊点最基础,但也最核心的东西——条件码。说白了,就是 CPU 内部那四个标志位:N、Z、C、V。你别看它只有四个 bit,整个分支控制、循环跳转、甚至异常处理的逻辑,都靠它们撑着。
我刚开始做 ARM64 开发时,总觉得条件码这东西太简单,不就是比较一下嘛。结果有一次在调试一个死循环时,发现 C 标志位被错误地修改了,查了整整两天。嗯,从那以后,我再也不敢小看这四位了。
1.1 NZCV 标志位详解
ARM64 的 PSTATE 寄存器里,有一个叫 NZCV 的域。它由四个独立的标志位组成,每个标志位都有明确的含义。
| 标志位 | 全称 | 含义 | 典型置位条件 |
|---|---|---|---|
| N | Negative | 运算结果为负数 | 结果的最高位(bit 63)为 1 |
| Z | Zero | 运算结果为零 | 结果的所有位均为 0 |
| C | Carry | 进位/借位/无符号溢出 | 加法产生进位,减法产生借位 |
| V | Overflow | 有符号溢出 | 正+正=负,或负+负=正 |
关键点:N 和 V 用于有符号数比较,C 和 Z 用于无符号数比较。千万别搞混了,否则条件判断会完全错乱。
举个例子,假设我们执行 ADDS X0, X1, X2(带标志位的加法):
- 如果 X1 + X2 的结果为 0,则 Z=1。
- 如果结果最高位为 1(即负数),则 N=1。
- 如果加法产生了向更高位的进位,则 C=1。
- 如果两个正数相加得到负数,或两个负数相加得到正数,则 V=1。
这里有个坑,我提醒一下:不是所有指令都会更新 NZCV。像 ADD(不带 S 后缀)就不会改标志位。只有带 S 后缀的指令(如 ADDS、SUBS)或比较指令(CMP、CMN)才会更新。这一点在性能调优时特别重要——你想想看,如果你不小心用错了指令,标志位没更新,后面的条件跳转就会完全跑偏。
1.2 条件码表:16 种条件助记符
ARM64 定义了 16 种条件码,对应 4 位二进制编码(0000 到 1111)。每种条件码都基于 NZCV 的某种组合来判断是否满足条件。
| 条件码 | 助记符 | 含义(有符号) | 含义(无符号) | 标志位条件 |
|---|---|---|---|---|
| 0000 | EQ | 等于 | 等于 | Z == 1 |
| 0001 | NE | 不等于 | 不等于 | Z == 0 |
| 0010 | CS/HS | — | 高于或相同 | C == 1 |
| 0011 | CC/LO | — | 低于 | C == 0 |
| 0100 | MI | 负数 | — | N == 1 |
| 0101 | PL | 正数或零 | — | N == 0 |
| 0110 | VS | 溢出 | — | V == 1 |
| 0111 | VC | 无溢出 | — | V == 0 |
| 1000 | HI | — | 高于 | C == 1 && Z == 0 |
| 1001 | LS | — | 低于或相同 | C == 0 || Z == 1 |
| 1010 | GE | 大于或等于 | — | N == V |
| 1011 | LT | 小于 | — | N != V |
| 1100 | GT | 大于 | — | Z == 0 && N == V |
| 1101 | LE | 小于或等于 | — | Z == 1 || N != V |
| 1110 | AL | 无条件 | 无条件 | 始终为真 |
| 1111 | NV | 从不执行 | 从不执行 | 始终为假 |
个人习惯:我一般把条件码分成三组来记:
- 简单组:EQ/NE(看 Z),MI/PL(看 N),VS/VC(看 V),CS/CC(看 C)。
- 无符号比较组:HI/LS(看 C 和 Z 的组合)。
- 有符号比较组:GE/LT/GT/LE(看 N 和 V 的关系)。
这样记起来快,写代码时也不容易出错。
这里有个细节值得注意:CS 和 HS 是同一个条件码,只是别名不同。CS 是 Carry Set 的缩写,HS 是 Higher or Same 的缩写。同理,CC 和 LO 也是同一个。ARM 汇编器都支持这两种写法,我个人习惯用 CS/CC,因为更贴近标志位本身的含义。
1.3 条件码在指令中的位置与编码格式
ARM64 的指令编码是固定 32 位的。条件码通常出现在指令的高 4 位(bit 31:28),但并不是所有指令都支持条件执行。这一点和 ARM32 有很大区别——ARM32 几乎所有指令都可以加条件后缀,但 ARM64 只有分支指令(B.cond)和少数特殊指令才支持。
来看一个典型的分支指令编码格式:
31 28 27 25 24 23 5 4 0
+--------+--------+--------+--------+--------+
| cond | 001 | 1010 | imm19 | 00000 |
+--------+--------+--------+--------+--------+
- bit 31:28:条件码(4 位),例如 EQ=0000,NE=0001,GE=1010。
- bit 27:25:固定的 001,标识这是 B.cond 指令。
- bit 24:固定的 1010,进一步区分指令类型。
- bit 23:5:19 位有符号立即数,表示跳转偏移量(以 4 字节为单位)。
- bit 4:0:固定的 00000。
举个例子,B.EQ label 这条指令,如果当前 PC 到 label 的偏移是 0x100(256 字节),那么 imm19 就是 256/4 = 64 = 0x40。编码后的 32 位指令就是:
cond=0000, 001, 1010, imm19=0000000000001000000, 00000
= 0b 0000 0011 0100 0000 0000 0001 0000 0000
= 0x34000040
我曾经踩过的坑:在手动构造指令编码时,忘记把偏移量除以 4。ARM64 的 PC 相对偏移是以指令(4 字节)为单位的,不是以字节为单位。如果你直接填字节偏移,跳转目标会完全错误。调试时看到 PC 跳到一个奇怪地址,查了半天才发现是这个低级错误。
1.4 知识体系总览
为了让大家对本章内容有个整体认识,我画了一张结构图。它把 NZCV 标志位、条件码表、指令编码这三块内容串在了一起。
这张图把本章的三个核心知识点串在了一起。从左到右看:NZCV 标志位是基础,16 种条件码是判断规则,指令编码格式是硬件实现。三者缺一不可。
1.5 实战小技巧:如何快速判断条件码
最后,分享一个我自己的小技巧。在调试时,我经常需要快速判断某个条件码是否满足。我的方法是:
- 先看 Z 标志:如果 Z=1,EQ 和 LS 满足;如果 Z=0,NE 和 HI 满足。
- 再看 C 标志:如果 C=1,CS/HS 和 HI 满足;如果 C=0,CC/LO 和 LS 满足。
- 最后看 N 和 V 的关系:如果 N==V,GE 和 GT 满足;如果 N!=V,LT 和 LE 满足。
这个顺序是我在实际项目中总结出来的。先判断简单的,再判断复杂的,不容易出错。
避坑指南:我曾经在写一个无符号数组边界检查时,用了 GE 条件码来判断索引是否越界。结果数组长度是 0xFFFFFFFF,索引是 0,GE 判断为真(因为 N==V),但实际上索引并没有越界。后来才发现,无符号比较应该用 HS/LO,而不是 GE/LT。这个教训让我记住了:有符号和无符号的条件码绝对不能混用。