汇编语言的AT&T和Intel格式有下面不同:

  • Intel代码省略指示大小的后缀
  • Intel代码省略寄存器前的%
  • Intel代码用不同方式描述内存中的位置
  • 在带有多个操作数的指令下,列出操作数的顺序相反

下面我们主要讲的是ATT(AT&T)语法

通用目的寄存器

63~0 31~0 15~0 7~0
rax eax ax al
rbx ebx bx bl
rcx ecx cx cl
rdx edx dx cl
rsi esi si sil
rdi edi di dil
rbp ebp bp bpl
rsp esp sp spl
r8 r8d r8w r8b
r9 r9d r9w r9b
r10 r10d r10w r10b
r11 r11d r11w r11b
r12 r12d r12w r12b
r13 r13d r13w r13b
r14 r14d r14w r14b
r15 r15d r15w r15b

操作数指示符(内存寻址表达式)

类型 格式 操作数值 名称
立即数 立即数寻址
寄存器 寄存器寻址
存储器 绝对寻址
存储器 间接寻址
存储器 偏移量寻址
存储器 变址寻址
存储器 变址寻址
存储器 比例变址寻址
存储器 比例变址寻址
存储器 比例变址寻址
存储器 比例变址寻址

后缀

C声明 Intel数据类型 汇编代码后缀 字节大小
char 字节 b 1
short w 2
int 双字 l 4
long 四字 q 8
char* 四字 q 8
float 单精度 s 4
double 双精度 l 8

为了后面述说方便,把此类后缀统称为

mov系

:可以操作的特定数据大小

movb movw movl movq movabsq

还有一个特殊的指令 ,目的只能是寄存器,表示传送绝对的四字

:从较小的源值(bytes1)传送到较大的目的(bytes2)时,将剩余的字节填充为0,称为零扩展

movzbw movzbl movzbq movzwl movzwq

事实上没有,使用来替代,因为任何为寄存器生成32位值的指令都会把该寄存器的高位部分置为0

:从较小的源值(bytes1)传送到较大的目的(bytes2)时,将剩余的字节填充为源值的符号位(最高位),称为符号扩展

movsbw movsbl movsbq movswl movswq cltq

其中是一条特殊的指令,没有操作数,仅用于将的符号扩展到

关于操作数的约定

,其中为源操作数,为目的操作数

源操作数指定的类型是立即数、寄存器或内存,目的操作数指定一个位置,是一个寄存器或是一个内存地址

,其中为源操作数,为目的操作数

源操作数指定的类型是寄存器或内存,目的操作数指定一个位置,是一个寄存器

,其中为源操作数,为目的操作数

源操作数指定的类型是寄存器或内存,目的操作数指定一个位置,是一个寄存器

在x86-64中,两个操作数不能都指向内存地址,实现一个值从内存地址复制到另一个内存地址需要先加载到寄存器中,再从寄存器中加载到内存地址中

movq 和 movabsq 的区别

movq指令只能以表示为32位补码数字的立即数作为源操作数,然后把这个值符号扩展得到64位的值

movabsq指令能够以任意64位立即数值作为源操作数,并且只能以寄存器作为目的

关于这个的详细讨论篇幅过大,为了不影响文章的观感,我将这部分的详细内容放在文末

pop、push系

指令 效果 等价 描述


将四字压入栈


将四字弹出栈

算术和逻辑操作

指令 效果 描述
加载有效地址
加1
减1
取负
取反
异或
左移
左移
算数右移
逻辑右移

lea系

是mov系的变形

源操作数必须是合法内存寻址表达式,目的操作数必须是寄存器

常用于计算目的地址和简单算数计算

移位运算

移位量可以是一个立即数,或者放在单字节寄存器%cl中

移位操作对w位长的数据值进行操作,移位量由%cl寄存器的低m位决定的,这里

异或运算

常常用表示赋值为0,初始化等,并且会清除标志位

特殊的算术操作

指令 效果 描述
有符号全乘法
无符号全乘法
转换为八字

有符号除法

无符号除法

对于有符号除法通常使用cqto实现符号拓展,无符号除法通常使用异或将RDX置0

