x86 与 x86-64 指令编码

最后更新: 2022-04-22 00:33:12 +0800 +0800
tags: Assembly | X86 | X86-64
原文以英文撰写。您正在阅读AI翻译版本。

x86 使用变长指令。一条指令可包含传统前缀、操作码、ModR/M 字节、比例—索引—基址(SIB)字节、位移量和立即数。只有操作码始终存在:

legacyprefixesopcodeModR/MSIBdisplacementimmediate

长模式可以在传统前缀和操作码之间添加 REX 前缀。对于较新的指令族,VEX、XOP 和 EVEX 编码会取代这一布局的部分字段。无论采用何种形式,体系结构层面的 x86 指令都不能超过 15 字节。

这些字段并非汇编语言词元的直接序列化结果。操作码可以编码操作数的一部分,ModR/M 字段可以扩展操作码,而地址表达式选择的是处理器的一种寻址形式,并非要求执行一系列加法和乘法。

操作与操作数之间没有固定边界 #

汇编语法用一条指令助记符加零个或多个操作数来表示指令:

nasm
xchg eax, ecx
inc  dword [eax]
mov  edx, [ecx + 0x32251212]

二进制编码不会为助记符预留一个字段、再为每个操作数分别预留字段。x86 使用多种紧凑的编码策略。

ModR/M 操作码扩展 #

许多指令使用三位 ModR/M reg 字段作为额外的操作码位。Intel 和 AMD 的语法将这些形式记为 /0 至 /7。例如,FF /0 是 INC r/m32,而 FF /1 是 DEC r/m32。

在 32 位模式下,这些指令编码为:

text
ff 05 00 00 00 00    inc dword [0]
ff 0d 00 00 00 00    dec dword [0]

两者都使用操作码字节 FF,但 ModR/M 字节不同:

text
05 = 00 000 101    mod=00, opcode extension=/0, r/m=101
0d = 00 001 101    mod=00, opcode extension=/1, r/m=101

在 32 位地址大小下,mod=00、r/m=101 表示只含位移量的地址。四个零字节就是该绝对 disp32。在这些编码中,reg 字段不能指定寄存器,因为它用于选择操作。

嵌入操作码的寄存器 #

某些操作码族将寄存器编号嵌入最终操作码字节的低三位。根据寄存器宽度,指令表将其记为 +rb、+rw、+rd 或 +rq。

例如,在 32 位模式下,单字节操作码 90 至 97 将 EAX 与八个 32 位通用寄存器之一交换:

汇编操作码
xchg eax, eax90
xchg eax, ecx91
xchg eax, edx92
xchg eax, ebx93
xchg eax, esp94
xchg eax, ebp95
xchg eax, esi96
xchg eax, edi97

指令表将这一族写作 90+rd。相比之下,xchg ebx, ecx 使用操作码 87 和一个 ModR/M 字节:

text
87 d9    xchg ebx, ecx

其他 +r 指令族包括 PUSH、POP、带立即数的 MOV 和 BSWAP。单字节 INC 和 DEC 寄存器形式仅在 64 位模式之外使用字节 40 至 4F。长模式将这些字节值分配给 REX 前缀。

汇编地址表达式选择编码字段 #

[eax + ebx*4 + 0x32] 这样的表达式不会编码算术指令。它要求汇编器选择 ModR/M、SIB 和位移量字段,使地址生成硬件计算该表达式。

例如:

text
ff 44 98 32    inc dword [eax + ebx*4 + 0x32]

这些字节可分解如下:

  • FF /0 选择 INC r/m32。
  • ModR/M 44 是 01 000 100:内存寻址、/0,后接 SIB。
  • SIB 98 是 10 011 000:比例因子 4、索引 EBX、基址 EAX。
  • 32 是 8 位位移量。

因此,同一个书写形式的助记符会根据操作数使用不同的长度和字段布局。

传统前缀 #

传统前缀分为四个语义组。一条指令最多可以从每组使用一个有效前缀。编码器不应发出同一组中的重复前缀:不同处理器对冗余或冲突前缀的行为并不一致。

组字节用途
1F0、F2、F3LOCK、重复或强制操作码选择
22E、36、3E、26、64、65段覆盖;历史上的分支提示
366操作数大小覆盖或强制操作码选择
467地址大小覆盖

汇编器使用约定的分组顺序,但传统解码并不要求所有组都按数字顺序出现。REX 前缀有所不同:如果存在,它必须位于传统前缀之后,并紧邻操作码或操作码映射转义字节之前。

锁定与重复前缀 #

F0 是 LOCK 前缀。它只适用于目标为内存的指定“读—修改—写”指令。相对于其他逻辑处理器,有效的锁定操作具有原子性。现代处理器一般通过缓存一致性实现该属性,同时保留受影响缓存行的独占所有权。它们不会为每条 LOCK 指令都断言外部总线锁。跨界访问和不可缓存内存可能需要更强的处理方式。对不受支持的指令或寄存器目标使用 LOCK 会引发 #UD。

