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),它对寻址模式和操作数类型进行编码。它仅由操作码用来做任何这样的操作数。它有三个位域:
在之后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=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。)
62是RegR/M字节。在二进制中它是0110 0010or 01 100 010。前两个字节(Mod字段)表示间接寻址模式,位移为 8 位。接下来的三位(reg字段)是100将SP寄存器(在本例中ESP,因为我们处于 32 位模式)编码为目标寄存器。最后三位是R/M字段,我们在010那里将D寄存器(在本例中EDX)编码为另一个(源)寄存器。
现在我们期望 8 位位移。它是:2f是位移,一个正数(十进制+47)。
最后一部分是imul指令需要的立即数的四个字节。在你的情况下,这就是6c 64 2d 6c小端的$6c2d646c.
这就是饼干碎的方式;-J
这些手册确实描述了如何区分一个、两个或三个操作数版本。

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