从操作码中查找指令中的操作数数量

Hri*_*ali 5 x86 assembly opcode disassembly

我正计划编写自己的小型反汇编程序。我想解码我在读取可执行文件时获得的操作码。我看到以下操作码:

69 62 2f 6c 64 2d 6c
Run Code Online (Sandbox Code Playgroud)

必须对应于:

imul   $0x6c2d646c,0x2f(%edx),%esp
Run Code Online (Sandbox Code Playgroud)

现在,“imul”指令可以有两个或三个操作数。我如何从那里的操作码中弄清楚这一点?

它基于英特尔的 i386 指令集。

Sas*_*asQ 11

尽管 x86 指令集相当复杂(无论如何它是 CISC),而且我看到这里有很多人不鼓励您尝试理解它,但我会说相反:它仍然可以理解,并且您可以在此过程中学习为什么它如此复杂,以及英特尔如何设法将它从 8086 一直扩展到现代处理器。

x86 指令使用可变长度编码,因此它们可以由多个字节组成。每个字节都用于编码不同的内容,其中一些是可选的(无论是否使用这些可选字段,它都在操作码中进行编码)。

例如,每个操作码前面可以有零到四个前缀字节,这是可选的。通常你不需要担心它们。它们用于更改操作数的大小,或作为转义码到带有现代 CPU(MMX、SSE 等)的扩展指令的操作码表的“二楼”。

然后是实际的操作码,它通常是一个字节,但对于扩展指令最多可以是三个字节。如果您只使用基本指令集,您也不必担心它们。

接下来是所谓的ModR/M字节(有时也称为mode-reg-reg/mem),它对寻址模式和操作数类型进行编码。它仅由操作码用来任何这样的操作数。它有三个位域:

  • 前两位(从左边开始,最重要的)编码寻址模式(4 种可能的位组合)。
  • 接下来的三位对第一个寄存器进行编码(8 种可能的位组合)。
  • 最后三位可以编码另一个寄存器,或扩展寻址模式,具体取决于前两位的设置。

在之后ModR/M的字节,可能有另一个名为可选字节(取决于寻址模式)SIBSCale的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=B寄存器(基数)

我怀疑他们的 8 位处理器只使用了这四个以这种方式编码的 8 位寄存器:

       second
      +---+---+
f     | 0 | 1 |          00 = A
i +---+---+---+          01 = C
r | 0 | A : C |          10 = D
s +---+ - + - +          11 = B
t | 1 | D : B |
  +---+---+---+
Run Code Online (Sandbox Code Playgroud)

然后,在 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
Run Code Online (Sandbox Code Playgroud)

但是现在您也可以选择将这些寄存器的两半一起使用,作为完整的 16 位寄存器。这是由操作码的最后一位(最低有效位,最右边的一位)完成的:如果是0,则这是一条 8 位指令。但是如果设置了该位(即操作码为奇数),则这是一条 16 位指令。在这种模式下,两位对ACDB寄存器之一进行编码,就像以前一样。图案保持不变。但他们现在编码完整的 16 位寄存器。但是当第三个字节(最高字节)也被设置时,它们会切换到另一组寄存器,称为索引/指针寄存器,它们是:(SP堆栈指针)、BP(基指针)、SI(源索引)、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
Run Code Online (Sandbox Code Playgroud)

在引入 32 位 CPU 时,他们再次将这些 bank 翻了一番。但模式保持不变。刚才的奇数操作码表示 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
Run Code Online (Sandbox Code Playgroud)

如您所见,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字节存在,则由100位中的R/M位模式发出信号。还有一个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
Run Code Online (Sandbox Code Playgroud)

所以现在让我们解码你的imul

69是它的操作码。它对imul's 版本进行编码,该版本不对 8 位操作数进行符号扩展。该6B版本确实对它们进行了符号扩展。(如果有人问,它们的区别在于操作码中的位 1。)

62RegR/M字节。在二进制中它是0110 0010or 01 100 010。前两个字节(Mod字段)表示间接寻址模式,位移为 8 位。接下来的三位(reg字段)是100SP寄存器(在本例中ESP,因为我们处于 32 位模式)编码为目标寄存器。最后三位是R/M字段,我们在010那里将D寄存器(在本例中EDX)编码为另一个(源)寄存器。

现在我们期望 8 位位移。它是:2f是位移,一个正数(十进制+47)。

最后一部分是imul指令需要的立即数的四个字节。在你的情况下,这就是6c 64 2d 6c小端的$6c2d646c.

这就是饼干碎的方式;-J


Igo*_*sky 3

这些手册确实描述了如何区分一个、两个或三个操作数版本。

IMUL指令

F6/F7:1个操作数;0F AF:两个操作数;6B/69:三个操作数。

  • @dwlech:根据当前默认操作数大小(16 位或 32 位),操作码以“0x66”为前缀(操作数大小覆盖前缀)。例如,在 32 位模式下,“69 ..”表示“IMUL r32,r/m32”,而“66 69 ..”表示“IMUL r16,r/m16”。 (2认同)