控制条件码

条件码 描述
CF 进位标志,最近的操作使最高位产生了进位则置1
ZF 零标志,最近的操作得到0则置1
SF 符号标志,最近的操作得到负数则置1
OF 溢出标志,最近操作导致一个补码溢出则置1

算术和逻辑运算中除了leaq不改变任何条件码,其他都会设置条件码

  • XOR设置CF、ZF为0
  • 移位操作设置CF为最后一个移出的位,OF设置为0
  • INC、DEC会设置ZF、OF,但不会设置CF

CMP、TEST系

指令 基于 描述
比较
比较字节
比较字
比较双字
比较四字
测试
测试字节
测试字
测试双字
测试四字

cmp、test系只会设置条件码而不更新目的寄存器

SET系

set系指令是一种根据条件码组合将字节设置为0或1的一套指令

其后缀不再是表示操作数大小而是操作条件,目的操作数是低位单字节寄存器元素之一或是一个字节的内存位置

指令 同义名 效果 设置条件
相等/零
不等/非零
负数
非负数
大于
大于等于
小于
小于等于
超过(无符号)
超过或等于(无符号)
低于(无符号)
| 低于或等于(无符号)

JMP系

指令 同义名 跳转条件 描述
直接跳转
间接跳转
相等/零
不相等/非零
负数
非负数
大于(有符号)
大于或等于(有符号)
小于(有符号)
小于或等于(有符号)
大于(无符号)
大于或等于(无符号)
小于(无符号)
小于或等于(无符号)

mov的正确使用注意点

立即数(immediate)是直接写在指令里的常量(比如-456),不是寄存器或内存里的值

立即数的 “宽度限制”

CPU 指令的编码空间有限,不能无限制支持任意宽度的立即数:

  • 最常用的是 32 位立即数(imm32):占 4 字节,编码紧凑(指令短,执行快),覆盖绝大多数场景(用户态地址、常见常量都在 32 位范围内)
  • 特殊场景支持 64 位立即数(imm64):占 8 字节,编码长(指令长,执行稍慢),仅用于超过 32 位的常量(如内核地址、大数值)

扩展规则的使用

x86-64 为了兼容 32 位程序,规定了两种 “32 位值扩展到 64 位” 的规则

  • 零扩展(Zero-Extend):高 32 位全部填充 0

    触发条件:对32 位寄存器执行写操作(如),硬件自动将对应的 64 位寄存器(%rax)高 32 位清 0

  • 符号扩展(Sign-Extend):高 32 位填充为32 位立即数的最高位(符号位)

    触发条件:使用movq的 7 字节编码(movq  r64, sign − ext − imm32),32 位立即数的最高位是1(负数)则高 32 位填1,是0(正数)则填0

指令编码格式的 “绑定规则”

指令 绑定的编码格式 立即数宽度 扩展规则 编码长度 目的地限制
(寄存器)
(内存)
32 位 零扩展(寄存器) 5 字节(寄存器)
≥6 字节(内存)
32 位寄存器、32 位内存
(寄存器 / 内存)
(仅寄存器)
①32 位
②64 位
①符号扩展
②无扩展
①7 字节
②10 字节
①64 位寄存器 / 64 位内存
②仅 64 位寄存器
(寄存器) 64 位 无扩展 10 字节(寄存器) 64 位寄存器

关键:指令后缀(l/q)和助记符(movabsq)直接锁定编码格式,汇编器不会自动转换(比如写 movq 就不会用 movl 的 5 字节编码)

movabsq和movq的第二种用法其实是等价的,机器会通过判断立即数来选择编码格式,来达到减小编码长度的目的

填充64位寄存器有三种方式:

  1. 移动到32位低位B8 +rd id,5字节 示例:mov eax,241 / mov[l] $241,%eax 将值移动到32位寄存器的低32位会将高32位清零
  2. 使用64位立即数进行移动48 B8 +rd io,10字节 示例:mov rax,0xf1f1f1f1f1f1f1f1 / mov[abs][q] $0xf1f1f1f1f1f1f1f1,%rax 移动一个完整的64位立即数
  3. 使用符号扩展的32位立即数进行移动48 C7 /0 id,7字节 示例:mov rax,0xffffffffffffffff / mov[q] $0xffffffffffffffff,%rax 将带符号的32位立即数移动到完整的64位寄存器中

