Assembly
汇编语言的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 |
还有一个特殊的指令
movzbw movzbl movzbq movzwl movzwq |
事实上没有
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位决定的,这里
异或运算
常常用
特殊的算术操作
| 指令 | 效果 | 描述 |
|---|---|---|
| 有符号全乘法 | ||
| 无符号全乘法 | ||
| 转换为八字 | ||
| 有符号除法 | ||
| 无符号除法 |
对于有符号除法通常使用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)是直接写在指令里的常量(比如
立即数的 “宽度限制”
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位寄存器有三种方式:
- 移动到32位低位:
B8 +rd id,5字节 示例:mov eax,241/mov[l] $241,%eax将值移动到32位寄存器的低32位会将高32位清零 - 使用64位立即数进行移动:
48 B8 +rd io,10字节 示例:mov rax,0xf1f1f1f1f1f1f1f1/mov[abs][q] $0xf1f1f1f1f1f1f1f1,%rax移动一个完整的64位立即数 - 使用符号扩展的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位 |
符号拓展
movq $0xFFFFFFFE, %rax ; 7字节编码:32位立即数0xFFFFFFFE的最高位是1,符号扩展后高32位填1 |
movq $0x12345678, (%rdi) ; 7字节编码:将32位立即数符号扩展为64位,写入%rdi指向的8字节内存 |
场景 2:立即数超过 32 位
movabsq $0x123456789ABCDEF0, %rax ; 10字节编码:直接加载64位立即数,无扩展 |
先加载到寄存器(movabsq),再用 movq 间接写内存。例子:
目标:将64位值0x123456789ABCDEF0写入%rdi指向的8字节内存 |
场景 3:操作 64 位绝对地址内存(比如内核地址 0xFFFF800000001234)
用 movabsq %r64, mem64直接写 64 位绝对地址,无需寄存器间接寻址
movabsq $0x123456789ABCDEF0, %rax ; 加载数据到%rax |
为什么不用 movq?movq %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 |