对于字符串操作,F3 表示 REP 或 REPE/REPZ,而 F2 表示 REPNE/REPNZ。计数值来自 CX、ECX 或 RCX,由地址大小决定。具体停止条件取决于指令。

对于若干 SSE 及后续指令族,F2、F3 或 66 是强制操作码选择前缀,而非重复或大小操作。根据该前缀,相同的操作码映射字节可以标识不同指令。VEX 和 EVEX 在其 pp 字段中编码等价的选择。

段覆盖与分支提示 #

段覆盖字节如下:

字节段
2ECS
36SS
3EDS
26ES
64FS
65GS

在 16 位和 32 位模式下,覆盖前缀会更改内存操作数使用的段。在 64 位模式下,CS、DS、ES 和 SS 的基址被视为零,因此一般只有 FS 和 GS 覆盖会影响地址形成。

一些较旧的处理器将条件分支上的 2E 和 3E 分别解释为静态“不跳转”和“跳转”提示。当前软件不应依赖这些提示。

操作数大小覆盖 #

66 为适用的指令选择非默认操作数大小。在 32 位代码段中,它一般在 32 位和 16 位操作数之间切换:

text
ff 05 00 00 00 00       inc dword [0]
66 ff 05 00 00 00 00    inc word  [0]

该前缀会更改 INC 加载和存储的整数宽度,但不会更改本例中的 32 位地址位移量。操作数大小与地址大小相互独立。

在 64 位模式下,默认整数操作数大小一般为 32 位。对于支持这些大小的指令,66 选择 16 位,而 REX.W 选择 64 位。各个操作码定义可能另有规则。

地址大小覆盖 #

67 选择非默认地址大小。在 32 位代码段中,它可以选择 16 位寻址。在 64 位模式下,它选择 32 位而非 64 位寻址;长模式不提供 16 位地址大小编码。

地址大小决定 ModR/M 和 SIB 使用哪些寄存器和寻址表。它还选择字符串指令的计数寄存器。地址大小本身不会改变所加载、存储或计算值的宽度。

操作码映射 #

传统编码使用多种操作码映射:

  • 单字节映射包含除 0F 转义以外的操作码。
  • 0F xx 选择双字节映射。
  • 0F 38 xx 和 0F 3A xx 选择其他三字节映射。

操作码可以包含方向、宽度、条件码或寄存器字段。是否后接 ModR/M、位移量和立即数字段,由指令手册而非仅由操作码字节数决定。

VEX 和 EVEX 前缀会编码操作码映射选择器,而不直接写出传统的 0F、0F 38 或 0F 3A 字节。它们还会编码强制前缀状态、向量长度、额外寄存器位和源操作数。因此,它们表面上的前缀字节属于一种独立的编码形式,而非一系列相互独立的传统修饰符。

ModR/M 字节 #

ModR/M 分为 mod、reg 和 r/m:

7mod65r4eg32r1/m0

操作码定义 reg 的含义。它可以指定操作数寄存器,也可以提供 /0 至 /7 操作码扩展。mod 和 r/m 字段选择寄存器或内存寻址形式。

mod32 位和 64 位地址大小下的一般含义
00内存,通常不含位移量
01内存,带符号扩展的 disp8
10内存,带 disp32
11寄存器操作数;不进行内存寻址

特殊的 r/m 和 SIB 值会改变 mod=00 规则。

寄存器形式 #

当 mod=11 时,r/m 指定一个寄存器。如果 reg 是操作数字段,它会指定另一个寄存器。在没有扩展位时,三位编码选择:

编码32 位16 位低 8 位传统高 8 位
000EAXAXAL-
001ECXCXCL-
010EDXDXDL-
011EBXBXBL-
100ESPSP使用 REX 时为 SPL不使用 REX 时为 AH
101EBPBP使用 REX 时为 BPL不使用 REX 时为 CH
110ESISI使用 REX 时为 SIL不使用 REX 时为 DH
111EDIDI使用 REX 时为 DIL不使用 REX 时为 BH

操作数大小和指令类别决定该编码标识通用、MMX、向量、控制、调试还是其他寄存器类别。REX、VEX 和 EVEX 扩展位可以增加编号更高的寄存器。

内存形式 #

当 mod 不等于 11 时,r/m 选择一种内存寻址形式。在 32 位地址大小下:

  • 多数 r/m 值选择基址寄存器。
  • r/m=100 选择后续 SIB 字节。
  • mod=00, r/m=101 选择仅由 disp32 组成且没有基址的地址。
  • mod=01 添加符号扩展的 disp8。
  • mod=10 添加 disp32。

在 64 位地址大小下,mod=00, r/m=101 改为选择 RIP + sign-extended disp32。RIP 相对寻址支持对附近代码和数据进行位置无关引用。绝对地址可能需要其他编码策略,包括 SIB 无基址形式,或先将地址加载到寄存器。

解码器分阶段选择有效地址形式:

Mmbnooaodds-R,eb/raM/oosmtrehesfrpoerrcmr/i/maml=100sS,IiB,bmodselectsdi0s,pl8a,ceomren3t2