对于每个立即值,我们有:

  • [0, 0x7fff_ffff]中的值可以使用(1),(2)和(3)进行编码
  • [0x8000_0000, 0xffff_ffff]中的值可以使用(1)和(2)进行编码
  • [0x1_0000_0000, 0xffff_ffff_7fff_ffff]中的值可以使用(2)进行编码
  • [0xffff_ffff_8000_0000, 0xffff_ffff_ffff_ffff]中的值可以使用(2)和(3)进行编码

为什么没有 “mov m64, imm64”(不能直接把 64 位立即数写内存)

硬件设计的权衡:64 位立即数占 8 字节,加上操作码和地址字段,指令会非常长(10 字节以上),而 “64 位立即数写内存” 的场景极少,大部分可用 “寄存器中转” 替代,CPU 厂商没有为这种小众场景设计指令 —— 所以若要写 64 位立即数到内存,必须先加载到寄存器(movabsq),再间接写内存(movq)

AT&T 语法 MOV 指令集表格

Opcode AT&T 指令 Op/En(操作数类型) Compat/Leg Mode(兼容 / 实模式) Description
88 /r MR
源 = 寄存器
目的 = 寄存器 / 内存
Valid 8 位寄存器 → 8 位寄存器 / 内存
REX + 88 /r MR N.E. 8 位扩展寄存器 → 8 位扩展寄存器 / 内存
89 /r MR Valid 16 位寄存器 → 16 位寄存器 / 内存
89 /r MR Valid 32 位寄存器 → 32 位寄存器 / 内存
REX.W + 89 /r MR N.E. 64 位寄存器 → 64 位寄存器 / 内存
8A /r RM
源 = 寄存器 / 内存
目的 = 寄存器
Valid 8 位寄存器 / 内存 → 8 位寄存器
REX + 8A /r RM N.E. 8 位扩展寄存器 / 内存 → 8 位扩展寄存器
8B /r RM Valid 16 位寄存器 / 内存 → 16 位寄存器
8B /r RM Valid 32 位寄存器 / 内存 → 32 位寄存器
REX.W + 8B /r RM N.E. 64 位寄存器 / 内存 → 64 位寄存器
8C /r MR Valid 段寄存器 → 16 位寄存器 / 内存
8C /r $movl%sreg, %r32\  < br> movw%sreg, %r16/%r/m16$ MR Valid 16 位段寄存器零扩展 → 32 位寄存器/ 16 位寄存器 / 内存
REX.W + 8C /r $movq%sreg, %r64\  < br> movw%sreg, %r/m16$ MR Valid 16 位段寄存器零扩展 → 64 位寄存器/ 16 位内存
8E /r RM Valid 16 位寄存器 / 内存 → 段寄存器
REX.W + 8E /r RM Valid 64 位寄存器 / 内存的低 16 位 → 段寄存器
A0 FD
源 = 绝对地址内存
目的 = 固定寄存器
Valid 绝对地址(段:偏移)的 8 位内存 → % al
REX.W + A0 FD N.E. 64 位绝对地址的 8 位内存 → % al
A1 FD Valid 绝对地址(段:偏移)的 16 位内存 → % ax
A1 FD Valid 绝对地址(段:偏移)的 32 位内存 → % eax
REX.W + A1 FD N.E. 64 位绝对地址的 64 位内存 → % rax
A2 TD
源 = 固定寄存器
目的 = 绝对地址内存
Valid % al → 绝对地址(段:偏移)的 8 位内存
REX.W + A2 TD N.E. % al → 64 位绝对地址的 8 位内存
A3 TD Valid % ax → 绝对地址(段:偏移)的 16 位内存
A3 TD Valid % eax → 绝对地址(段:偏移)的 32 位内存
REX.W + A3 TD N.E. % rax → 64 位绝对地址的 64 位内存
B0+rb ib OI
源 = 立即数
目的 = 寄存器
Valid 8 位立即数 → 8 位寄存器
REX + B0+rb ib OI N.E. 8 位立即数 → 8 位扩展寄存器
B8+rw iw OI Valid 16 位立即数 → 16 位寄存器
B8+rd id OI Valid 32 位立即数 → 32 位寄存器,64 位模式下自动零扩展到 64 位
REX.W + B8+rd io OI N.E. 64 位立即数 → 64 位寄存器
C6 /0 ib MI
源 = 立即数
目的 = 寄存器 / 内存
Valid 8 位立即数 → 8 位寄存器 / 内存
REX + C6 /0 ib MI N.E. 8 位立即数 → 8 位扩展寄存器 / 内存
C7 /0 iw MI Valid 16 位立即数 → 16 位寄存器 / 内存
C7 /0 id MI Valid 32 位立即数 → 32 位寄存器 / 内存
REX.W + C7 /0 id MI N.E. 32 位立即数符号扩展到 64 位 → 64 位寄存器 / 内存

