选择语句的机器级表示
机器一级没有 if。它只有一个动作:改 PC。所以所有的选择语句最终都被翻译成同一个骨架——
“产生”和”消费”分属两条指令、中间靠 PSW 传递,这是 4.1.5 立下的结构,这一节把它用起来。
机制
Jcc 全表:有符号和无符号读的是不同的位
| 关系 | 有符号 | 无符号 | 有符号依据 | 无符号依据 |
|---|---|---|---|---|
je / jz | je / jz | ZF | ZF | |
jne / jnz | jne / jnz | ZF | ZF | |
jg | ja | ZF | CF | |
jge | jae | SF | CF | |
jl | jb | SF | CF | |
jle | jbe | ZF | CF |
助记符的词源就是记忆钥匙:
- 有符号用
g/l—— greater / less - 无符号用
a/b—— above / below
这张表是 2.1.3 那条”同一串二进制,按有符号和无符号读是两个数”在指令层的直接后果。 一条 cmp 之后,硬件已经把 CF、OF、ZF、SF 全部算好了——它不知道你的数据是有符号还是无符号,也不需要知道,因为你随后挑哪条 Jcc,就等于告诉了它。硬件不做选择,硬件提供全部选项。
if-else 的标准翻译:条件是取反的
if (x > y) { A } else { B } cmp ebx, ecx ; x - y,置标志位
jle .Lelse ; ★ 注意:源码写 >,机器码跳的是 <=
<A> ; then 分支,直接 fall through
jmp .Lend
.Lelse:
<B>
.Lend:
编译器把条件取反,是因为 then 分支要”顺流而下”(fall through)——顺序执行不需要指令,能省一条 jmp。所以:
读汇编时的第一反应:看到
jle .L2,源码里写的多半是>而不是<=。
这一条是读汇编真题最省时间的一个习惯。 若嵌套多层,一层层取反回去即可。
if 没有 else 时更简单,只是少了 jmp .Lend 和一个标号:
cmp ebx, ecx
jle .Lend
<A>
.Lend:
switch:两种实现,判据是分支的密度
(1)级联比较——分支少或 case 值稀疏时:
cmp eax, 1
je .L1
cmp eax, 2
je .L2
...
jmp .Ldefault
时间与分支数成正比,
(2)跳转表——分支多且 case 值密集时:
sub eax, 100 ; ★ 先减去最小 case 值,把范围平移到 0 开始
cmp eax, 5
ja .Ldefault ; ★ 无符号比较,一次同时挡住 <100 和 >105 两侧越界
jmp [.L4 + eax*4] ; ★ 查表跳转
.L4: .long .L100, .L101, .L102, .L103, .L104, .L105
时间与分支数无关,
sub平移:把case值域搬到 开始,表就能从下标 0 排起。ja用无符号比较挡两侧:若eax原本小于 100,减法之后会变成一个很大的无符号数,同样落在> 5里被挡掉——一次比较挡住上下两个方向的越界,这是无符号比较的经典用法。用jg(有符号)就漏掉下界了。jmp [.L4 + eax*4]用的正是 4.2.2 的变址寻址 + 存储器间接寻址:.L4是形式地址(表首址,常量),eax是变址寄存器(下标,变量),比例因子 4 是地址宽度。“变址寻址中形式地址是常量、寄存器是变量”那条判据,在这里落地了。
关联对照:跳转表把
switch变成"用数据换分支"跳转表是间接寻址那两个优点的完整体现(4.2.2):目标地址存在数据区、运行时才确定,指令本身一个字节不用改。
代价也很明确:多一次访存(读表项),而且这次访存的地址由数据决定,分支预测器很难猜准。 所以编译器不会无脑用跳转表——
case稀疏时(比如 1、100、10000)表会大到离谱,反而是级联比较划算。“密集用表、稀疏用比较”就是判据。
条件传送 cmov:把分支彻底消掉
x = (a > b) ? a : b; mov eax, ebx ; 先假设结果是 b
cmp ebx, ecx
cmovl eax, ecx ; 若 b < c,改成 c
没有任何转移指令,因此不存在分支预测失败。在 5.5 指令流水线(本地资料)里,一次预测失败要清空流水线、代价十几个周期,而 cmov 的代价是恒定的一两拍。
但它有一个硬限制:两个分支的值都必须被真的算出来。 所以下面这种绝对不能用 cmov 优化:
p = (ptr != NULL) ? *ptr : 0; // 用 cmov 会无条件解引用 ptr → 崩溃
n = (d != 0) ? x/d : 0; // 用 cmov 会无条件做除法 → 除零异常判据:分支体有副作用、可能出错、或开销很大时,不能用条件传送。 这条边界正是”控制冒险的软件解法”的适用范围,会在 5.5 再遇到。
边界
条件转移指令只读标志位,不改标志位。 所以连续几条 Jcc 可以依赖同一次 cmp 的结果——编译器翻译 a <= x && x <= b 时就是这么干的。
cmp 和 Jcc 之间可以插入指令,只要它不改相关标志位。 mov、lea、push、pop 都安全(4.3.1)。判断题若说”条件转移指令必须紧跟在比较指令之后”,错。
有符号和无符号用错助记符,是一类真实存在的 bug,也是判断题的常客。 对 cmp eax, ebx 之后用 jg 还是 ja,取决于这两个数在 C 里的类型。编译器根据类型自动选,人写汇编时选错就得到诡异的结果——尤其是负数被当成大正数。
cmp a, b 做的是 cmp eax, ebx 是 cmp %ebx, %eax 也是
jmp [.L4+eax*4] 是”取表项内容作为目标地址”(存储器间接),不是”跳到表项所在的地址”。 少解一层引用就跳飞了。方括号在 jmp 里表示访存,这和 lea 里的方括号完全相反——同一个符号在两条指令里含义不同,是 x86 语法的一处真实不一致。
统考的选择语句题几乎总是给出汇编、要求反推 C。 反推的关键就两条:条件是取反的,分支密度决定了 switch 用哪种实现。
对照速查
| 源码关系 | 有符号跳转 | 无符号跳转 |
|---|---|---|
jg | ja | |
jge | jae | |
jl | jb | |
jle | jbe | |
je / jne | 同 |
switch 实现 | 适用 | 复杂度 | 用到的寻址方式 |
|---|---|---|---|
| 级联比较 | 分支少、case 稀疏 | 相对寻址(每个 je) | |
| 跳转表 | 分支多、case 密集 | 变址 + 存储器间接 |
| 判断 | 对错 |
|---|---|
源码写 >,机器码里的跳转也是”大于”类 | ❌(取反,跳的是 jle) |
Jcc 会修改标志位 | ❌(只读) |
Jcc 必须紧跟 cmp | ❌ |
有符号比较用 ja/jb | ❌(那是无符号;有符号用 jg/jl) |
| 跳转表比级联比较永远更好 | ❌(case 稀疏时表太大) |
| 跳转表检查越界要用有符号比较 | ❌(用无符号 ja 才能一次挡两侧) |
cmov 可以优化 p ? *p : 0 | ❌(会无条件解引用) |
cmov 不会造成分支预测失败 | ✅ |
考点
cmp产生标志位 → PSW →Jcc消费,是所有选择语句的骨架g/l是有符号,a/b是无符号;同一条cmp支持两种解读- 编译器把条件取反,让 then 分支 fall through——读汇编时的第一反应
switch两种实现:密集用跳转表 ,稀疏用级联比较- 跳转表三步:减下界 → 无符号
ja挡越界 → 变址+存储器间接跳转 ja能一次挡住上下两侧越界,因为负数减法后变成大无符号数Jcc只读标志位,中间可插入mov/leacmov消除分支,但两个分支都会被求值,有副作用或可能出错时不可用jmp [...]的方括号表示访存,lea [...]的方括号不表示访存
链接
- 🏠 返回总览:计算机组成原理第 4 章:指令系统总览
- ⬅️ 上一节:4.3.1 常用汇编指令介绍
- ➡️ 下一节:4.3.3 循环语句的机器级表示
- 🔗 2.1.3 整数的表示(同一串位的两种解读)
- 🔗 4.2.2 常见的数据寻址方式(跳转表用的两种寻址)
- 🔗 5.5 指令流水线(本地资料)(控制冒险与分支预测)
- 📖 名词库:第 4 章名词库