x86 是 CISC register machine,其中任何指令的最多 1 个操作数可以是 显式 内存地址而不是寄存器,使用像 [rdi + rax*4] 这样的寻址模式。 (有些指令可以有 2 个内存操作数,其中一个或两个是隐式的,不过:What x86 instructions take two (or more) memory operands?)
典型的 x86 整数指令有 2 个操作数,都是显式的,例如 add eax, edx,它执行 eax+=edx。
还有一些真正的 1 操作数 ALU 指令(没有隐含的其他操作数),例如 inc/dec、neg、not,它们是隐式 1 的加/减或从 0 的加/减的快捷方式,或与 -1 进行异或(一些具有不同的 FLAGS 语义)。还有bswap。此外,具有隐式 1 计数的移位/循环指令基本上是 1 操作数,并且某些汇编程序确实允许您编写 shr %eax。
旧版 x87 FP 代码使用带有 x87 堆栈的 1 操作数指令,例如 faddp st1,其中 x87 堆栈的顶部 (st0) 是一个隐式操作数。还有一些像 fchs 这样的 0 操作数指令只对 st0 隐式运行。 (SSE2 是 x86-64 的基线,因此 x87 不再广泛使用。)
现代 FP 代码使用 SSE/SSE2 2 操作数指令,如 addsd xmm0,xmm1 或 3 操作数 AVX 编码,如 vaddsd xmm2, xmm0, xmm1
x86 指令有 0、1、2、3 甚至 4 个显式操作数。
有多种指令格式,但显式 reg/memory 操作数通常编码在操作码字节之后的 ModR/M 字节中。 (x86-64 instruction encoding on osdev 有很好的细节和图表)。它有 3 个字段:
- r/m 操作数的 2 位模式(寄存器直接
reg,寄存器间接[reg],[reg+disp8],[reg+disp32])。带有位移位的模式表明这些字节跟随 ModR/M 字节。
- 3 位 r/m 字段(寄存器直接或间接的寄存器编号,或者可以是转义码,表示 ModRM 之后有一个 Scale/Index/Base SIB 字节,可以编码 r 的缩放索引寻址模式/m 操作数)。有关特殊情况/转义码的详细信息,请参阅rbp not allowed as SIB base?。
- 3 位 reg 字段,总是一个寄存器号。 (或者在单操作数或
r/m, immediate 指令中,used as extra opcode bits,例如用于移位/旋转选择哪种类型。)
大多数指令至少有 2 种编码,reg/memory 目标或 reg/memory 源。如果您想要的操作数都是寄存器,您可以使用任一操作码,add r/m32, r32 或 add r32, r/m32。 (一些汇编器have syntax to let you select the non-default encoding。理论上,汇编器/编译器可以使用这些选择作为水印来显示是哪个工具生成的。)
普通指令也有其他直接源形式的操作码,但通常它们使用 ModR/M 中的 reg 字段作为额外的操作码位,因此您仍然只能获得 2 个操作数,如 add eax, 123。一个例外是 imul 的直接形式加上 186,例如imul eax, [rdi + rbx*4], 12345。它不与其他立即指令共享编码空间,而是在 ModR/M 中具有寄存器 dst 和 r/m 源加上操作码隐含的立即操作数。
一些单操作数指令使用相同的技巧,即使用reg 字段作为额外的操作码位,但没有立即数。例如neg r/m32、not r/m32、inc r/m32 或 shl/shr/rotate 编码移动隐式 1(不是 cl 或立即数)。所以不幸的是,你不能复制和转移(直到 BMI2)。
有一些特殊情况的编码可以提高代码密度,例如 push rax/push rdx 的单字节编码,将 reg 字段打包到操作码字节的低 3 位中。在 16/32 位模式下,inc/dec 任何寄存器的单字节编码。但在 64 位模式下,这些 0x4? 代码用作 REX 前缀,以扩展 reg 和 r/m 字段以提供 16 个架构寄存器。
也有带有部分或全部隐式操作数的指令,例如movsb,它将一个字节从[rsi]复制到[rdi],并且可以与rep前缀一起使用来重复rcx 次。
或者mul ecx 是edx:eax = eax * ecx。一个显式源操作数、一个隐式源和 2 个隐式目标寄存器。 div/idiv 类似。
具有至少 1 个显式 reg/mem 操作数的指令对其使用 ModR/M 编码,但具有零显式操作数的指令(如 movsb 或 cdq)没有 ModR/M 字节。他们只有操作码。有些指令根本没有操作数,甚至没有隐含的,like mfence。
立即操作数不能通过 ModR/M 发出信号,只能由操作码本身发出,因此push imm32 or push imm8 有自己的操作码。隐式目的地(内存在[rsp],RSP 本身更新为rsp-=8)。
LEA 是一种解决方法,它提供 x86 3 操作数移位和加法,例如 lea eax, [rdi + rdi*2 + 123] 在一条指令中执行 eax = rdi*3 + 123。参见Using LEA on values that aren't addresses / pointers? 目的寄存器在ModR/M 的reg 字段中编码,两个源寄存器在寻址模式中编码。 (涉及一个 SIB 字节,其存在由 ModR/M 字节使用编码表示,否则意味着 base = RSP)。
VEX 前缀(由 AVX 引入)提供 3 操作数指令,例如 bzhi eax, [rsi], edx 或 vaddps ymm0, ymm1, [rsi]。(对于许多指令,第二个源是可选的内存源,但对于某些指令而言,它是第一个来源。)
第三个操作数以 2 或 3 字节 VEX 前缀编码。
有一些 3 操作数非 VEX 指令,例如像 vpblendvb xmm1, xmm2/m128, <XMM0> 这样的 SSE4.1 变量混合,其中 XMM0 是使用该寄存器的隐式操作数。
AVX 版本使其成为非破坏性的(以 VEX 前缀编码的单独目标),和使混合控制操作数显式(以 1 字节的高 4 位编码即时)。 这给了我们一个带有 4 个显式操作数的指令,VPBLENDVB xmm1, xmm2, xmm3/m128, xmm4。
x86 非常狂野并且已经被扩展了很多次,但是典型的整数代码主要使用 2 操作数指令,并投入了大量的 LEA 来保存指令。