我打算编写自己的小型反汇编程序。我想解码读取可执行文件时得到的操作码。我看到以下操作码:
69 62 2f 6c 64 2d 6c
必须对应于:
imul $0x6c2d646c,0x2f(%edx),%esp
现在,“imul”指令可以有两个或三个操作数。我如何从那里的操作码中弄清楚这一点?
它基于英特尔的 i386 指令集。
我打算编写自己的小型反汇编程序。我想解码读取可执行文件时得到的操作码。我看到以下操作码:
69 62 2f 6c 64 2d 6c
必须对应于:
imul $0x6c2d646c,0x2f(%edx),%esp
现在,“imul”指令可以有两个或三个操作数。我如何从那里的操作码中弄清楚这一点?
它基于英特尔的 i386 指令集。
虽然 x86 指令集相当复杂(反正它是 CISC)而且我看到这里很多人都在阻止你尝试理解它,但我会说相反:它仍然可以理解,你可以在路上学习为什么它如此复杂,以及英特尔如何成功地将它从 8086 一路扩展到现代处理器。
x86 指令使用可变长度编码,因此它们可以由多个字节组成。每个字节用于编码不同的东西,其中一些是可选的(无论是否使用这些可选字段,它都在操作码中编码)。
例如,每个操作码前面可以有零到四个前缀字节,这是可选的。通常你不需要担心它们。它们用于更改操作数的大小,或作为现代 CPU(MMX、SSE 等)扩展指令的操作码表“第二层”的转义码。
然后是实际的操作码,通常是一个字节,但对于扩展指令最多可以是三个字节。如果您只使用基本指令集,您也不必担心它们。
接下来是所谓的ModR/M
字节(有时也称为mode-reg-reg/mem
),它对寻址模式和操作数类型进行编码。它仅由确实具有任何此类操作数的操作码使用。它具有三个位字段:
在ModR/M
字节之后,可能还有另一个可选字节(取决于寻址模式),称为SIB
( S
cale ndex I
ase B
)。它用于更奇特的寻址模式来编码比例因子(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 |
+---+---+---+
然后,在 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
(基指针),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
在引入 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 位“长”的东西)。您可以在我为快速参考和手动组装/拆卸的东西制作的这个操作码映射中看到更多:(
它还不是一个完整的表格,一些操作码丢失了。也许有一天我会更新它。)
如您所见,算术和逻辑指令大多位于表格的上半部分,左右半部分遵循类似的布局。数据移动指令位于下半部分。所有分支指令(条件跳转)都在 row 中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
所以现在让我们解码你的imul
:
69
是它的操作码。它对imul
不符号扩展 8 位操作数的 ' 版本进行编码。该6B
版本确实对它们进行了符号扩展。(如果有人问的话,它们的区别在于操作码中的第 1 位。)
62
是RegR/M
字节。在二进制中它是0110 0010
or 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
这些手册确实描述了如何区分一个、两个或三个操作数版本。
F6/F7:一个操作数;0F AF:两个操作数;6B/69:三个操作数。
一些建议,首先获取所有你可以得到的指令集文档。对于这个 x86 案例,请尝试一些旧的 8088/86 手册以及最新的,来自英特尔的以及网络上丰富的操作码表。各种解释和文档可能首先存在细微的文档错误或差异,其次有些人可能会以不同且更易于理解的方式呈现信息。
其次,如果这是您的第一个反汇编程序,我建议避免使用 x86,这非常困难。由于您的问题暗示可变字长指令集很困难,因此要制作远程成功的反汇编程序,您需要按照执行顺序而不是内存顺序遵循代码。因此,您的反汇编程序必须使用某种方案不仅要解码和打印指令,还要解码跳转指令和标记目标地址作为指令的入口点。例如 ARM,是固定指令长度,您可以编写一个 ARM 反汇编程序,从 ram 的开头开始并直接反汇编每个单词(当然假设它不是 arm 和 thumb 代码的混合)。thumb(不是 thumb2)可以这样反汇编,因为只有一种 32 位指令,其他都是 16 位,
您将无法反汇编所有内容(使用可变长度指令集),并且由于某些手动编码或故意策略的细微差别,以防止反汇编您按执行顺序遍历代码的前置代码可能有我所说的碰撞,例如您上面的说明。假设一条路径将您带到 0x69 作为指令的入口点,并且您从中确定这是一条 7 字节指令,但是说在其他地方有一条分支指令,其目的地计算为 0x2f 作为指令的操作码,尽管非常聪明的编程可能会完成类似的事情,反汇编程序更有可能被引导反汇编数据。例如
clear condition flag
branch if condition flag clear
data
反汇编器不会知道数据是数据,并且如果没有额外的智能,反汇编器不会意识到条件分支实际上是一个无条件分支(条件清除和条件清除分支之间的不同分支路径上可能有许多指令)所以它假设条件分支之后的字节是一条指令。
最后,我为你的努力鼓掌,我经常鼓吹编写简单的反汇编程序(假设代码很短,故意制作的代码)来很好地学习指令集。如果您不将反汇编程序置于必须按照执行顺序执行的情况,而是可以按内存顺序执行(基本上不要在指令之间嵌入数据,将其放在末尾或其他地方,只留下要反汇编的指令字符串)。了解指令集的操作码解码可以让您更好地为该平台编程,无论是低级语言还是高级语言。
简短的回答,英特尔曾经出版,也许现在仍然出版处理器的技术参考手册,我仍然有我的 8088/86 手册,一份用于电气材料的硬件手册,一份用于指令集及其工作原理的软件手册。我有一个 486,可能还有一个 386。伊戈尔答案中的快照直接类似于英特尔手册。因为指令集随着时间的推移发生了如此大的变化,所以 x86 充其量只是一个困难的野兽。同时,如果处理器本身可以遍历这些字节并执行它们,您就可以编写一个可以做同样事情但对它们进行解码的程序。
那不是机器代码指令(由操作码和零个或多个操作数组成)。
这是文本字符串的一部分,它翻译为:
$ echo -e "\x69\x62\x2f\x6c\x64\x2d\x6c"
ib/ld-l
这显然是 string 的一部分"/lib/ld-linux.so.2"
。
如果您不想翻阅操作码表/手册,那么从其他项目中学习总是有帮助的,例如开源反汇编程序、bea-engine,您可能会发现您甚至不需要再创建自己的项目了,取决于你这样做的目的。