Appearance
x86 汇编语言基础
2026 大纲 四(六)1 编译器、汇编器和链接器的基本概念。
四步里,只有第三步在产生机器码
从一份 .c 到一个能双击运行的文件,中间经过四个工具,很多人记得住这四个名字,却说不准机器码是在哪一步出现的。看一眼四步的产物就清楚了:
前两步的输出都还是人能读的文本,真正把助记符翻译成二进制的是汇编器。链接器的输入已经是机器码了,它一个字都不翻译,只做两件事:把各模块里互相引用的符号对上号(符号解析),把地址填成最终值(重定位)。
四步的顺序也不是可以随便调的,每一步的输入恰好是上一步的输出:预处理是纯文本替换,必须最先做;编译要做语法分析与代码生成,是最重的一步;汇编是助记符到二进制的机械翻译;链接要跨模块解析符号,必须等所有模块都编译完才可能开始。
弄清这条链的位置感之后,这一篇余下的部分处理的是它中间那一段产物——汇编代码本身长什么样:x86 有哪些寄存器、操作数怎么写、常用指令分几类、同一份机器码在 AT&T 与 Intel 两种格式下的写法差在哪。这些是读懂机器级表示那一篇里那些反汇编片段的前提。
一、从源程序到可执行文件:四个工具
汇编代码是编译器的输出、汇编器的输入。四步流水线,每一步的输入必须是上一步的输出,顺序不能换:
| 工具 | 输入 | 输出 |
|---|---|---|
| 预处理器(cpp) | 源程序 .c | 展开宏与 #include 之后的 .i |
| 编译器(cc1) | 预处理后的源程序 | 汇编语言程序 .s |
| 汇编器(as) | 汇编语言程序 .s | 可重定位目标文件 .o(已是机器码) |
| 链接器(ld) | 若干 .o | 可执行目标文件 |
分四步各有理由:预处理是纯文本替换;编译要做语法分析与代码生成,是最重的一步;汇编是助记符到二进制的机械翻译;链接要跨模块解析符号并重定位地址,必须等所有模块都编译完才能做。

