Chapter 03 Machine Level Representation of Programs
3.1 历史观点¶
3.1.1 Intel 处理器系列 (x86)¶
- 向后兼容性: 每一代处理器都能执行上一代处理器的机器代码.
- 摩尔定律 (Moore's Law): 晶体管数量每 18 个月翻一番.
3.2 程序编码¶
3.2.1 机器级代码¶
- 指令集体系结构 (ISA): 定义了处理器状态, 指令的格式, 以及每条指令对状态的影响.
- 机器代码: 字节序列, 处理器执行的实际代码.
- 汇编代码: 机器代码的文本表示.
- 编译器 (gcc) 将 C 语言代码转换为汇编代码 (
-S), 汇编器 (as) 将汇编代码转换为机器代码 (-c), 链接器 (ld) 将目标文件组合成可执行文件.
3.2.2 汇编代码格式¶
- AT&T 格式 (gcc 默认): 源操作数在左, 目的操作数在右. 寄存器前加
%, 立即数前加$. - Intel 格式: 源操作数在右, 目的操作数在左.
3.3 数据格式¶
3.3.1 字长后缀¶
b: byte (1 字节)w: word (2 字节)l: double word (4 字节)q: quad word (8 字节)- 单精度浮点数:
s - 双精度浮点数:
l(注意与 4 字节整数后缀重名)
3.4 访问信息¶
3.4.1 操作数指示符¶
- 立即数 (Immediate):
$Imm. 如$0x1F. - 寄存器 (Register):
%rax.R[r_a]. - 内存引用 (Memory Reference):
Imm(r_b, r_i, s). - 有效地址: \(Imm + R[r_b] + R[r_i] \times s\).
s必须是 1, 2, 4, 8.
3.4.2 数据传送指令¶
MOV S, D: \(D \leftarrow S\).movb,movw,movl,movq.- 不影响条件码:
mov指令只会更新目的操作数,不会改变条件码寄存器。 - 零扩展:
movzbw(byte -> word),movzbl... - 符号扩展:
movsbw(byte -> word),movsbl... cltq: 把%eax符号扩展到%rax.
movl 的特殊行为
movl 指令以寄存器作为目的时, 会把该寄存器的高 4 字节设置为 0. (x86-64 的规定: 任何为寄存器生成 32 位值的指令都会把该寄存器的高 32 位清零).
3.4.3 压入和弹出栈数据¶
- 栈向下增长 (高地址 -> 低地址).
pushq S:R[%rsp] -= 8; M[R[%rsp]] = S.popq D:D = M[R[%rsp]]; R[%rsp] += 8.push和pop指令不影响条件码。
3.5 算术和逻辑操作¶
3.5.1 加载有效地址¶
leaq S, D: \(D \leftarrow \&S\).- 用于计算地址, 不访问内存.
- 也常用于执行简单的算术运算 (如
x + 2*y). - 不影响条件码:
leaq仅用于地址计算,不会改变条件码寄存器。这是它与add等指令的重要区别。
3.5.2 一元和二元操作¶
- 一元:
inc,dec,neg,not. inc和dec会设置 OF, SF, ZF, 但不会改变 CF (进位标志)。这是一个常见的坑。- 二元:
add,sub,imul,xor,or,and. xor,or,and: 这些逻辑操作会清除 CF 和 OF (置为 0),并根据结果设置 SF, ZF 和 PF。- 移位:
sal(算术左移),shl(逻辑左移),sar(算术右移),shr(逻辑右移). - 移位量由
%cl寄存器或立即数指定. - 移位操作的标志位: CF 被设置为最后移出的位。OF 仅在移位量为 1 时定义(如果符号位改变则置 1)。
3.5.3 特殊的算术操作¶
- 128 位乘法/除法:
imulq,mulq,idivq,divq. - 结果存储在
%rdx:%rax中.
3.6 控制¶
3.6.1 条件码¶
CF(Carry Flag): 进位标志. (无符号溢出)ZF(Zero Flag): 零标志. (结果为 0)SF(Sign Flag): 符号标志. (结果为负)OF(Overflow Flag): 溢出标志. (有符号溢出)cmp指令: 类似于sub, 但只设置条件码, 不保存结果.test指令: 类似于and, 但只设置条件码, 不保存结果. 常用于testq %rax, %rax检查%rax是正数、负数还是零。
指令对条件码的影响总结
- 不改变任何标志位:
leaq,mov,push,pop. - 修改所有标志位:
add,sub,neg(CF, ZF, SF, OF). - 逻辑运算 (
xor,and,or): CF=0, OF=0. 更新 ZF, SF. - 自增/自减 (
inc,dec): 更新 ZF, SF, OF. CF 保持不变. - 移位操作: CF = 最后移出的位.
3.6.2 访问条件码¶
set指令: 根据条件码设置目的操作数 (低 8 位) 为 0 或 1.sete(Equal / Zero)sets(Negative)setg(Greater, signed)seta(Above, unsigned)
3.6.3 跳转指令¶
jmp: 无条件跳转.jcc: 条件跳转 (如je,jne,jg,jge,jl,jle,ja,jb).- 跳转目标可以是直接标号或间接地址 (
*).
3.6.4 跳转指令的编码¶
- PC 相对编码: 目标地址 = 下一条指令地址 + 偏移量.
- 绝对地址编码.
3.6.5 翻译条件分支¶
if-else语句通常翻译为条件跳转.goto语句.
3.6.6 循环¶
do-while: 至少执行一次.while: 可能一次都不执行 (先跳转到测试条件, 或先测试条件再跳转).for: 初始化 -> 测试 -> 循环体 -> 更新.
3.6.7 条件传送指令¶
cmovcc S, R: 如果条件cc成立, 则 \(R \leftarrow S\).- 避免了控制冒险 (Control Hazard), 更有利于流水线执行 (因为不需要分支预测).
- 限制: 源和目的操作数必须都在寄存器或内存中 (通常是寄存器), 且计算源值不能有副作用.
3.6.8 switch 语句¶
- 跳转表 (Jump Table): 一个数组, 存储了代码段的地址.
- 通过索引直接跳转到对应的代码段, 效率高 (O(1)).
- 适用于 case 值比较密集的情况.
3.7 过程¶
3.7.1 运行时栈¶
- 栈帧 (Stack Frame): 每个过程调用在栈中分配的一块内存区域.
%rsp: 栈指针, 指向栈顶.%rbp: 帧指针 (可选, 用于变长栈帧或调试).- 对齐要求: 在调用函数 (
call) 之前,栈指针%rsp必须是 16 字节对齐的。
3.7.2 转移控制¶
call Label:- 将返回地址 (下一条指令地址) 压入栈.
- 跳转到
Label. ret:- 从栈中弹出返回地址.
- 跳转到返回地址.
3.7.3 数据传送¶
- 前 6 个整型参数通过寄存器传递:
%rdi,%rsi,%rdx,%rcx,%r8,%r9. - 超过 6 个的参数通过栈传递 (逆序压栈, 第 7 个参数在栈顶).
- 返回值放在
%rax中.
3.7.4 栈上的局部存储¶
- 局部变量通常保存在寄存器中.
- 必须保存在内存中的情况:
- 寄存器不够用.
- 对局部变量使用了取地址符
&. - 局部变量是数组或结构.
3.7.5 寄存器中的局部存储空间¶
- 被调用者保存 (Callee-saved):
%rbx,%rbp,%r12-%r15. - 被调用函数必须在返回前恢复这些寄存器的值 (通常是先 push 保存, 返回前 pop 恢复).
- 调用者保存 (Caller-saved):
%r10,%r11, 以及参数寄存器. - 调用者负责保存这些寄存器 (如果后续还需要用到).
3.8 数组分配和访问¶
3.8.1 基本原则¶
T A[N]: 分配 \(L \times N\) 字节的连续空间.A[i]的地址: \(x_A + L \times i\).
3.8.2 指针运算¶
- 指针加 1, 地址增加该指针指向类型的大小 \(L\).
3.8.3 嵌套数组¶
T A[R][C]: 行优先存储.A[i][j]的地址: \(x_A + L(C \times i + j)\).
3.8.4 定长和变长数组¶
- 变长数组 (C99): 允许数组维度是表达式.
- 编译器会生成计算数组大小和偏移量的代码 (乘法).
3.9 异质数据结构¶
3.9.1 结构 (Structure)¶
- 成员在内存中连续存放.
- 编译器维护每个成员的偏移量.
3.9.2 联合 (Union)¶
- 所有成员共享同一块内存.
- 大小等于最大成员的大小.
- 用于节省空间或访问同一数据的不同位模式.
3.9.3 数据对齐¶
- 原则: \(K\) 字节基本对象的地址必须是 \(K\) 的倍数. (通常 \(K=1, 2, 4, 8\)).
- 目的: 提高内存访问效率 (硬件通常一次访问对齐的块).
- 编译器会在结构体成员之间插入填充 (Padding) 以满足对齐要求.
- 结构体总大小必须是其最大成员对齐要求的倍数 (末尾也可能填充).
3.10 在机器级程序中将控制与数据结合起来¶
3.10.1 理解指针¶
- 指针是地址.
- 指针有类型 (决定了如何解释该地址处的数据以及指针运算的步长).
- 指针可以强制转换.
3.10.2 缓冲区溢出 (Buffer Overflow)¶
- C 语言不进行数组边界检查.
- 向栈上的局部数组写入超过其长度的数据, 可能覆盖返回地址, 导致程序跳转到攻击者指定的代码 (如 Shellcode).
3.10.3 对抗缓冲区溢出攻击¶
- 栈随机化 (Stack Randomization / ASLR): 每次运行程序时栈的位置都不同. 攻击者难以猜测栈地址.
- 栈破坏检测 (Stack Canary): 在缓冲区和返回地址之间插入一个特殊值 (Canary). 函数返回前检查 Canary 是否被修改.
- 限制可执行代码区域 (NX bit): 标记栈和堆为不可执行.
3.11 浮点代码¶
3.11.1 浮点传送和转换操作¶
%xmm0-%%xmm15: 128 位 XMM 寄存器 (也可以看作 256 位 YMM).- 浮点数参数通过
%xmm0-%%xmm7传递. - 返回值放在
%xmm0中. - 数据传送:
movss(单精度),movsd(双精度),movaps(对齐的压缩单精度),movapd(对齐的压缩双精度).
3.11.2 过程中的浮点代码¶
- XMM 寄存器都是调用者保存的 (Caller-saved).
3.11.3 浮点运算操作¶
addss,addsd,subss, ...- 标量运算 (Scalar, 结尾
ss/sd) 只对低位分量进行运算. - 向量运算 (Packed, 结尾
ps/pd) 对所有分量进行并行运算 (SIMD).
3.11.4 定义和使用浮点常数¶
- 通常存储在只读数据段 (.rodata) 中, 通过内存引用访问.
3.11.5 C 语言中的浮点位级操作¶
- 浮点比较指令 (
ucomiss,ucomisd) 设置条件码. - 还要考虑 NaN 的情况 (Unordered).