x86 使用变长指令。一条指令可包含传统前缀、操作码、ModR/M 字节、比例—索引—基址(SIB)字节、位移量和立即数。只有操作码始终存在:
长模式可以在传统前缀和操作码之间添加 REX 前缀。对于较新的指令族,VEX、XOP 和 EVEX 编码会取代这一布局的部分字段。无论采用何种形式,体系结构层面的 x86 指令都不能超过 15 字节。
这些字段并非汇编语言词元的直接序列化结果。操作码可以编码操作数的一部分,ModR/M 字段可以扩展操作码,而地址表达式选择的是处理器的一种寻址形式,并非要求执行一系列加法和乘法。
操作与操作数之间没有固定边界 #
汇编语法用一条指令助记符加零个或多个操作数来表示指令:
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 位模式下,这些指令编码为:
ff 05 00 00 00 00 inc dword [0]
ff 0d 00 00 00 00 dec dword [0]两者都使用操作码字节 FF,但 ModR/M 字节不同:
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, eax | 90 |
xchg eax, ecx | 91 |
xchg eax, edx | 92 |
xchg eax, ebx | 93 |
xchg eax, esp | 94 |
xchg eax, ebp | 95 |
xchg eax, esi | 96 |
xchg eax, edi | 97 |
指令表将这一族写作 90+rd。相比之下,xchg ebx, ecx 使用操作码 87 和一个 ModR/M 字节:
87 d9 xchg ebx, ecx其他 +r 指令族包括 PUSH、POP、带立即数的 MOV 和 BSWAP。单字节 INC 和 DEC 寄存器形式仅在 64 位模式之外使用字节 40 至 4F。长模式将这些字节值分配给 REX 前缀。
汇编地址表达式选择编码字段 #
[eax + ebx*4 + 0x32] 这样的表达式不会编码算术指令。它要求汇编器选择 ModR/M、SIB 和位移量字段,使地址生成硬件计算该表达式。
例如:
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 位位移量。
因此,同一个书写形式的助记符会根据操作数使用不同的长度和字段布局。
传统前缀 #
传统前缀分为四个语义组。一条指令最多可以从每组使用一个有效前缀。编码器不应发出同一组中的重复前缀:不同处理器对冗余或冲突前缀的行为并不一致。
| 组 | 字节 | 用途 |
|---|---|---|
| 1 | F0、F2、F3 | LOCK、重复或强制操作码选择 |
| 2 | 2E、36、3E、26、64、65 | 段覆盖;历史上的分支提示 |
| 3 | 66 | 操作数大小覆盖或强制操作码选择 |
| 4 | 67 | 地址大小覆盖 |
汇编器使用约定的分组顺序,但传统解码并不要求所有组都按数字顺序出现。REX 前缀有所不同:如果存在,它必须位于传统前缀之后,并紧邻操作码或操作码映射转义字节之前。
锁定与重复前缀 #
F0 是 LOCK 前缀。它只适用于目标为内存的指定“读—修改—写”指令。相对于其他逻辑处理器,有效的锁定操作具有原子性。现代处理器一般通过缓存一致性实现该属性,同时保留受影响缓存行的独占所有权。它们不会为每条 LOCK 指令都断言外部总线锁。跨界访问和不可缓存内存可能需要更强的处理方式。对不受支持的指令或寄存器目标使用 LOCK 会引发 #UD。
对于字符串操作,F3 表示 REP 或 REPE/REPZ,而 F2 表示 REPNE/REPNZ。计数值来自 CX、ECX 或 RCX,由地址大小决定。具体停止条件取决于指令。
对于若干 SSE 及后续指令族,F2、F3 或 66 是强制操作码选择前缀,而非重复或大小操作。根据该前缀,相同的操作码映射字节可以标识不同指令。VEX 和 EVEX 在其 pp 字段中编码等价的选择。
段覆盖与分支提示 #
段覆盖字节如下:
| 字节 | 段 |
|---|---|
2E | CS |
36 | SS |
3E | DS |
26 | ES |
64 | FS |
65 | GS |
在 16 位和 32 位模式下,覆盖前缀会更改内存操作数使用的段。在 64 位模式下,CS、DS、ES 和 SS 的基址被视为零,因此一般只有 FS 和 GS 覆盖会影响地址形成。
一些较旧的处理器将条件分支上的 2E 和 3E 分别解释为静态“不跳转”和“跳转”提示。当前软件不应依赖这些提示。
操作数大小覆盖 #
66 为适用的指令选择非默认操作数大小。在 32 位代码段中,它一般在 32 位和 16 位操作数之间切换:
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:
操作码定义 reg 的含义。它可以指定操作数寄存器,也可以提供 /0 至 /7 操作码扩展。mod 和 r/m 字段选择寄存器或内存寻址形式。
mod | 32 位和 64 位地址大小下的一般含义 |
|---|---|
00 | 内存,通常不含位移量 |
01 | 内存,带符号扩展的 disp8 |
10 | 内存,带 disp32 |
11 | 寄存器操作数;不进行内存寻址 |
特殊的 r/m 和 SIB 值会改变 mod=00 规则。
寄存器形式 #
当 mod=11 时,r/m 指定一个寄存器。如果 reg 是操作数字段,它会指定另一个寄存器。在没有扩展位时,三位编码选择:
| 编码 | 32 位 | 16 位 | 低 8 位 | 传统高 8 位 |
|---|---|---|---|---|
000 | EAX | AX | AL | - |
001 | ECX | CX | CL | - |
010 | EDX | DX | DL | - |
011 | EBX | BX | BL | - |
100 | ESP | SP | 使用 REX 时为 SPL | 不使用 REX 时为 AH |
101 | EBP | BP | 使用 REX 时为 BPL | 不使用 REX 时为 CH |
110 | ESI | SI | 使用 REX 时为 SIL | 不使用 REX 时为 DH |
111 | EDI | DI | 使用 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 无基址形式,或先将地址加载到寄存器。
解码器分阶段选择有效地址形式:
16 位地址大小使用由 BX、BP、SI 和 DI 组合而成的独立表;它没有 SIB 字节。在该地址大小下,mod=10 选择的位移量为 16 位。
SIB 字节 #
在 32 位和 64 位地址大小形式中,内存 ModR/M 值 r/m=100 选择一个 SIB 字节:
有效地址为:
base + index * scale + displacement比例字段的编码如下:
| 位 | 比例因子 |
|---|---|
00 | 1 |
01 | 2 |
10 | 4 |
11 | 8 |
没有扩展位时,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:
| 位 | 功能 |
|---|---|
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 位操作数。- 栈、分支和定宽指令具有操作码专属规则。
例如:
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 会复用在较旧模式中具有其他含义的字节值。模式和前缀有效性规则让处理器能够区分这些形式。应将它们作为完整的前缀结构解码,而不是逐字节应用传统前缀规则。
实用解码流程 #
按依赖顺序解码指令:
- 确定执行模式、代码段默认值和已启用的体系结构扩展。
- 读取有效的传统前缀。
- 在适用时识别 REX、VEX、XOP 或 EVEX 形式。
- 选择操作码映射和操作码。
- 查询操作码定义,确定是否需要 ModR/M。
- 如果 ModR/M 选择内存,且在支持 SIB 的地址大小下
r/m=100,则解码 SIB。 - 根据地址大小、
mod和特殊字段值确定偏移量宽度。 - 根据操作码和有效操作数大小确定立即数宽度。
- 应用寄存器扩展字段和指令专属语义规则。
没有任何字节具有自描述能力。例如,66 可以是操作数大小覆盖、强制操作码选择器,或间接编码在 VEX/EVEX 状态中。因此,可靠的解码需要使用处理器手册中的指令表。
评论
在 GitHub 上阅读或评论(发表评论需要登录)