Appearance
x86汇编语言基础
考情分析
程序的机器级代码表示是 2022 年大纲新增的一条,但相关知识在此之前就已经以综合题的形式出现过了——只是当时挂在别的条目下。统考主要考查 x86 指令(Intel 格式),偶尔涉及 MIPS(会附功能说明)。
大纲定位
考纲第四章(六)「高级语言程序与机器级代码之间的对应」第 1 条:编译器、汇编器和链接器的基本概念。
本篇讲的汇编语言正是这条工具链的中间产物,所以先把工具链交代清楚,再看汇编本身长什么样。第 2~4 条(选择/循环/过程调用的机器级表示)见《程序的机器级表示》。
从 C 源码到可执行文件:四个工具
汇编代码不是凭空出现的——它是编译器的输出、汇编器的输入。四步流水线,每一步的输入必须是上一步的输出,顺序不能换:

图 4.16 可重定位目标文件与静态库的链接
| 工具 | 吃什么 | 吐什么 |
|---|---|---|
| 预处理器(cpp) | 源程序 .c | 展开宏和 #include 后的 .i |
| 编译器(cc1) | 预处理后的源程序 | 汇编语言程序 .s |
| 汇编器(as) | 汇编语言程序 .s | 可重定位目标文件 .o(机器码) |
| 链接器(ld) | 若干 .o + 库文件(libc.a 等) | 可执行目标文件 |
图里 main.c 经"翻译器(cpp、cc1、as)"变成 main.o,再和自定义静态库 mylib.a、标准库 libc.a 里取出的模块一起送进静态链接器 ld,最后产出完整链接的可执行文件。
编者注(口径):考研题里常用两级简化模型——"编译程序:高级语言
机器目标代码",把编译和汇编合起来说。gcc 的细粒度划分里编译只到 .s,汇编器才产生机器码。两种口径都有人用,判题时看题目问的是哪一层:问"输入是高级语言、输出是目标代码的是谁",答编译程序;问"谁把汇编语言翻成机器语言",答汇编程序。三个易混点:汇编程序(assembler,一个翻译工具)≠ 汇编语言程序(用汇编语言写的源代码);解释程序逐句解释执行、不产生目标代码文件;链接器的输入已经是机器码,它只做符号解析和地址重定位,不做翻译。
工具链的更完整介绍见《计算机系统层次结构》。
x86 通用寄存器
x86 有 8 个 32 位通用寄存器:
| 寄存器 | 低16位 | 高/低8位 | 常见用途 |
|---|---|---|---|
| EAX | AX | AH/AL | 累加器,函数返回值 |
| EBX | BX | BH/BL | 基址寄存器 |
| ECX | CX | CH/CL | 计数器(循环/移位) |
| EDX | DX | DH/DL | 数据寄存器,乘除法扩展 |
| ESI | SI | — | 源变址寄存器 |
| EDI | DI | — | 目的变址寄存器 |
| EBP | BP | — | 基址指针(栈帧底部) |
| ESP | SP | — | 栈指针(栈顶) |
EBP 和 ESP 有专用功能,其余寄存器用途较灵活。