16 位地址大小使用由 BX、BP、SI 和 DI 组合而成的独立表;它没有 SIB 字节。在该地址大小下,mod=10 选择的位移量为 16 位。

SIB 字节 #

在 32 位和 64 位地址大小形式中,内存 ModR/M 值 r/m=100 选择一个 SIB 字节:

7scal6e5in4dex32ba1se0

有效地址为:

text
base + index * scale + displacement

比例字段的编码如下:

位比例因子
001
012
104
118

没有扩展位时,index=100 表示无索引。在长模式下,扩展位允许选择其他寄存器。对于 mod=00,base=101 表示无基址并要求一个 disp32;对于 mod=01 或 10,相同基址位选择 EBP 或 RBP,并使用 mod 指定的位移量。

因此,即使不需要缩放索引,以 ESP 或 RSP 为基址也需要 SIB 字节。汇编器会隐藏这一表示细节。

位移量与立即数 #

位移量参与有效地址计算。是否存在位移量及其编码宽度取决于寻址形式:

  • disp8 被符号扩展到当前地址大小。
  • disp16 出现在部分 16 位寻址形式中。
  • disp32 用于 32 位形式,以及长模式下的基址相对或 RIP 相对形式。

立即数是嵌入指令的操作数值。其宽度由具体操作码决定,而不是目标宽度的直接结果。例如:

  • 对 16 位或 32 位操作数执行算术运算,使用符号扩展的 imm8;
  • 对 64 位操作数执行算术运算,使用符号扩展的 imm32;
  • MOV r64, imm64 可以携带完整的 64 位立即数;
  • 移位指令的计数可以是隐含值 1、寄存器 CL 或编码的 imm8。

位移量和立即数以小端序存储。反汇编器需要先知道操作码和模式,才能判断一个字段在哪里结束、下一个字段从哪里开始。

长模式 REX 编码 #

单字节 REX 前缀的形式为 0100WRXB,取值范围为 40 至 4F:

706150403W2R1X0B
位功能
W为支持该宽度的指令选择 64 位操作数
R扩展 ModR/M reg 字段
X扩展 SIB index 字段
B扩展 ModR/M r/m、SIB base 或嵌入操作码的寄存器

当指令使用受支持的 64 位操作数、引用 R8 至 R15,或需要字节寄存器 SPL、BPL、SIL 或 DIL 时,需要 REX 前缀。它必须紧邻操作码之前。位于 REX 字节之后的传统前缀会使该字节无法充当指令的有效 REX 前缀。

任何 REX 前缀都会使 AH、BH、CH 和 DH 在该指令中不可用。它们的传统编码与 REX 引入的低字节寄存器编码重叠。

长模式的默认值并非统一为 64 位:

  • 地址大小默认为 64 位,67 选择 32 位。
  • 多数整数指令默认为 32 位操作数。
  • 66 一般选择 16 位操作数。
  • REX.W 在有定义时选择 64 位操作数。
  • 栈、分支和定宽指令具有操作码专属规则。

例如:

text
48 8b 44 8b 20    mov rax, [rbx + rcx*4 + 0x20]
  • 48 是 REX.W。
  • 8B 是 MOV r64, r/m64。
  • ModR/M 44 选择内存操作数、目标寄存器 RAX、一个 SIB 字节和 disp8。
  • SIB 8B 选择比例因子 4、索引 RCX 和基址 RBX。
  • 20 是位移量。

VEX 与 EVEX #

REX 扩展传统通用编码,但向量指令族需要更多状态。VEX 编码操作码映射、强制前缀选择、向量长度、寄存器扩展和额外的源寄存器字段。常见 VEX 形式使用两个或三个字节。

EVEX 增加更多寄存器位,以及掩码、清零、广播、舍入和更大向量长度的控制。一些 EVEX 内存操作数使用压缩位移量缩放,因此编码的 disp8 会在地址计算前乘以一个取决于元组的因子。

VEX 和 EVEX 会复用在较旧模式中具有其他含义的字节值。模式和前缀有效性规则让处理器能够区分这些形式。应将它们作为完整的前缀结构解码,而不是逐字节应用传统前缀规则。

实用解码流程 #

按依赖顺序解码指令:

  1. 确定执行模式、代码段默认值和已启用的体系结构扩展。
  2. 读取有效的传统前缀。
  3. 在适用时识别 REX、VEX、XOP 或 EVEX 形式。
  4. 选择操作码映射和操作码。
  5. 查询操作码定义,确定是否需要 ModR/M。
  6. 如果 ModR/M 选择内存,且在支持 SIB 的地址大小下 r/m=100,则解码 SIB。
  7. 根据地址大小、mod 和特殊字段值确定偏移量宽度。
  8. 根据操作码和有效操作数大小确定立即数宽度。
  9. 应用寄存器扩展字段和指令专属语义规则。

没有任何字节具有自描述能力。例如,66 可以是操作数大小覆盖、强制操作码选择器,或间接编码在 VEX/EVEX 状态中。因此,可靠的解码需要使用处理器手册中的指令表。

参考资料 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)