← SEARCHABLE ARCHIVE
Assembly · 2026-08-15

彻底理解 x86-64 标志位与条件分支

CMP 与 TEST、有符号与无符号条件,以及无分支代码的取舍

INTERACTIVE READING MODE →

彻底理解 x86-64 标志位与条件分支

本博客文章以教学为目的。 示例采用 Intel 语法;性能必须在目标 CPU 与真实输入分布上测量。

cmp eax, edx 没有保存结果,后面的 jl 为何知道谁更小?同一组位 0xFFFFFFFF 为何有时是 -1,有时是 4,294,967,295?关键在 RFLAGS,以及消费标志时采用 signed 还是 unsigned 条件。

常见状态标志

标志意义
ZF结果为 0
CFunsigned carry/borrow
SF结果最高位
OFsigned 结果超出范围
PF结果低字节 parity

普通条件分支主要组合 ZF、CF、SF、OF。

cmp 与 test

cmp rax, rdx

概念上计算 rax-rdx,丢弃数值,只更新标志。

cmp eax, 10
je equal_ten
jl signed_less_than_ten

分支并不是语法上绑定最近的 cmp。中间若有会改标志的指令,分支就读取新的标志。

cmp eax, 10
add ecx, 1
jl somewhere      ; 读取 add 的标志

test 像丢弃结果的 and,常用于零值与 bit mask 检查。

test rax, rax
jz is_zero
test eax, 1
jnz is_odd

相等与大小关系

相等不分 signedness:je/jz 看 ZF=1,jne/jnz 看 ZF=0。大小关系则必须区分。

意义signedunsigned
left < rightjljb
left <= rightjlejbe
left > rightjgja
left >= rightjgejae
mov eax, 0FFFFFFFFh
cmp eax, 1
jl signed_less       ; -1 < 1,成立
jb unsigned_below    ; 4294967295 < 1,不成立

寄存器没有 signed 类型标签;差别在于如何解释同一组比较标志。

CF 与 OF 是不同溢出

8 位 0xFF+1=0x00 有 unsigned carry,但 signed 的 -1+1 正常。0x7F+1=0x80 对 unsigned 是 128,对 signed 则超出 127。

add al, 1
jc unsigned_overflow
jo signed_overflow

C/C++ unsigned 算术定义为 modulo;signed overflow 是未定义行为。CPU 提供 OF 并不会替语言定义它。

inc、dec 保留 CF,所以和 add 1、sub 1 并非所有标志都相同。

setcc 与 cmovcc

xor eax, eax
cmp edi, esi
setl al
ret

把 signed edi < esi 转成 0 或 1。setcc 只写一个字节,因此要明确清理高位。

cmp eax, edx
cmovl eax, edx

条件移动不分割控制流,但可能拉长 dependency chain,并不保证比 branch 快。

branchless 不等于自动更快

现代 CPU 会预测分支。高度可预测的 jcc 很便宜;输入不规则且两条路径都很短时,setcc、cmovcc、mask 可能有利。必须根据真实分支分布、misprediction、两侧工作量、增加的指令与依赖、编译器输出,在目标 CPU 测量。

循环与短路条件

xor eax, eax
xor edx, edx
loop_test:
    cmp rdx, rsi
    jae loop_done
    add eax, [rdi + rdx*4]
    inc rdx
    jmp loop_test
loop_done:
    ret

jae 是 unsigned i >= count。优化可能用 rotation、unrolling、vectorization 改变形状。

test rcx, rcx
jz done
cmp byte ptr [rcx + READY_OFFSET], 0
je done
call use
done:

这是 p != nullptr && p->ready 的可能形式。第一个分支避免 null dereference,顺序本身就是安全契约。

jump table

sub eax, 3
cmp eax, 4
ja default_case
jmp qword ptr [table + rax*8]

先正规化 case,再用 unsigned 范围检查限制 table index,最后间接跳转。范围检查也是重要的安全边界。

函数调用不保存普通条件标志

cmp eax, edx
call helper
jl old_result       ; helper 可能已改变标志

需要结果时,应在调用前转成 0/1,或调用后重新比较。

总结

cmp 与 test 不保存计算结果,只建立标志。相等看 ZF;大小必须区分 signed 的 SF/OF 与 unsigned 的 CF/ZF。不要只找最近的 cmp,而要追踪最后定义标志的指令、条件 signedness,以及每条控制边上的可能输入范围。

官方资料