图 3.2 IA-32 的定点寄存器组
图里的位刻度说明了子寄存器是怎么嵌套的:EAX 是 32 位,其低 16 位叫 AX,AX 再拆成高 8 位 AH 和低 8 位 AL。所以 mov al, 5 只改 EAX 的最低字节,其余 24 位不动——反汇编里看到 8 位/16 位寄存器名时要意识到这一点。
图下方还有两组本篇不展开但要认得的寄存器:EIP(指令指针,即 PC)、EFLAGS(标志寄存器,CF/ZF/SF/OF 就在里面),以及 6 个段寄存器。
操作数标记
<reg>:寄存器(如 eax、ebx)<mem>:内存地址(如[eax]、[ebp-8]、dword ptr [eax+ebx*4+8])
dword ptr 是什么:内存操作数只给了地址,没说要按几个字节访问,所以要加一个长度前缀说明宽度。
| 前缀 | 访问宽度 |
|---|---|
byte ptr | 1 字节 |
word ptr | 2 字节 |
dword ptr | 4 字节(双字) |
qword ptr | 8 字节 |
真题的反汇编里几乎行行都有它,如 cmp dword ptr [ebp-0Ch], ecx 就是"把 [ebp-0Ch] 处的 4 字节和 ecx 比较"。当另一个操作数是寄存器时宽度已经确定,前缀可省。
<con>:立即数/常数
两个操作数不能同时为内存。
怎么读一行反汇编
真题给的不是光秃秃的汇编,而是三列格式——这三道大题的格式完全一致,先认清每列是什么:
序号 虚拟地址 机器码 汇编
19 004010AE C7 84 82 00 20 42 00 0A 00 00 00 mov [ecx+edx*4+00422000h], 0Ah
20 004010B9 8B 45 F8 mov eax, [ebp-8]| 列 | 是什么 | 能拿来算什么 |
|---|---|---|
| 序号 | 题目给的行号 | 定位用 |
| 虚拟地址 | 该指令首字节的地址 | 相对寻址的基准 |
| 机器码 | 该指令的字节序列 | 数它有几个字节 = 指令长度 |
| 汇编 | 助记符 + 操作数 | 语义 |
机器码那一列是最容易被浪费的信息,两条规则从它推出来:
验证一下上面的例子:第 19 条机器码 C7 84 82 00 20 42 00 0A 00 00 00 共 11 字节,
编者注(易错):第二条公式末尾那个"
末指令长度"常被漏成 或干脆不加。末指令通常是 ret(机器码C3,1 字节),这时才恰好等于。若末指令不是 ret,就要按它自己的字节数加。
常用指令
以下使用 Intel 格式(目的操作数在前,源操作数在后)。
数据传送指令
| 指令 | 语法 | 功能 |
|---|---|---|
| mov | mov dst, src | 将 src 复制到 dst |
| push | push src | ESP-4,再将 src 压入栈顶 |
| pop | pop dst | 弹出栈顶到 dst,ESP+4 |
| lea | lea dst, [addr] | 将有效地址(非内存内容)加载到 dst |
为什么 push 是 ESP
高地址 ┌──────────────┐
│ 已入栈数据 │
├──────────────┤
│ ← 压栈前 ESP │
├──────────────┤
│ 新压入的 4B │ push:ESP 先 −4,再写入
├──────────────┤
│ ← 压栈后 ESP │
低地址 └──────────────┘弹栈相反:先从 ESP 处读出,再 ESP call 压入返回地址、ret 弹出返回地址,同样各让 ESP 变化 4。统计栈深度或算某个局部变量地址时,这几个
asm
mov eax, ebx ; R[eax] <- R[ebx]
mov eax, [ebp+8] ; R[eax] <- M[R[ebp]+8]
mov [ebp-4], eax ; M[R[ebp]-4] <- R[eax]
push eax ; ESP-4, M[ESP] <- R[eax]
pop ebx ; R[ebx] <- M[ESP], ESP+4
lea eax, [edx+eax*2+8] ; R[eax] <- R[edx]+R[eax]*2+8(计算地址,不访存)lea 是"取地址"指令,常被编译器用来做加法运算(如 lea eax, [edx+eax] 等价于 add eax, edx)。
算术与逻辑运算指令
| 指令 | 功能 | 示例 |
|---|---|---|
| add | 加法,结果存入第一操作数 | add eax, 10 |
| sub | 减法 | sub eax, ebx |
| inc/dec | 自增/自减 1 | inc eax |
| imul | 有符号乘法(两种形式,见下) | imul eax, ebx |
| idiv | 有符号除法,被除数为 edx:eax | idiv ecx |
| neg | 取负(补码) | neg eax |
edx:eax 是什么:两个 32 位寄存器拼成一个 64 位数,EDX 存高 32 位、EAX 存低 32 位。乘除法都要用到它。
imul 的两种形式——真题考过这个区别:
| 形式 | 写法 | 结果去哪 |
|---|---|---|
| 单操作数 | imul ebx | 完整 64 位积落在 edx:eax |
| 双/三操作数 | imul eax, ebx | 只保留低 32 位存回目的寄存器,高半截丢弃 |
道理很简单:
除法反过来:idiv ecx 用 edx:eax 这个 64 位数作被除数,商放 EAX、余数放 EDX。所以除法前常见 cdq 指令——把 EAX 的符号位扩展填满 EDX。 | and/or/xor | 按位与/或/异或 | and eax, 0FH | | not | 按位取反 | not eax | | shl/shr | 逻辑左移/右移 | shl eax, 1 | | sal/sar | 算术左移/右移 | sar eax, 2 |
编者注(易错):编译器常用
shl eax, 1代替乘 2、sar eax, 2代替除以 4,但"移位 = 乘除 2 的幂"只对整数(补码/无符号)成立。IEEE 754 浮点数是"符号位 + 阶码 + 尾数"三段结构,整体移位会破坏这个结构——浮点乘 2 要改的是阶码字段,不是把整个位串左移。看到题目在浮点数上做移位,先怀疑它是不是在设陷阱。
asm
xor edx, edx ; 将 edx 清零(常见技巧)
imul esi, edi, 25 ; R[esi] <- R[edi] * 25
idiv ebx ; eax <- edx:eax / ebx(商),edx <- 余数控制流指令
| 指令 | 功能 |
|---|---|
| jmp label | 无条件转移到 label |
| cmp a, b | 按 |
| test a, b | 按 |
| call label | 将返回地址压栈(ESP |
| ret | 弹出返回地址(ESP |
条件转移的标志条件(真题会直接问"jle 的转移条件是什么"):
| 助记符 | 含义 | 标志条件 |
|---|---|---|
| je / jz | 相等 | |
| jne / jnz | 不等 | |
| jl | 小于(有符号) | |
| jle | 小于等于(有符号) | |
| jg | 大于(有符号) | |
| jge | 大于等于(有符号) | |
| jb | 低于(无符号) | |
| jbe | 低于等于(无符号) | |
| ja | 高于(无符号) | |
| jae | 高于等于(无符号) |
编者注(速记):有符号看
和 ,无符号看 。有符号的判据是" 与 是否一致"——因为溢出会把结果的符号位翻转, 恰好说明"真实结果为负"。带"等于"的再或上一个 。
减法的 是借位(无符号 时 ),不是加法器的进位输出 ,两者是取反关系。推导见《ALU 与运算电路》。
asm
cmp eax, ebx
jle done ; 若 eax <= ebx,转移到 done
test eax, eax
jz xxxx ; 若 eax == 0,转移到 xxxxAT&T 格式 vs Intel 格式
| 区别 | AT&T | Intel |
|---|---|---|
| 操作数顺序 | 源, 目的 | 目的, 源 |
| 寄存器 | %eax | eax |
| 立即数 | $100 | 100 |
| 内存引用 | (%eax) | [eax] |
| 长度后缀 | movl(32位) / movw(16位) / movb(8位) | dword ptr / word ptr / byte ptr |
| 复杂寻址 | 8(%edx, %eax, 2) | [edx+eax*2+8] |
统考通常使用 Intel 格式。
对比示例:
| AT&T | Intel | 含义 |
|---|---|---|
movl $100, %eax | mov eax, 100 | |
movl %eax, (%ebx) | mov [ebx], eax | |
movl %eax, -8(%ebp) | mov [ebp-8], eax |
考点清单
- x86 有 8 个 32 位通用寄存器,EBP 为栈帧基址,ESP 为栈顶指针
- 两个操作数不能同时为内存
- lea 是取有效地址,不访存,常被用作加法/乘法
- call = push 返回地址 + jmp 目标;ret = pop 返回地址 + jmp
- cmp 和 test 只设标志位不保存结果,配合 jcondition 使用
- 有符号比较用 jg/jl/jge/jle,无符号比较用 ja/jb/jae/jbe
- 条件转移的标志条件:有符号看
与 是否一致,无符号看 ;带"等于"再或上 edx:eax= EDX 高 32 位 + EAX 低 32 位拼成 64 位;imul单操作数形式结果落这里,双操作数形式只留低 32 位- 栈向低地址增长:push / call 让 ESP
,pop / ret 让 ESP - 机器码字节数 = 指令长度,下一条地址 = 本条地址 + 该字节数
- 移位代替乘除 2 的幂只对整数成立,浮点要改阶码字段
- 工具链顺序:预处理
编译(出 .s)汇编(出 .o)链接(出可执行文件),每步输入是上步输出
教材出处
- 袁春风《计算机系统基础(第 2 版)》§3.1 程序转换概述:图 3.2(p97);§4.3 链接:图 4.16(p201)