4 汇编初步
4.1 计算机系统的抽象层级与指令集架构
理解汇编语言,首要前提是建立计算机系统的抽象层次观。汇编语言作为高级语言与机器级二进制代码之间的桥梁,是理解程序底层行为、性能优化及系统安全不可或缺的基石。本讲重点剖析主流的x86-64体系结构。
4.1.1 抽象层级
现代计算系统通过层层抽象来屏蔽底层复杂性:
- 高级语言 (High-Level Language, 如 C/C++):提供变量、控制流、数据结构等高度抽象的逻辑表达。
- 汇编语言 (Assembly Language):高级语言经过编译器 (Compiler) 翻译后的文本表示,是机器指令的助记符形式。
- 机器语言 (Machine Code):由汇编器 (Assembler) 生成,CPU 可直接解码并执行的纯二进制序列。
- 微架构 (Microarchitecture):硬件层面对指令集架构的具体物理实现(如超标量流水线、分支预测器、多级缓存)。
4.1.2 指令集架构 (Instruction Set Architecture, ISA)
设计哲学 / 核心概念
ISA 是软件与硬件之间最关键的一份“契约”。它精确定义了处理器的状态(如寄存器、内存模型)、机器指令的格式、语义以及每条指令对处理器状态造成的影响。
在 x86-64 架构下,程序可见的状态主要包括:
- 程序计数器 (PC, 在 x86 中称为
%rip):始终存储下一条将要执行的指令在内存中的地址。 - 寄存器文件 (Register File):包含一组可以直接被指令访问的高速存储单元。
- 条件码寄存器 (Condition Codes/Flags):存储最近执行的算术或逻辑指令的状态特征(如进位、溢出),是实现控制流分支的硬件基础。
- 内存 (Memory):对汇编级程序而言,内存被抽象为一个巨大的字节数组。
4.2 x86-64 寄存器组织 (Register Organization)
寄存器是 CPU 内部访问速度最快的存储层级。x86-64 架构在传统的 32 位 x86 (IA32) 基础上,将通用寄存器的数量扩展至 16 个,位宽扩展至 64 位。
4.2.1 寄存器分类与系统级规约
每一个 64 位寄存器均可按需作为 32 位、16 位或 8 位来访问(例如%rax的低 32 位是%eax,低 16 位是%ax,低 8 位是%al)。根据System V AMD64 ABI(类 UNIX/Linux 系统标准的应用程序二进制接口),寄存器被赋予了特定的功能语义:
- 函数参数传递:
%rdi(Arg 1),%rsi(Arg 2),%rdx(Arg 3),%rcx(Arg 4),%r8(Arg 5),%r9(Arg 6)。 - 返回值:
%rax专用于存储函数的返回值。 - 栈与帧指针:
%rsp(Stack Pointer):栈顶指针,动态指向当前运行时栈的顶部。在 x86-64 中,栈向低地址方向生长。%rbp(Base Pointer):帧指针,常用于标识当前函数栈帧的基准位置(尽管在开启现代编译器优化如-O1或-O2后,帧指针常被省略以释放作为通用寄存器使用)。
- 被调用者保存 (Callee-saved) 寄存器:
%rbx,%rbp,%r12~%r15。函数在修改这些寄存器前必须将其原值压栈保护,并在返回前恢复。 - 调用者保存 (Caller-saved) 寄存器:除上述外的其余通用寄存器,调用子函数期间可能会被破坏,调用者需自行负责保存。
4.3 寻址模式与内存访问 (Addressing Modes)
指令如何获取其操作数?x86-64 提供了高度灵活的寻址机制,其本质是有效地址 (Effective Address, EA)的计算。
4.3.1 操作数的三种基本形态
- 立即数寻址 (Immediate):操作数直接嵌在指令码中。在 AT&T 语法下,立即数以
$为前缀,如$-577或$0x1F。 - 寄存器寻址 (Register):操作数存储在寄存器中,直接通过寄存器名称访问,如
%rax。 - 内存寻址 (Memory):最复杂的寻址方式,根据计算出的有效地址去内存中读取或写入数据。
4.3.2 通用内存寻址范式
在 x86-64 中,内存寻址的最一般化形式为:D(Rb, Ri, S)
其底层硬件逻辑对应的地址计算多项式为:
EA = Reg[Rb] + (Reg[Ri] × S) + D
- Rb (Base Register):基址寄存器,可以是任意通用寄存器。
- Ri (Index Register):变址寄存器,除
%rsp外的任意通用寄存器。 - S (Scale Factor):比例因子,受硬件位移逻辑限制,仅能取
1, 2, 4, 8(对应不同数据类型的大小,如int对应 4,double对应 8)。 - D (Displacement):位移量,通常为一个 8 位、16 位或 32 位的常量。
示例:
若%rdx存储了数组基地址,%rcx存储了数组索引,则0x8(%rdx, %rcx, 4)计算出的物理地址即为Reg[%rdx] + Reg[%rcx] * 4 + 8。这在 C 语言中完美契合了结构体数组struct_array[i].field的访问逻辑。
4.4 数据传送指令 (Data Movement Instructions)
mov指令族是汇编中最频繁使用的指令,用于在寄存器与内存之间搬运数据。
4.4.1 基本指令与位宽后缀
AT&T 语法采用后缀显式声明操作的数据位宽:
movb(byte, 8-bit)movw(word, 16-bit)movl(long word, 32-bit)movq(quad word, 64-bit)
指令格式:mov SRC, DST(将 SRC 传送到 DST)。
硬件级限制:x86-64 架构不允许直接的内存到内存 (Memory-to-Memory)的单一数据传送。必须先将源内存加载到寄存器,再从寄存器写入目的内存。
4.4.2 符号扩展与零扩展
当将一个较窄的数据类型复制到较宽的寄存器时,高位如何填充是一个关键问题:
- 零扩展 (
movz族):例如movzbl(将 byte 零扩展至 long)。高位全部填充0。主要用于无符号数 (Unsigned)。 - 符号扩展 (
movs族):例如movslq(将 long 符号扩展至 quad)。高位全部填充符号位(最高位)。主要用于有符号补码数 (Two’s Complement)。
(补充:在 x86-64 中,任何生成 32 位寄存器结果的指令,都会自动将该寄存器的高 32 位清零。)
4.5 算术与逻辑指令 (Arithmetic & Logical Operations)
4.5.1 LEA 指令 (Load Effective Address)
leaq S, D是 x86 体系中最精妙的指令之一。
它在形式上类似movq,接受一个内存寻址模式S。但它绝不进行内存解引用,而是纯粹将计算出的有效地址 (EA)作为数值存入目的寄存器D。
除了用于计算指针地址,现代编译器极度青睐用leaq来执行简单的算术运算。
优化示例:
// C 代码longscale(longx,longy,longz){returnx+4*y+12*z;}# 对应的汇编核心逻辑 (假设 x 在 %rdi, y 在 %rsi, z 在 %rdx) leaq (%rdi, %rsi, 4), %rax # %rax = x + 4*y leaq (%rax, %rdx, 8), %rax # %rax = %rax + 8*z leaq (%rax, %rdx, 4), %rax # %rax = %rax + 4*z (相当于加了 12*z) # leaq 既完成了乘法,又完成了加法,且不占用 ALU 的乘法器,执行速度极快。4.5.2 基础算术运算
- 一元操作:
inc D(加 1),dec D(减 1),neg D(取负补码),not D(按位取反)。 - 二元操作:
add S, D(D = D + S),sub S, D(D = D - S),imul S, D(D = D * S)。 - 移位操作:
sal / shl S, D:左移,右端补零(算术与逻辑左移等价)。sar S, D:算术右移,左端补符号位(保证负数仍为负)。shr S, D:逻辑右移,左端补零。
4.6 控制流与状态标志 (Control Flow & Condition Codes)
顺序执行是程序的常态,但分支(if-else)与循环(for/while)构成了图灵完备的基础。硬件通过条件码 (Condition Codes)和条件跳转指令协同实现控制流的转移。
4.6.1 核心条件码
处理器内部维护了一个特殊的%eflags/%rflags寄存器,每当算术/逻辑单元 (ALU) 运算完成时,硬件会自动更新以下标志位:
- CF (Carry Flag, 进位标志):最高位发生进位或借位。用于探测无符号数溢出。
- ZF (Zero Flag, 零标志):运算结果为 0。
- SF (Sign Flag, 符号标志):运算结果为负(即最高位为 1)。
- OF (Overflow Flag, 溢出标志):发生有符号补码溢出(正正得负,或负负得正)。
4.6.2 显式状态设置:CMP 与 TEST
为了专门设置条件码而不污染寄存器,指令集提供了两种探测指令:
cmp S1, S2(Compare):计算S2 - S1,丢弃结果,仅更新条件码。是实现if (a < b)的基石。test S1, S2(Test):计算S2 & S1,丢弃结果,仅更新条件码。常用于掩码测试,或test %rax, %rax来判断%rax是正、负还是零。
4.6.3 条件流转:跳转指令族
- 无条件跳转:
jmp L(直接将%rip设置为标签 L 的地址)。 - 条件跳转 (
jX):依据条件码的状态组合进行判断。- 相等测试:
je(Equal,ZF=1),jne(Not Equal,ZF=0)。 - 有符号比较:
jg(Greater, 大于),jl(Less, 小于),jge(大于等于),jle(小于等于)。
(以jl为例,其触发条件为SF ^ OF = 1,即符号位与溢出位异或为 1。因为若产生正向溢出,结果虽为负但实际是 S2 > S1;若无溢出,SF=1 则确切表明 S2 < S1。硬件设计极度严密) - 无符号比较:
ja(Above, 高于),jb(Below, 低于)。依靠 CF 和 ZF 判断。
- 相等测试:
通过cmp配合jX,编译器可将高级语言的结构化控制流完全展平为带状态测试的 goto 语句。
// C 语言 if (x > y)if(x>y){x=x-y;}# 对应的核心汇编逻辑 cmpq %rsi, %rdi # 比较 x (%rdi) 和 y (%rsi) jle .L2 # 若 x <= y,跳转跳过 if 块 subq %rsi, %rdi # x = x - y .L2: