从一条CMP指令讲起:减法背后的标志位逻辑
很多人刚开始写x86汇编时,都会觉得比较指令是最好糊弄过去的部分——无非就是CMP、TEST,后面跟个条件跳转,能跑就行。但真到了调试阶段,尤其是碰上一些边界值、符号位、溢出场景,你会发现“能跑”和“理解”之间隔着一条很宽的沟。我见过不少朋友在写字符串扫描、循环找最大值、自写memcmp这类代码时,被CMP之后那几个标志位搞到怀疑人生;甚至有人宁可多写几行低效代码,也要绕开比较指令,就是因为没搞懂JE、JGE、JA、JG之间的区别到底在哪。
这篇就专门把x86汇编的比较指令讲透。我会从CMP和TEST的底层逻辑讲起,再拆解标志位的推导规则,最后结合条件跳转和实际调试经验,把常见坑点一个个拉出来说清楚。内容覆盖面比较广,但不需要你有很深的汇编基础,只要知道什么是寄存器、什么是内存地址,跟着一步步看就能明白。
1. CMP指令:嘴上说比较,手上做减法
先记住一个重要结论:CMP指令的本质是减法,但它只修改标志位,不保存运算结果。
这听起来像一句废话,但很多问题的根源恰恰出在这里。CMP dst, src做的事情,就是拿目标操作数减去源操作数,结果被直接丢弃,运算过程中产生的状态信息——比如结果是不是0、是不是产生了借位、有没有溢出——全部记录在FLAGS寄存器里。后面的条件跳转指令再根据这些标志位决定要不要跳转。
举个例子:
asm复制mov eax, 10
mov ebx, 6
cmp eax, ebx
这条CMP实际执行的是 10 - 6 = 4。因为结果不是0,所以ZF(零标志位)为0;因为够减、没产生借位,所以CF(进位/借位标志)为0;因为结果是正数,所以SF(符号标志位)为0;因为两个正数相减得正数,没发生符号溢出,所以OF(溢出标志位)为0。此时eax和ebx的值都没变,你后续想用这两个数做其他计算,完全不受影响。
但如果你写的是:
asm复制mov eax, 5
mov ebx, 8
cmp eax, ebx
执行的是 5 - 8,在32位寄存器里实际得到的是 0xFFFFFFFD,也就是十进制的-3。此时ZF=0(结果非零),CF=1(不够减,需要借位),SF=1(结果的最高位是1,表示负数),OF=0(负数减正数不会溢出)。
所以说,CMP从硬件层面看就是一条减法指令,只不过它把减法的“答案”扔了,只保留了“状态”。这个设计非常巧妙,因为比较这个动作本身不需要保存差值,只需要知道差值的关系——等于、大于、小于、溢出——而这些关系全部能从标志位里推出来。这也是为什么CMP后面必须搭配条件跳转或者SETCC、CMOVCC这类指令才能发挥价值,单独一条CMP是看不出任何显式结果的。
我自己在实际调试中经常遇到的一种情况,是新手把CMP和SUB混淆。SUB不仅修改标志位,还会把减法结果写回目标寄存器:
asm复制sub eax, ebx ; eax = eax - ebx,eax的值被覆盖了
cmp eax, ebx ; 只比较,eax保持不变
如果你只是想做大小判断,用SUB会破坏后续逻辑;反过来,如果你需要用差值做进一步计算,用CMP就取不到差值。两种指令的取舍,本质上取决于你到底要不要这个减法结果。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 标志位推导明细:一次比较到底改变了什么
CMP指令设置了多个标志位,但真正参与大小判断的只有四个:ZF、CF、SF、OF。另外还有一个PF(奇偶标志位),对于比较来说基本用不上,可以忽略。我把它们整理成一张表,这样对照使用会清楚很多。
| 标志位 | 全称 | 置1的条件 | 类比说明 |
|---|---|---|---|
| ZF | Zero Flag | 减法结果为0 | 两个数相等 |
| CF | Carry Flag | 无符号减法发生借位 | 无符号中被减数小于减数 |
| SF | Sign Flag | 结果的最高位为1 | 结果被解释为负数 |
| OF | Overflow Flag | 有符号运算发生溢出 | 结果超出有符号数范围 |
先说ZF。这个最简单,两个操作数相等,相减结果就是0,ZF就是1;不相等,ZF就是0。所以判断相等,本质上就是检查“减法结果是不是0”。
CF这个标志位,看的是无符号视角下的借位。什么叫无符号视角?比如 cmp eax, ebx,如果 eax 里的数值比 ebx 小,按照无符号数来理解,eax - ebx 就不够减,要向更高位借1,CF就变成1。反过来说,CF=0说明无符号意义下eax大于等于ebx。这里特别容易混淆的一点是,