例子(此部分由AI生成)

场景 1:立即数在 32 位范围内

零拓展

movl $0x12345678, %eax   ; 5字节编码:%eax = 0x12345678,硬件自动清%rax高32位
→ %rax = 0x0000000012345678(正确)

movl $0xFFFFFFFF, %eax ; 5字节编码:%eax = 0xFFFFFFFF,高32位清0
→ %rax = 0x00000000FFFFFFFF(正确,这是之前想要的结果)

符号拓展

movq $0xFFFFFFFE, %rax   ; 7字节编码:32位立即数0xFFFFFFFE的最高位是1,符号扩展后高32位填1
→ %rax = 0xFFFFFFFFFFFFFFFFFE(64位 `-2`,符合需求)
movq $0x12345678, (%rdi)  ; 7字节编码:将32位立即数符号扩展为64位,写入%rdi指向的8字节内存
→ 内存内容:0x0000000012345678(正数扩展0)

movq $0xFFFFFFFE, (%rdi) ; 7字节编码:符号扩展后写入,内存内容:0xFFFFFFFFFFFFFFFFFE

场景 2:立即数超过 32 位

movabsq $0x123456789ABCDEF0, %rax  ; 10字节编码:直接加载64位立即数,无扩展
→ %rax = 0x123456789ABCDEF0(正确)

movabsq $0xFFFFFFFFFFFFFFFF, %rax ; 10字节编码:加载64位全1,即64位 `-1`
→ %rax = 0xFFFFFFFFFFFFFFFF(正确)

先加载到寄存器(movabsq),再用 movq 间接写内存。例子:

目标:将64位值0x123456789ABCDEF0写入%rdi指向的8字节内存
movabsq $0x123456789ABCDEF0, %rax ; 步骤1:加载64位立即数到%rax(10字节)
movq %rax, (%rdi) ; 步骤2:通过%rax间接写入内存(7字节)

场景 3:操作 64 位绝对地址内存(比如内核地址 0xFFFF800000001234

movabsq %r64, mem64直接写 64 位绝对地址,无需寄存器间接寻址

movabsq $0x123456789ABCDEF0, %rax  ; 加载数据到%rax
movabsq %rax, 0xFFFF800000001234 ; 直接将%rax的值写入64位绝对地址(10字节编码)

为什么不用 movqmovq %rax, 0xFFFF800000001234 不支持 64 位绝对地址,只能用 “基址 + 位移” 的间接寻址(如 movq %rax, 0x1234(%rbx)

场景 4:加载符号地址(比如变量 / 函数名 symbol

这是实际编程中最常用的场景,优先用 lea(RIP 相对寻址,高效且位置无关),特殊情况用 movabsq

lea msg(%rip), %rdi  ; 加载字符串msg的地址到%rdi,支持PIE(现代Linux默认),编码紧凑

特殊场景必须用绝对地址(如内核编程)

movabsq $kernel_var, %rdi  ; 加载内核变量kernel_var的绝对地址到%rdi