05 · 条件码、分支、循环与 switch
来源:第 5 讲 pp.9~64;小班第 5 讲;教材 §3.6;2025 阶段卷第 10~14 题。
四个主要条件码#
CF:最高位的进位或减法借位,用于无符号判断。ZF:结果为零。SF:结果最高位为 1。OF:有符号溢出。PF 表示低字节中 1 的个数为偶数,浮点比较还会用它表达无序结果。
8 位加法:0x7F+1=0x80,CF=0、OF=1、SF=1、ZF=0;0xFF+1=0,CF=1、OF=0、SF=0、ZF=1。CF 不能替代 OF,SF 也不能单独判断带溢出的有符号大小。
cmp b,a 计算 a-b 的标志而不保存差值;test b,a 计算 a&b 的标志而不保存结果。test x,x 判断零/负;test mask,x 检查特定位。
条件选择表#
下表假设前一条设置标志的是 cmp b,a,要判断 a 与 b。后缀可用于 jcc、setcc、cmovcc(具体操作宽度限制不同)。
| 含义 | 后缀 | 标志条件 |
|---|---|---|
| 等于 | e / z | ZF |
| 不等于 | ne / nz | !ZF |
| 有符号小于 | l / nge | SF xor OF |
| 有符号小于等于 | le / ng | (SF xor OF) or ZF |
| 有符号大于 | g / nle | !(SF xor OF) and !ZF |
| 有符号大于等于 | ge / nl | !(SF xor OF) |
| 无符号小于 | b / c / nae | CF |
| 无符号小于等于 | be / na | CF or ZF |
| 无符号大于 | a / nbe | !CF and !ZF |
| 无符号大于等于 | ae / nb / nc | !CF |
| 负 / 非负 | s / ns | SF / !SF |
| 溢出 / 未溢出 | o / no | OF / !OF |
为什么 l 是 SF xor OF?若 a-b 不溢出,差值符号就是真实大小关系;若溢出,截断后的符号翻转,OF=1 正好把 SF 反过来。例如 8 位 -128-1 得到 127,SF=0、OF=1,但 -128<1 仍为真。
set、jump 与 cmov#
setle %al 只写一个字节,要返回完整 int 布尔值常接 movzbl %al,%eax。不能把 setle %rax 当作合法的 64 位 set 指令。
条件跳转根据标志修改控制流;无条件跳转 jmp label 是直接跳转,jmp *%rax 或 jmp *table(,%rdi,8) 是间接跳转。相对跳转目标是下一条指令地址 + 带符号位移。例如 0x100: eb fe 长 2 字节,目标 0x102-2=0x100。
cmov 根据条件把源值复制到目标寄存器;目标不能是内存,没有普通 8 位 cmov 形式。它通常先计算候选值,再选择结果,减少错误分支预测带来的代价。代价是候选表达式都可能执行:昂贵函数、不安全解引用、可见副作用不能随便提前执行。内存源 cmov 不保证在条件不满足时避免访存异常。
# 返回 x<=y 的布尔值,x、y 是 long
cmpq %rsi, %rdi
setle %al
movzbl %al, %eax
ret
读分支时检查最后一次修改标志的指令,不能无视中间的 add/sub/test;mov 和 lea 则通常保留原标志。
if 与条件表达式#
先画控制流图:比较块 → 真分支/假分支 → 汇合。编译器可能让真分支顺序执行、假分支跳转,也可能相反;不要按标签位置猜真假。
if (test) A else B
↓
if (!test) goto else_part;
A; goto done;
else_part: B;
done:
条件传送示例:先算 r=x-y、t=y-x,再以 x>y 选择 t;得到的是 x<=y ? x-y : y-x,是非正的差值,不能只看函数名 absdiff 就认定为绝对值。2025 阶段卷中的同名函数正是一个提醒。
三种循环与边界#
- do-while:先执行 Body,再 Test,至少执行一次。
- while 跳到中间版本:入口先跳 Test,条件成立转 Body,Body 后回 Test。
- while guarded-do 版本:入口先否定条件检查,失败直接结束;成功后采用 do-while。
- for:Init → Test → Body → Update → Test;
continue应进入 Update,不能把它错误地直连 Test。
还原汇编时依次找初始化、循环体、测试、更新、出口、返回值。循环不一定有单独的“索引 i”,可能通过指针推进或移位掩码控制。
/* unsigned 版本明确位级行为;限制 1<=n<=63 才保证推进 */
unsigned long pick(unsigned long x, unsigned n) {
unsigned long result = 0;
for (unsigned long mask=1; mask!=0; mask<<=n)
result |= x & mask;
return result;
}
n=0 会使 mask 不变化;n≥位宽的 C 移位不合法,硬件低位掩码也不等于修复了源代码。Popcount 可逐位累加 (x&1) 再右移,也可反复 x&=x-1;x86 的 POPCNT 是专门指令,但编译器是否采用取决于目标 ISA、选项和识别能力。
switch 与跳转表#
稠密 case 常用跳转表,稀疏 case 可能采用比较树。流程通常为:索引归一化 → 范围检查 → 间接跳转 → 各 case 代码。
subq $3, %rdi # 原 case 从 3 开始
cmpq $4, %rdi
ja .Ldefault # 无符号检查同时排除原值<3 或 >7
jmp *.Ltable(,%rdi,8) # 每项是 8 字节代码地址
表中的第 0 项对应原值 3,不一定对应 case 0。多个 case 可共享同一地址;缺失值可指向 default;fall-through 表现为执行一个代码块后直接接另一个块,不意味着两个标签必须相同。
位置无关代码可能用 4 字节相对偏移表:先读有符号表项,再加表基址得到目标,因此“所有跳转表表项必为 8 字节”错误。一定看 .quad/.long 和加载指令。
调试时 x/8xg 地址 可显示 8 个 8 字节十六进制单元;这是显示内存表项,不自动执行跳转。反汇编区分机器码、表中数据与真实指令边界。
自检#
给任意 cmp 顺序,能选择正确的 signed/unsigned 条件;能解释 set 后为什么需要零扩展;能给出不适合 cmov 的具体例子;能从循环还原初值、终止条件和更新;能处理 switch 的默认分支、重复标签和穿透。教材作业 3.60、3.63。