图 4.16 可重定位目标文件与静态库的链接
图里 main.c 经"翻译器(cpp、cc1、as)"变成 main.o,再和自定义静态库 mylib.a、标准库 libc.a 里取出的模块一起送进静态链接器 ld,产出可执行文件。
编者注(口径):另有一种更粗的两级模型——"编译程序:高级语言
机器目标代码",把编译和汇编合起来说,区别只在粒度。
三个名字长得像,指的却是三样东西:汇编程序(assembler)是把 .s 翻译成 .o 的那个工具;汇编语言程序是用汇编语言写的那份源代码;解释程序则既不是这两者中的任何一个——它逐句解释执行高级语言语句,根本不产生目标代码文件。
二、x86 通用寄存器
x86(IA-32)有 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 的定点寄存器组
位刻度说明子寄存器怎么嵌套,这是为兼容早期代码保留的。图下方还有 EIP(指令指针,即 PC)、EFLAGS(CF/ZF/SF/OF 住在里面)与 6 个段寄存器。
嵌套关系要读准一件事:写子寄存器只改自己那几位,其余位原封不动。mov al, 5 只改 EAX 的最低字节,高 24 位保持原值;mov ax, 5 只改低 16 位。把它当成"给 EAX 赋值 5",追踪寄存器内容时会整段错下去。
三、操作数与长度前缀
操作数分三类:<reg> 寄存器、<mem> 内存地址(如 [ebp-8]、[eax+ebx*4+8])、<con> 立即数。内存操作数只给地址、没说按几个字节访问,所以要加长度前缀:
| 前缀 | 访问宽度 |
|---|---|
byte ptr | 1 字节 |
word ptr | 2 字节 |
dword ptr | 4 字节(双字) |
qword ptr | 8 字节 |
cmp dword ptr [ebp-0Ch], ecx 即"把 [ebp-0Ch] 处的 4 个字节和 ecx 比较"。另一个操作数是寄存器时宽度已定,前缀可省;只有两边都无法确定宽度时(如内存与立即数)才必需。
还有一条硬性限制:两个操作数不能同时是内存。原因在数据通路上——一个指令周期内只安排了一次数据访存,要在两个内存单元之间搬数据,必须先 mov 进寄存器再 mov 出去。看到一条汇编写着两个 [...],那一定是抄错了。
四、常用指令
下面一律用 Intel 格式:目的在前、源在后。
数据传送
| 指令 | 语法 | 功能 |
|---|---|---|
| mov | mov dst, src | 把 src 复制到 dst |
| push | push src | ESP |
| pop | pop dst | 从栈顶读出送 dst,ESP |
| lea | lea dst, [addr] | 把有效地址(而非该地址处的内容)送入 dst |
lea 值得单独说一句:它只算地址、不访存、也不改标志位,因此编译器常拿它当加法乘法用——lea eax, [edx+eax] 的效果与 add eax, edx 相同,却不会破坏后面还要用的标志位。对照着记最清楚:lea eax, [ebx] 得到的是 ebx 的值,mov eax, [ebx] 得到的是 M[ebx] 的内容。
六条传送指令的传送语句写法,以及栈为什么向低地址增长(对不上 ESP 该加还是该减、或想核对 lea 与 mov 的写法差别时展开)
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(只算地址,不访存)x86 的栈向低地址增长。栈底在高地址,压栈时栈顶指针往下走:
高地址 ┌──────────────┐
│ 已入栈数据 │
├──────────────┤
│ ← 压栈前 ESP │
├──────────────┤
│ 新压入的 4B │ push:ESP 先 −4,再写入
├──────────────┤
│ ← 压栈后 ESP │
低地址 └──────────────┘弹栈相反:先从 ESP 处读出,再 ESP call 压入返回地址、ret 弹出返回地址,同样各让 ESP 变化 4。
算术与逻辑运算
| 指令 | 功能 | 示例 |
|---|---|---|
| add / sub | 加 / 减,结果存回第一操作数 | add eax, 10 |
| inc / dec | 自增 / 自减 1 | inc eax |
| neg | 取负(补码) | neg eax |
| imul | 有符号乘法(两种形式,见下) | imul eax, ebx |
| idiv | 有符号除法,被除数为 edx:eax | idiv ecx |
| and / or / xor | 按位与 / 或 / 异或 | and eax, 0FH |
| not | 按位取反 | not eax |
| shl / shr | 逻辑左移 / 右移 | shl eax, 1 |
| sal / sar | 算术左移 / 右移 | sar eax, 2 |
乘除法为什么牵扯 edx:eax:imul 两种形式的结果去向与 cdq 的作用(算不清高低 32 位落在哪时展开)
edx:eax 是两个 32 位寄存器拼成的一个 64 位数,EDX 存高 32 位、EAX 存低 32 位。乘除法都要用到它。
| 形式 | 写法 | 结果去哪 |
|---|---|---|
| 单操作数 | imul ebx | 完整的 64 位积落在 edx:eax |
| 双 / 三操作数 | imul eax, ebx | 只保留低 32 位存回目的寄存器,高半截丢弃 |
道理很直接:
除法反过来:idiv ecx 用 edx:eax 这个 64 位数作被除数,商放 EAX、余数放 EDX。所以除法前常见一条 cdq 指令——把 EAX 的符号位扩展填满 EDX,先把被除数补成正确的 64 位。
编译器还常用 shl eax, 1 代替乘 2、sar eax, 2 代替除以 4,但这一招只对整数成立:IEEE 754 是"符号位
控制流
| 指令 | 功能 |
|---|---|
jmp label | 无条件转移到 label |
cmp a, b | 按 |
test a, b | 按 |
call label | 把返回地址压栈(ESP |
ret | 弹出返回地址(ESP |
条件转移分有符号、无符号两套,十条助记符背后其实只有两条规则:有符号看 SF 与 OF 是否一致(
有符号那条为什么不能简化成"只看 SF",值得多想一层:溢出会把符号位翻转。两个大正数相减若发生溢出,真实差值明明是正的,SF 却是 1;此时
| 助记符 | 含义 | 标志条件 |
|---|---|---|
| je / jz | 相等 | |
| jne / jnz | 不等 | |
| jl | 小于(有符号) | |
| jle | 小于等于(有符号) | |
| jg | 大于(有符号) | |
| jge | 大于等于(有符号) | |
| jb | 低于(无符号) | |
| jbe | 低于等于(无符号) | |
| ja | 高于(无符号) | |
| jae | 高于等于(无符号) |
asm
cmp eax, ebx
jle done ; 若 eax <= ebx(有符号),转移到 done
test eax, eax
jz zero_case ; 若 eax == 0,转移五、AT&T 格式与 Intel 格式
同一条机器指令的两套书写约定:
| 区别 | AT&T | Intel |
|---|---|---|
| 操作数顺序 | 源, 目的 | 目的, 源 |
| 寄存器 | %eax | eax |
| 立即数 | $100 | 100 |
| 内存引用 | (%eax) | [eax] |
| 长度标注 | 指令后缀 movl / movw / movb | 操作数前缀 dword ptr / word ptr / byte ptr |
| 复合寻址 | 8(%edx, %eax, 2) | [edx+eax*2+8] |
对照几条:
| AT&T | Intel | 含义 |
|---|---|---|
movl $100, %eax | mov eax, 100 | |
movl %eax, (%ebx) | mov [ebx], eax | |
movl %eax, -8(%ebp) | mov [ebp-8], eax |
两种格式描述的是完全相同的机器码,差别纯属书写习惯。最易错的是操作数顺序恰好相反:movl %eax, %ebx 的目的是 ebx,mov eax, ebx 的目的是 eax。
地址推算的数字锚点:11 字节的指令,下一条落在哪(想给速查最后一行的两条换算关系找个具体数时展开)
某指令位于 004010AE,机器码为 C7 84 82 00 20 42 00 0A 00 00 00,共 11 字节,那么下一条指令的地址就是
反过来,相邻两条指令地址相减,得到的就是前一条指令的长度——这是在只知道地址、看不到机器码时推算指令长度的办法。定长指令机器(如 MIPS)里这个差值恒为 4,变长指令机器里则参差不齐,这正是《CISC 与 RISC》最外显的一处差异。
考点速记
- 预处理
编译 汇编 链接,每步输入是上一步输出;汇编器才是产生机器码的那一步,链接器的输入已是机器码,只做符号解析与地址重定位。 - 8 个通用寄存器里只有 EBP、ESP 有专用功能;写
AL只改最低字节;两个操作数不能同时是内存,宽度不明时靠长度前缀补上;lea只算地址、不访存、不改标志位。 - 条件转移只有两条规则:有符号看 SF 与 OF 是否一致,无符号看 CF;AT&T 与 Intel 是同一份机器码的两种写法,操作数顺序恰好相反。
这一节在真题里被考过的形式:
- 问四步流水线的顺序(2022-20)。答案是预处理
编译 汇编 链接。四个选项都以预处理开头,区别全在后三步怎么排——判据是"每一步的输入必须是上一步的输出":编译的输入是预处理后的源程序,汇编的输入是汇编语言程序,链接的输入是可重定位目标文件。把汇编排在编译前面(错项 B、D)就等于说汇编器能读 C 源程序,把链接排在汇编前面(错项 C)则等于说链接器能处理还没变成机器码的东西。 - 问哪个程序把高级语言源程序转换为机器目标代码(2016-12)。答编译程序——这里用的是上面那条编者注说的两级粗口径(编译 = 高级语言
机器目标代码),而不是四步细口径。另三个选项恰好把三个易混对象摆齐了:汇编程序的输入是汇编语言而不是高级语言;链接程序的输入已经是目标代码,它不做翻译;解释程序逐句解释执行、根本不产生目标代码文件。答这类题先看题干问的是"输入是什么、输出是什么",按输入输出对号,不要按名字的字面意思猜。
复习优先级:这一篇的真题产出集中在第一节那四步流水线上,两道都是 1 分档的概念题。后面四节(寄存器、操作数、常用指令、两种汇编格式)本身不单独成题,它们的价值是让你读得懂机器级表示里那些反汇编片段——真正的分数在那一篇。
易错:以为链接器还要做翻译。它的输入已是机器码,只做符号解析与地址重定位。
易错:把汇编程序、汇编语言程序、解释程序三个名字混用。前者是工具,中间是源代码,后者不产生目标文件。
易错:把
mov al, 5当成给 EAX 赋值。它只改最低字节,高 24 位不动。
易错:把有符号比较的判据简化成"只看 SF"。溢出会翻转符号位,
与 是否一致才可靠。
教材出处
- 袁春风《计算机组成与系统结构(第 3 版)》§4.2.6 标志信息的生成与使用:表 4.1 IA-32 部分条件转移指令的转移条件,以及
反映"大于"关系、 说明有借位的推理(印刷页 p99) - 袁春风《计算机系统基础(第 2 版)》§3.1 程序转换概述:图 3.2 IA-32 的定点寄存器组(印刷页 p97);§4.3 链接:图 4.16 可重定位目标文件与静态库的链接(印刷页 p201)
相关知识
程序的机器级表示|计算机系统层次结构|ALU 与运算电路|CISC 与 RISC