常用汇编指令介绍
4.1 和 4.2 讲的是”指令这个东西长什么样”,4.3 换成”一段 C 程序落到指令上是什么样”。这一节先把工具铺开——寄存器、两种汇编书写格式、常用指令——后面三节才拿这些工具去翻译 if、for 和函数调用。
统考不要求写汇编,但要求读汇编:给一段代码,判断它在干什么、访存几次、哪条指令改了哪个标志位。所以这一节的目标不是记忆助记符表,而是建立”看到一条指令就能判断它属于 4.1.5 哪一类、动没动主存、动没动标志位”的反射。
机制
x86 的寄存器:名字的三层沿革
| 64 位 | 32 位 | 16 位 | 8 位 | 惯用途 |
|---|---|---|---|---|
RAX | EAX | AX | AL/AH | 累加器、返回值 |
RBX | EBX | BX | BL/BH | 基址 |
RCX | ECX | CX | CL/CH | 计数 |
RDX | EDX | DX | DL/DH | 数据、乘除法高位 |
RSI | ESI | SI | SIL | 源变址 |
RDI | EDI | DI | DIL | 目的变址 |
RBP | EBP | BP | BPL | 栈帧基址 |
RSP | ESP | SP | SPL | 栈顶指针 |
R8~R15 | R8D~ | R8W~ | R8B~ | x86-64 新增 |
E 是 Extended(16→32),R 是 Register(32→64)。写 EAX 就是 RAX 的低 32 位——它们不是两个寄存器,是同一个寄存器的两种宽度视图。这一条在读真题的位数陷阱时用得上。
“惯用途”是 ABI 层的约定,不是硬件强制的(见 4.1.1)。硬件不会阻止你拿 ESP 做算术,只是这么干会踩坏栈。
两种书写格式:读题时先认格式
同一条机器指令,两派汇编器写法完全不同。408 真题两种都出现过,所以判据必须清楚。
| Intel 格式(王道主用) | AT&T 格式(GCC 默认) | |
|---|---|---|
| 操作数顺序 | mov dst, src | mov src, dst 反的 |
| 寄存器 | eax | %eax |
| 立即数 | 8 | $8 |
| 内存操作数 | [ebp+8] | 8(%ebp) |
| 复杂寻址 | [eax+ebx*4+8] | 8(%eax,%ebx,4) |
| 操作数长度 | dword ptr [ebp] | 靠后缀 movl / movw / movb / movq |
一眼认格式的判据:有没有 % 和 $。 有就是 AT&T,那么第一个操作数是源;没有就是 Intel,第一个操作数是目的。把方向看反,整道题就全错了,这是读汇编题的第一道关。
两种格式汇编出来的机器码完全相同——书写格式属于汇编器约定,不属于 ISA(4.1.1 已辨析)。
常用指令:按 4.1.5 的五类归位
| 类 | 指令 | 说明 | 访存 | 改标志位 |
|---|---|---|---|---|
| 传送 | mov | 搬运 | 看操作数 | ❌ |
movz / movs | 零扩展 / 符号扩展后搬 | 看操作数 | ❌ | |
push / pop | 压栈 / 弹栈(自动改 SP) | ✅ | ❌ | |
lea | 取有效地址,见下节 | ❌ 从不访存 | ❌ | |
xchg | 交换 | 看操作数 | ❌ | |
| 算术 | add sub | 加减 | 看操作数 | ✅ |
inc dec | 加 1 / 减 1 | 看操作数 | ✅(不改 CF) | |
neg | 取负(求补) | 看操作数 | ✅ | |
imul / mul | 有符号 / 无符号乘 | 看操作数 | ✅ | |
idiv / div | 有符号 / 无符号除 | 看操作数 | ✅ | |
| 逻辑 | and or xor not | 按位运算 | 看操作数 | ✅(not 不改) |
| 移位 | shl/sal shr sar | 逻辑左/算术左、逻辑右、算术右 | 看操作数 | ✅ |
rol ror rcl rcr | 循环移位(后两个带 CF) | 看操作数 | ✅ | |
| 比较 | cmp | 做减法,只置标志位 | 看操作数 | ✅ |
test | 做与,只置标志位 | 看操作数 | ✅ | |
| 转移 | jmp jcc | 无条件 / 条件转移 | 取指 | ❌(读标志位) |
call ret | 调用 / 返回 | ✅(压/弹栈) | ❌ |
三条值得单记的细节:
shl和sal是同一条机器指令的两个助记符。 左移时逻辑移位和算术移位的行为完全一样(低位都补 0),所以硬件只需要一条;右移才必须分家(shr补 0,sar补符号位)。这一点在 2.2.2 已经推过,这里是它在真实 ISA 上的印证。inc/dec不改 CF,add/sub改。 这是为了让它们能安全地用在多精度加法的循环里而不破坏进位链。not不改任何标志位,neg改。 因为not是纯按位取反,neg是的减法。
四个标志位:谁产生,谁消费
| 标志 | 全称 | 什么时候置 1 | 服务于 |
|---|---|---|---|
| CF | Carry Flag | 无符号运算进位/借位 | 无符号比较 |
| OF | Overflow Flag | 有符号运算溢出 | 有符号比较 |
| ZF | Zero Flag | 结果为 0 | 相等判断 |
| SF | Sign Flag | 结果最高位为 1(负) | 有符号比较 |
同一次减法同时置好这四位,是”同一条 cmp 能同时支持有符号和无符号比较”的原因——区别只在随后的 Jcc 去读哪几位。完整的对应表在 4.3.2。
CF 和 OF 的区分,就是 2.2.3 里”无符号溢出”和”有符号溢出”那两个判据,硬件把两个判断电路都做了,把两个结果都存下来,让软件自己挑要哪个。“硬件不做选择,硬件提供全部选项”——这是理解标志位设计的关键一句。
LEA:借用内存操作数的语法写算术
lea 是全章最值得单独拿出来讲的一条指令。
语义只有一句:把有效地址本身送进寄存器,不去那个地址取数。
mov eax, [ebx+8] ; eax ← 主存[ebx+8] 里的内容 ★访存
lea eax, [ebx+8] ; eax ← ebx+8 这个地址值本身 ★不访存
方括号在 lea 里不代表访存,它只是”借用内存操作数的书写语法来表达一个地址表达式”。这是最容易看错的一点,也是统考唯一考的一点。
疑问点:汇编实验里有个优化,
LEA计算有效地址比 ALU 快,可以用它做一些运算“能拿来做运算”完全正确,而且这是它在真实编译输出里最常见的用法;“比 ALU 快”这个理由要换掉——赢的不是单条延迟,是指令条数和执行端口。
先说为什么能做运算。 x86 的内存操作数语法本身就是一个三输入表达式:
lea把这个表达式算出来就交差,于是一条指令顶一串:lea eax, [ebx + ecx*4 + 8] ; eax = ebx + 4*ecx + 8用常规算术指令要四条:
mov eax, ecx shl eax, 2 add eax, ebx add eax, 8再说三个真正的优势(每一条都比”快”这个理由更硬):
优势 说明 一条顶四条 少取指、少占译码带宽、代码密度高 不影响标志位 可以插在 cmp和Jcc之间而不破坏条件等效三操作数 lea eax,[ebx+ecx]不覆盖ebx也不覆盖ecx;而add ebx,ecx是二地址指令,结果必然覆盖第一个操作数(4.1.2)。lea相当于在二地址的 ISA 里偷到了一条三地址指令。最后修正”更快”。
lea走的是地址生成单元 AGU,不是 ALU:
- 它不占 ALU 的执行端口,因此可以和真正的算术指令同一拍并行发射——赢的是吞吐
- 但单条延迟并不占优:简单形式(两输入)延迟 1 周期,和
add一样;复杂形式(三输入base+index*s+disp)在多数微架构上反而是 3 周期所以准确的说法是:
lea赢在指令条数、执行端口和不碰标志位;不赢在单条指令的延迟。 这几条全部属于微体系结构层,统考不考,但知道了就不会把”更快”这个错理由写进大题。顺带一提:
lea是 CISC 设计的一个正面例证——一条复杂指令确实换来了实实在在的代码密度。这条会在 4.4.1 用到。
边界
lea 从不访存。 这是它和 mov 的唯一区别,也是它唯一的考点。判断题若说”lea 需要一次访存取出有效地址处的内容”,错。
操作数顺序看格式,不看直觉。 Intel 的 mov eax, ebx 是 eax ← ebx;AT&T 的 mov %eax, %ebx 是 ebx ← eax。同样的字符串,方向相反。
cmp 和 sub 的差别只在写不写回。 两者置的标志位完全一样。所以 cmp a,b 之后再看标志位,等价于”算了
x86 不允许两个操作数同时是主存。 mov [eax],[ebx] 是非法的,必须借道寄存器。这是 CISC 也要守的一条限制,因为一条指令里只有一套地址生成通路。RISC 更严格:除 load/store 外没有任何指令能碰主存(4.4.2)。
imul 的单操作数形式和双操作数形式结果去向不同。 单操作数 imul ebx 把 64 位积放进 EDX:EAX;双操作数 imul eax, ebx 只保留低 32 位放进 eax。统考出现的是后者,但看到 EDX:EAX 要知道是前者。
movz 与 movs 的差别是2.1.4 类型转换那一套。 零扩展用于无符号数,符号扩展用于有符号数——扩展方式必须与原类型的符号性一致,否则值会变。这是 C 语言隐式类型转换 bug 在机器级的样子。
统考不要求背助记符,要求读。 遇到没见过的助记符,用四个问题定位它:属于哪一类?访存吗?改标志位吗?改 PC 吗? 这四问在 4.1.5 建立,在这里第一次实战。
对照速查
| 判断 | 对错 |
|---|---|
lea 要访存一次 | ❌(从不访存) |
lea 里的方括号表示取内容 | ❌(只是地址表达式的语法) |
lea 会改标志位 | ❌ |
lea 比 add 单条延迟更短 | ❌(相同或更长;赢在条数和端口) |
| Intel 格式第一个操作数是源 | ❌(是目的) |
| AT&T 格式第一个操作数是源 | ✅ |
| 两种格式生成的机器码不同 | ❌(完全相同) |
mov 会改标志位 | ❌ |
inc 会改 CF | ❌ |
cmp 会把结果写回目的操作数 | ❌ |
shl 和 sal 是同一条机器指令 | ✅ |
shr 和 sar 是同一条机器指令 | ❌(补 0 vs 补符号位) |
| x86 允许两个主存操作数 | ❌ |
EAX 和 RAX 是两个独立寄存器 | ❌(同一个的不同宽度视图) |
考点
lea取有效地址、不访存、不改标志位——本节第一考点lea能当算术用,因为内存操作数语法本身是;它等效于一条三地址指令 - Intel
mov dst,src/ AT&Tmov src,dst,靠%$认格式;机器码相同 - 传送类不改标志位(
movleapushpop);inc/dec不改 CF;not不改标志位 cmp= 减法不写回,test= 与不写回- CF 管无符号溢出,OF 管有符号溢出,硬件两个都算、软件自己挑
shl=sal是一条指令;shr与sar必须分家EAX是RAX的低 32 位;x86 不允许两个主存操作数
链接
- 🏠 返回总览:计算机组成原理第 4 章:指令系统总览
- ⬅️ 上一节:4.2.2 常见的数据寻址方式
- ➡️ 下一节:4.3.2 选择语句的机器级表示
- 🔗 2.2.2 定点数的移位运算(
shl/sar的原理) - 🔗 2.2.3 定点数的加减运算(CF 与 OF 两个溢出判据)
- 🔗 2.1.4 C 语言中的整数类型及类型转换(
movz/movs) - 🔗 4.4.1 复杂指令系统计算机(CISC)(
lea是 CISC 的正面例证) - 📖 名词库:第 4 章名词库
- 📝 原始提问:#32 / #33(本地资料)