CSAPP - 03.Machine-Level Programming II
机器级编程 II:算数与控制 (Machine-Level Programming II: Arithmetic & Control)
真正的程序员可以用任何语言编写汇编代码。(REAL PROGRAMMERS CAN WRITE ASSEMBLY CODE IN ANY LANGUAGE.)
— Larry Wall, 美国程序员 (American Programmer)
主要内容:
本章关注计算机如何实现逻辑控制。包括:
- 条件码 (Condition Codes):CPU 如何通过
ZF,SF等标志位记住上一次计算的结果。 - 跳转指令 (Jumps):汇编中没有
if和while,只有goto(即jmp/je)。 - 翻译控制结构:详细讲解了 C 语言中的
if-else、while、for和switch是如何被“降维打击”成汇编中的条件跳转和跳转表的。
Motivation:
高级语言有优雅的结构(如循环、分支),但底层硬件非常“笨”,只会按顺序执行或跳转。理解这一章,你就能明白编译器是如何“欺骗”硬件来模拟复杂逻辑的,也能理解为什么某些写法(如 switch 跳转表)会比其他写法更快。
今日主题 (Today)
- 控制:条件码 (Control: Condition codes)
- 条件分支 (Conditional branches)
- 循环 (Loops)
- Switch 语句 (Switch Statements)
2.1 控制流 (Control Flow)
高级语言使用 if/else 这样的控制结构,但汇编只有跳转 (jumps)。
C 代码 (C Code) 示例:
1 | |
汇编 (Assembly) 中的控制流:
1 | |
- 这一切都是用 GOTO (跳转) 完成的!
处理器状态 (Processor State) (x86-64, 部分)
这是关于当前执行程序的信息。
- 寄存器 (Registers): 临时数据 (Temporary data)
%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp%r8-%r15
- 指令指针 (Instruction pointer) (
%rip):- 当前代码控制点的位置 (Location of current code control point)
- 条件码 (Condition codes):
CF,ZF,SF,OF- 最近测试的状态 (Status of recent tests)
条件码 (Condition Codes) (隐式设置)
这些是单位寄存器 (Single bit registers),它们作为算术操作(例如 addq)的副作用被隐式设置。
CF(Carry Flag): 进位标志 (用于无符号数)- 如果最高有效位 (most significant bit) 产生进位(无符号溢出),则设置。
- 在执行减法操作(如
sub或cmp)时,它充当的是 借位标志 (Borrow Flag)。
ZF(Zero Flag): 零标志- 如果结果
t == 0,则设置。
- 如果结果
SF(Sign Flag): 符号标志 (用于有符号数)- 如果结果
t < 0(作为有符号数),则设置。
- 如果结果
OF(Overflow Flag): 溢出标志 (用于有符号数)- 如果发生二补数(有符号)溢出,则设置。
(a>0 && b>0 && t<0) || (a<0 && b<0 && t>=0)
注意: lea (Load Effective Address) 指令不会设置条件码。
显式设置
cmp a, b- 计算
b - a(就像sub),但是sub指令会将结果(差)放在b中,但是cmp不会。 - 根据结果设置条件码
- 在 AT&T 语法(即
Instruction Source, Destination)中,条件跳转指令的阅读顺序通常是从右往左读,或者说是第二个操作数 与 第一个操作数 比较
- 在 AT&T 语法(即
- 用于
if (a < b)这样的判断
- 计算
test a, b- 计算
b & a(就像and),区别在于不改变b - 根据结果设置条件码(主要是
SF和ZF) - 最常见用途:
test %rX, %rX,用于比较%rX和 0
- 计算
条件码小结
- 构成: 四个标识 (Four flags):
CF,ZF,SF,OF - 设置方法:
- 显式 (Explicit):
CMP与TEST - 隐式 (Implicit): 算术运算 (Arithmetic operations)
- 显式 (Explicit):
- 访问方法:
- 显式 (Explicit):
SET指令 (SET instructions) - 隐式 (Implicit): 条件跳转 (Conditional jumps)、条件赋值 (Conditional moves)
- 显式 (Explicit):
2.2 条件分支(Conditional branches)
访问条件码:Jumping (jX)
jX 指令根据条件码跳转到代码的不同部分。
| 指令 (Instruction) | 条件 (Condition) | 描述 (Description) |
|---|---|---|
jmp |
1 |
无条件 (Unconditional) |
je |
ZF |
相等 / 零 (Equal / Zero) |
jne |
~ZF |
不相等 / 非零 (Not Equal / Not Zero) |
js |
SF |
负数 (Negative) |
jns |
~SF |
非负数 (Nonnegative) |
jg |
~(SF^OF) & ~ZF |
大于 (Greater) (有符号) |
jge |
~(SF^OF) |
大于或等于 (Greater or Equal) (有符号) |
jl |
(SF^OF) |
小于 (Less) (有符号) |
jle |
(SF^OF) | ZF |
小于或等于 (Less or Equal) (有符号) |
ja |
~CF & ~ZF |
高于 (Above) (无符号) |
jb |
CF |
低于 (Below) (无符号) |
访问条件码:Setting (setX)
setX 指令根据条件码的组合将目标的低位字节 (low-order byte) 设置为 0 或 1。
- 它不会改变寄存器中剩余的 7 个字节。
setX的参数总是一个低位字节寄存器(如%al,%r8b等)。
| 指令 (Instruction) | 条件 (Condition) | 描述 (Description) |
|---|---|---|
sete |
ZF |
相等 / 零 (Equal / Zero) |
setne |
~ZF |
不相等 / 非零 (Not Equal / Not Zero) |
sets |
SF |
负数 (Negative) |
setns |
~SF |
非负数 (Nonnegative) |
setg |
~(SF^OF) & ~ZF |
大于 (Greater) (有符号) |
setge |
~(SF^OF) |
大于或等于 (Greater or Equal) (有符号) |
setl |
(SF^OF) |
小于 (Less) (有符号) |
setle |
(SF^OF) | ZF |
小于或等于 (Less or Equal) (有符号) |
seta |
~CF & ~ZF |
高于 (Above) (无符号) |
setb |
CF |
低于 (Below) (无符号) |
setX 指令示例
movzbl (Move Zero-Extend Byte to Long) 通常在 setX 之后使用,以将 32 位寄存器的高位清零。
C 代码 (C Code):
1 | |
汇编 (Assembly Code):
1 | |
movzbl %al, %eax将%al中的 1 字节结果移动到 4 字节的%eax,并将高 3 字节填充为 0。这也将 64 位%rax寄存器的高 4 字节清零。- 在 x86-64 架构中,任何向 32 位寄存器(如
%eax)写入的操作,都会自动将高 32 位(即%rax的高半部分)全部清零。
条件分支示例 (Conditional Branch Example)
C 代码 (C Code):
1 | |
汇编 (Assembly) (使用分支):
1 | |
用 Goto 代码表达 (Expressing with Goto Code)
C 语言允许 goto 语句。编译器实质上是将 if-else 结构翻译成基于 goto 的版本。
goto 版本 (编译器视角):
1 | |
结构化编程 (Structured Programming)
- 1968年,Edsger Dijkstra 写了“Go To Statement Considered Harmful”(Go To 语句被认为有害)的里程碑式论文。
- 他认为
goto语句会导致“面条式代码” (spaghetti code),使程序难以理解。 - 新语言引入了用于受控分支 (controlled branching) 的结构:
if / else-if / else和switch语句while和for循环
通用条件表达式翻译 (使用分支)
C 代码 (C Code) (三元运算符):val = Test ? Then_Expr : Else_Expr;
Goto 版本 (Goto Version):
1 | |
这为 then 和 else 表达式创建了单独的代码区域,并执行适当的一个。
使用条件移动 (Using Conditional Moves)
- 条件移动指令 (Conditional Move Instructions) (
cmovX) 提供了分支 (branching) 的替代方案。 - 指令支持:
if (Test) Dest = Src。 - 为何使用它们?
- 分支 (Branches) 对现代处理器的指令流水线 (pipelines) 具有非常大的干扰性。
- 条件移动不需要控制转移 (control transfer)。
条件移动 (Conditional Move) 示例
C 代码 (C Code):
1 | |
汇编 (Assembly) (使用 cmov):
1 | |
- 此代码计算了
x-y和y-x两者。 cmovle(Conditional Move if Less or Equal) 根据cmpq的标志位选择正确的结果,而无需跳转。- 在不使用 jump 语句时候,处理器运行速度更快。
条件移动的不适用情况 (Bad Cases for Conditional Move)
条件移动会计算两个表达式,因此在以下情况下表现不佳:
- 昂贵的计算 (Expensive Computations):
val = Test(x) ? Hard1(x) : Hard2(x);Hard1(x)和Hard2(x)都会被计算。
- 有风险的计算 (Risky Computations) (不安全 Unsafe):
val = p ? *p : 0;- 这会尝试解引用 (dereference)
p,即使p是NULL,导致错误。
- 有副作用的计算 (Computations with side effects) (非法 Illegal):
val = x > 0 ? x *= 7 : x += 3;- 代码必须没有副作用。
2.3循环 (Loops)
“Do-While” 循环示例
此函数计算 x 中 ‘1’ 的数量 (popcount)。
C 代码 (C Code):
1 | |
Goto 版本 (Goto Version):
1 | |
“Do-While” 循环编译
汇编 (Assembly) (针对 pcount_goto):
1 | |
通用 “Do-While” 翻译
C 代码 (C Code):
1 | |
Goto 版本 (Goto Version):
1 | |
通用 “While” 翻译 #1 (Jump-to-middle)
- “跳转到中间” (Jump-to-middle) 翻译
- 与
-Og编译选项一起使用
While 版本 (While version):
1 | |
Goto 版本 (Goto Version):
1 | |
通用 “While” 翻译 #2 (Do-while conversion)
- “Do-while” 转换
- 与
-O1编译选项一起使用
While 版本 (While version):
1 | |
Do-While 版本 (Do-While Version):
1 | |
“For” 循环 (For Loop)
for 循环通过将其转换为 do-while 形式来进行翻译。
For 版本 (For version):
1 | |
Do-While 版本 (Do-While Version):
1 | |
总结 (Summarizing)
编译器生成代码序列以实现复杂的 C 控制结构。
| C 控制 (C Control) | 汇编控制 (Assembler Control) |
|---|---|
if-then-else |
条件跳转 (Conditional jump) 或 条件移动 (Conditional move) |
do-while |
条件跳转 (Conditional jump) |
while, for |
条件跳转 (Conditional jump) (转换为 do-while 或 jump-to-middle) |
switch |
间接跳转 (Indirect jump) (通过跳转表 (jump tables)) |
- 稀疏的
switch语句可能使用决策树 (decision trees) (if-elseif-else)。