Skip to content

x86 汇编语言基础

2026 大纲 四(六)1 编译器、汇编器和链接器的基本概念

四步里,只有第三步在产生机器码

从一份 .c 到一个能双击运行的文件,中间经过四个工具,很多人记得住这四个名字,却说不准机器码是在哪一步出现的。看一眼四步的产物就清楚了:

.c 预处理 .i (仍是文本) 编译 .s (汇编语言,仍是文本) 汇编 .o (机器码) 链接 可执行文件

前两步的输出都还是人能读的文本,真正把助记符翻译成二进制的是汇编器。链接器的输入已经是机器码了,它一个字都不翻译,只做两件事:把各模块里互相引用的符号对上号(符号解析),把地址填成最终值(重定位)。

四步的顺序也不是可以随便调的,每一步的输入恰好是上一步的输出:预处理是纯文本替换,必须最先做;编译要做语法分析与代码生成,是最重的一步;汇编是助记符到二进制的机械翻译;链接要跨模块解析符号,必须等所有模块都编译完才可能开始

弄清这条链的位置感之后,这一篇余下的部分处理的是它中间那一段产物——汇编代码本身长什么样:x86 有哪些寄存器、操作数怎么写、常用指令分几类、同一份机器码在 AT&T 与 Intel 两种格式下的写法差在哪。这些是读懂机器级表示那一篇里那些反汇编片段的前提。

一、从源程序到可执行文件:四个工具

汇编代码是编译器的输出、汇编器的输入。四步流水线,每一步的输入必须是上一步的输出,顺序不能换

工具输入输出
预处理器(cpp)源程序 .c展开宏与 #include 之后的 .i
编译器(cc1)预处理后的源程序汇编语言程序 .s
汇编器(as)汇编语言程序 .s可重定位目标文件 .o(已是机器码)
链接器(ld)若干 .o + 库文件可执行目标文件

分四步各有理由:预处理是纯文本替换;编译要做语法分析与代码生成,是最重的一步;汇编是助记符到二进制的机械翻译;链接要跨模块解析符号并重定位地址,必须等所有模块都编译完才能做

图 4.16 可重定位目标文件与静态库的链接

图 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 位习惯用途
EAXAXAH / AL累加器,函数返回值
EBXBXBH / BL基址寄存器
ECXCXCH / CL计数器(循环、移位)
EDXDXDH / DL数据寄存器,乘除法的高半部分
ESISI源变址寄存器
EDIDI目的变址寄存器
EBPBP基址指针(栈帧底部)
ESPSP栈指针(栈顶)

名字里的用途是历史习惯而非硬性限制——除 EBP、ESP 外其余可自由使用;习惯还活着,是因为乘除法、串操作等指令确实隐含指定了特定寄存器。

图 3.2 IA-32 的定点寄存器组

图 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 ptr1 字节
word ptr2 字节
dword ptr4 字节(双字)
qword ptr8 字节

cmp dword ptr [ebp-0Ch], ecx 即"把 [ebp-0Ch] 处的 4 个字节ecx 比较"。另一个操作数是寄存器时宽度已定,前缀可省;只有两边都无法确定宽度时(如内存与立即数)才必需。

还有一条硬性限制:两个操作数不能同时是内存。原因在数据通路上——一个指令周期内只安排了一次数据访存,要在两个内存单元之间搬数据,必须先 mov 进寄存器再 mov 出去。看到一条汇编写着两个 [...],那一定是抄错了。

四、常用指令

下面一律用 Intel 格式:目的在前、源在后。

数据传送

指令语法功能
movmov dst, src把 src 复制到 dst
pushpush srcESP 4,再把 src 写入栈顶
poppop dst从栈顶读出送 dst,ESP +4
lealea 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 +4call 压入返回地址、ret 弹出返回地址,同样各让 ESP 变化 4。

算术与逻辑运算

指令功能示例
add / sub加 / 减,结果存回第一操作数add eax, 10
inc / dec自增 / 自减 1inc eax
neg取负(补码)neg eax
imul有符号乘法(两种形式,见下)imul eax, ebx
idiv有符号除法,被除数为 edx:eaxidiv 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 位存回目的寄存器,高半截丢弃

道理很直接:n× n 位最多产生 2n 位,硬件永远算满 64 位,只是双操作数形式把高 32 位扔了。扔掉的部分是否含有效信息,由 OF/CF 报告——当高 32 位不是低 32 位符号位的符号扩展时(即结果放不进 32 位),OF 与 CF 被置 1。

除法反过来:idiv ecxedx:eax 这个 64 位数作被除数,商放 EAX、余数放 EDX。所以除法前常见一条 cdq 指令——把 EAX 的符号位扩展填满 EDX,先把被除数补成正确的 64 位。

编译器还常用 shl eax, 1 代替乘 2、sar eax, 2 代替除以 4,但这一招只对整数成立:IEEE 754 是"符号位 + 阶码 + 尾数"的三段结构,整体移位会跨越字段边界,把阶码的低位挤进尾数——浮点数乘 2 要改的是阶码字段,不是整体左移。

控制流

指令功能
jmp label无条件转移到 label
cmp a, babZF/SF/CF/OF 四位,不保存差值
test a, ba AND b 置 ZF/SF(CF、OF 清零),不保存结果
call label把返回地址压栈(ESP 4),转移到 label
ret弹出返回地址(ESP +4),转移回调用点

条件转移分有符号、无符号两套,十条助记符背后其实只有两条规则:有符号看 SF 与 OF 是否一致SF=OF 即"大于等于"),无符号看 CF(减法里的借位,有借位说明被减数小);带"等于"的再并上 ZF=1

