虽然 x86 指令集相当复杂(反正它是 CISC),而且我看到这里很多人不鼓励你尝试理解它,但我会说相反:它仍然可以理解,你可以学习为什么它如此复杂,以及英特尔如何成功地将它从 8086 一路扩展到现代处理器。
x86 指令使用可变长度编码,因此它们可以由多个字节组成。每个字节用于编码不同的东西,其中一些是可选的(无论是否使用这些可选字段,它都在操作码中编码)。
例如,每个操作码前面可以有零到四个前缀字节,这是可选的。通常你不需要担心它们。它们用于更改操作数的大小,或作为现代 CPU(MMX、SSE 等)扩展指令的操作码表“第二层”的转义码。
然后是实际的操作码,通常是一个字节,但对于扩展指令最多可以是三个字节。如果你只使用基本指令集,你也不必担心它们。
接下来是所谓的ModR/M 字节(有时也称为mode-reg-reg/mem),它对寻址模式和操作数类型进行编码。它仅由 确实 具有任何此类操作数的操作码使用。它具有三个位域:
- 前两位(从左到右,最高有效位)编码寻址模式(4 种可能的位组合)。
- 接下来的三位对第一个寄存器进行编码(8 种可能的位组合)。
- 最后三位可以对另一个寄存器进行编码,或扩展寻址模式,具体取决于前两位的设置。
在ModR/M 字节之后,可能还有另一个可选字节(取决于寻址模式),称为SIB (Scale Index Base)。它用于更奇特的寻址模式来编码比例因子(1x、2x、4x)、基地址/寄存器和使用的索引寄存器。它具有与ModR/M 字节类似的布局,但从左起的前两位(最高有效位)用于编码比例,接下来的三位和最后三位编码索引和基址寄存器,顾名思义.
如果使用了任何位移,它会紧随其后。它可能是 0、1、2 或 4 个字节长,具体取决于寻址模式和执行模式(16 位/32 位/64 位)。
最后一个总是直接数据,如果有的话。它也可以是 0、1、2 或 4 个字节长。
所以现在,当您了解 x86 指令的整体格式后,您只需要知道所有这些字节的编码是什么。还有一些模式,与普遍的看法相反。
例如,所有寄存器编码都遵循简洁的模式ACDB。即对于 8 位指令,寄存器代码的最低两位对 A、C、D 和 B 寄存器进行编码,对应:
00 = A 寄存器(累加器)
01 = C 寄存器(计数器)
10 = D 寄存器(数据)
11 = @ 987654338@寄存器(基)
我怀疑他们的 8 位处理器只使用了以这种方式编码的这四个 8 位寄存器:
second
+---+---+
f | 0 | 1 | 00 = A
i +---+---+---+ 01 = C
r | 0 | A : C | 10 = D
s +---+ - + - + 11 = B
t | 1 | D : B |
+---+---+---+
然后,在 16 位处理器上,他们将这组寄存器加倍,并在寄存器编码中再增加一位来选择组,这样:
second second 0 00 = AL
+----+----+ +----+----+ 0 01 = CL
f | 0 | 1 | f | 0 | 1 | 0 10 = DL
i +---+----+----+ i +---+----+----+ 0 11 = BL
r | 0 | AL : CL | r | 0 | AH : CH |
s +---+ - -+ - -+ s +---+ - -+ - -+ 1 00 = AH
t | 1 | DL : BL | t | 1 | DH : BH | 1 01 = CH
+---+---+-----+ +---+----+----+ 1 10 = DH
0 = BANK L 1 = BANK H 1 11 = BH
但现在您也可以选择同时使用这些寄存器的两半,作为完整的 16 位寄存器。这是由操作码的最后一位完成的(最低有效位,最右边的一位):如果是0,则这是一条8位指令。但是如果该位被设置(即操作码为奇数),则这是一条 16 位指令。在这种模式下,两位编码ACDB 寄存器之一,如前所述。模式保持不变。但它们现在对完整的 16 位寄存器进行编码。但是当第三个字节(最高字节)也被设置时,它们会切换到另一组寄存器,称为索引/指针寄存器,它们是:SP(堆栈指针)、BP(基指针)、@ 987654345@(源索引),DI(目标/数据索引)。所以现在寻址如下:
second second 0 00 = AX
+----+----+ +----+----+ 0 01 = CX
f | 0 | 1 | f | 0 | 1 | 0 10 = DX
i +---+----+----+ i +---+----+----+ 0 11 = BX
r | 0 | AX : CX | r | 0 | SP : BP |
s +---+ - -+ - -+ s +---+ - -+ - -+ 1 00 = SP
t | 1 | DX : BX | t | 1 | SI : DI | 1 01 = BP
+---+----+----+ +---+----+----+ 1 10 = SI
0 = BANK OF 1 = BANK OF 1 11 = DI
GENERAL-PURPOSE POINTER/INDEX
REGISTERS REGISTERS
在引入 32 位 CPU 时,他们再次将这些存储区增加了一倍。但模式保持不变。刚才奇操作码是指 32 位寄存器,偶操作码和以前一样是 8 位寄存器。我将奇数操作码称为“长”版本,因为根据 CPU 及其当前操作模式使用 16/32 位版本。当它在 16 位模式下运行时,奇数(“长”)操作码表示 16 位寄存器,但当它在 32 位模式下运行时,奇数(“长”)操作码表示 32 位寄存器。可以通过在整个指令前加上 66 前缀(操作数大小覆盖)来翻转它。偶数操作码(“短”操作码)始终为 8 位。所以在 32 位 CPU 中,寄存器代码是:
0 00 = EAX 1 00 = ESP
0 01 = ECX 1 01 = EBP
0 10 = EDX 1 10 = ESI
0 11 = EBX 1 11 = EDI
如您所见,ACDB 模式保持不变。 SP,BP,SI,SI 模式也保持不变。它只是使用较长版本的寄存器。
操作码中也有一些模式。其中一个我已经描述过(偶数与奇数 = 8 位“短”与 16/32 位“长”的东西)。你可以在我为快速参考和手工组装/拆卸的东西制作的这个操作码映射中看到更多:
(这还不是一张完整的表格,一些操作码丢失了。也许有一天我会更新它。)
如您所见,算术和逻辑指令大多位于表格的上半部分,左右半部分遵循类似的布局。数据移动指令位于下半部分。所有分支指令(条件跳转)都在7* 行中。还有一整行B* 为mov 指令保留,这是将立即值(常量)加载到寄存器中的简写。它们都是单字节操作码,后面紧跟立即数,因为它们在操作码中对目标寄存器进行编码(它们由表中的列号选择),在其三个最低有效字节(最右边的字节)中.它们遵循相同的寄存器编码模式。第四位是“短”/“长”选择一个。
您可以看到您的imul 指令已经在表格中,正好在69 位置(呵呵.. ;J)。
对于许多指令,“短/长”位之前的位用于编码操作数的顺序:ModR/M 字节中编码的两个寄存器中的哪一个是源,哪一个是目标(这适用于有两个寄存器操作数的指令)。
至于ModR/M字节的寻址方式字段,解释如下:
-
11 是最简单的:它对寄存器到寄存器的传输进行编码。一个寄存器由该字节的后三位(reg 字段)编码,另一个寄存器由该字节的其他三位(R/M 字段)编码。
-
01 表示在这个字节之后,会有一个字节的位移。
-
10 表示相同,但使用的位移是四字节(在 32 位 CPU 上)。
-
00 是最棘手的:它表示间接寻址或简单置换,具体取决于 R/M 字段的内容。
如果存在SIB 字节,则由R/M 位中的100 位模式发出信号。还有一个代码 101 用于 32 位仅位移模式,它根本不使用 SIB 字节。
以下是所有这些寻址模式的摘要:
Mod R/M
11 rrr = register-register (one encoded in `R/M` bits, the other one in `reg` bits).
00 rrr = [ register ] (except SP and BP, which are encoded in `SIB` byte)
00 100 = SIB byte present
00 101 = 32-bit displacement only (no `SIB` byte required)
01 rrr = [ rrr + disp8 ] (8-bit displacement after the `ModR/M` byte)
01 100 = SIB + disp8
10 rrr = [ rrr + disp32 ] (except SP, which means that the `SIB` byte is used)
10 100 = SIB + disp32
现在让我们解码你的imul:
69 是它的操作码。它对imul 的版本进行编码,该版本不对8 位操作数进行符号扩展。 6B 版本确实对它们进行了符号扩展。 (如果有人问的话,它们的区别在于操作码中的第 1 位。)
62 是RegR/M 字节。在二进制中它是0110 0010 或01 100 010。前两个字节(Mod 字段)表示间接寻址模式,位移为 8 位。接下来的三位(reg 字段)是100,并将SP 寄存器(在本例中为ESP,因为我们处于32 位模式)编码为目标寄存器。最后三位是R/M 字段,我们在那里有010,它将D 寄存器(在本例中为EDX)编码为使用的另一个(源)寄存器。
现在我们期望 8 位位移。就是这样:2f 是位移,一个正数(十进制+47)。
最后一部分是立即数的四个字节,这是imul 指令所要求的。在您的情况下,这是 6c 64 2d 6c,在 little-endian 中是 $6c2d646c。
这就是饼干碎的方式;-J