ZJZJ / ICS NOTES
整本阅读
COMPUTER SYSTEMS · PEKING UNIVERSITY

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 阶段卷中的同名函数正是一个提醒。

三种循环与边界#

还原汇编时依次找初始化、循环体、测试、更新、出口、返回值。循环不一定有单独的“索引 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。