有符号那条为什么不能简化成"只看 SF",值得多想一层:溢出会把符号位翻转。两个大正数相减若发生溢出,真实差值明明是正的,SF 却是 1;此时 OF=1SFOF 恰好把这种情况排除掉。所以 SFOF 是否一致才是可靠判据。另一处口径是 减法里的 CF 是借位,它与加法器的进位输出 Cout取反关系,不是同一个量——ab 在机器里做的是 a+b+1,加法器不产生进位(Cout=0)才意味着"不够减"、要借位(CF=1)。

助记符含义标志条件
je / jz相等ZF=1
jne / jnz不等ZF=0
jl小于(有符号)SFOFZF=0
jle小于等于(有符号)SFOFZF=1
jg大于(有符号)SF=OFZF=0
jge大于等于(有符号)SF=OFZF=1
jb低于(无符号)CF=1ZF=0
jbe低于等于(无符号)CF=1ZF=1
ja高于(无符号)CF=0ZF=0
jae高于等于(无符号)CF=0ZF=1
asm
cmp  eax, ebx
jle  done        ; 若 eax <= ebx(有符号),转移到 done
test eax, eax
jz   zero_case   ; 若 eax == 0,转移

五、AT&T 格式与 Intel 格式

同一条机器指令的两套书写约定:

区别AT&TIntel
操作数顺序源, 目的目的, 源
寄存器%eaxeax
立即数$100100
内存引用(%eax)[eax]
长度标注指令后缀 movl / movw / movb操作数前缀 dword ptr / word ptr / byte ptr
复合寻址8(%edx, %eax, 2)[edx+eax*2+8]

对照几条:

AT&TIntel含义
movl $100, %eaxmov eax, 100100R[eax]
movl %eax, (%ebx)mov [ebx], eaxR[eax]M[R[ebx]]
movl %eax, -8(%ebp)mov [ebp-8], eaxR[eax]M[R[ebp]8]

两种格式描述的是完全相同的机器码,差别纯属书写习惯。最易错的是操作数顺序恰好相反:movl %eax, %ebx 的目的是 ebxmov eax, ebx 的目的是 eax

地址推算的数字锚点:11 字节的指令,下一条落在哪(想给速查最后一行的两条换算关系找个具体数时展开)

某指令位于 004010AE,机器码为 C7 84 82 00 20 42 00 0A 00 00 00,共 11 字节,那么下一条指令的地址就是

004010AE+11=004010B9

反过来,相邻两条指令地址相减,得到的就是前一条指令的长度——这是在只知道地址、看不到机器码时推算指令长度的办法。定长指令机器(如 MIPS)里这个差值恒为 4,变长指令机器里则参差不齐,这正是《CISC 与 RISC》最外显的一处差异。

考点速记

  1. 预处理 编译 汇编 链接,每步输入是上一步输出;汇编器才是产生机器码的那一步,链接器的输入已是机器码,只做符号解析与地址重定位。
  2. 8 个通用寄存器里只有 EBP、ESP 有专用功能;写 AL 只改最低字节;两个操作数不能同时是内存,宽度不明时靠长度前缀补上;lea 只算地址、不访存、不改标志位。
  3. 条件转移只有两条规则:有符号看 SF 与 OF 是否一致,无符号看 CF;AT&T 与 Intel 是同一份机器码的两种写法,操作数顺序恰好相反

这一节在真题里被考过的形式

  • 问四步流水线的顺序(2022-20)。答案是预处理 编译 汇编 链接。四个选项都以预处理开头,区别全在后三步怎么排——判据是"每一步的输入必须是上一步的输出":编译的输入是预处理后的源程序,汇编的输入是汇编语言程序,链接的输入是可重定位目标文件。把汇编排在编译前面(错项 B、D)就等于说汇编器能读 C 源程序,把链接排在汇编前面(错项 C)则等于说链接器能处理还没变成机器码的东西。
  • 问哪个程序把高级语言源程序转换为机器目标代码(2016-12)。答编译程序——这里用的是上面那条编者注说的两级粗口径(编译 = 高级语言 机器目标代码),而不是四步细口径。另三个选项恰好把三个易混对象摆齐了:汇编程序的输入是汇编语言而不是高级语言;链接程序的输入已经是目标代码,它不做翻译;解释程序逐句解释执行、根本不产生目标代码文件。答这类题先看题干问的是"输入是什么、输出是什么",按输入输出对号,不要按名字的字面意思猜。

复习优先级:这一篇的真题产出集中在第一节那四步流水线上,两道都是 1 分档的概念题。后面四节(寄存器、操作数、常用指令、两种汇编格式)本身不单独成题,它们的价值是让你读得懂机器级表示里那些反汇编片段——真正的分数在那一篇。

易错:以为链接器还要做翻译。它的输入已是机器码,只做符号解析与地址重定位。

易错:把汇编程序、汇编语言程序、解释程序三个名字混用。前者是工具,中间是源代码,后者不产生目标文件。

易错:把 mov al, 5 当成给 EAX 赋值。它只改最低字节,高 24 位不动。

易错:把有符号比较的判据简化成"只看 SF"。溢出会翻转符号位,SFOF 是否一致才可靠。

教材出处
  • 袁春风《计算机组成与系统结构(第 3 版)》§4.2.6 标志信息的生成与使用:表 4.1 IA-32 部分条件转移指令的转移条件,以及 SF=OF 反映"大于"关系、CF=1 说明有借位的推理(印刷页 p99)
  • 袁春风《计算机系统基础(第 2 版)》§3.1 程序转换概述:图 3.2 IA-32 的定点寄存器组(印刷页 p97);§4.3 链接:图 4.16 可重定位目标文件与静态库的链接(印刷页 p201)

相关知识

程序的机器级表示计算机系统层次结构ALU 与运算电路CISC 与 RISC

真题练习