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

时间:2011-08-03 10:17:10

标签: assembly opcode

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

69 62 2f 6c 64 2d 6c

必须符合:

imul   $0x6c2d646c,0x2f(%edx),%esp

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

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

谢谢和问候,
Hrishikesh Murali

5 个答案:

答案 0 :(得分:2)

手册的确描述了如何区分一个,两个或三个操作数版本。

IMUL instruction

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

答案 1 :(得分:2)

一些建议,首先得到你可以得到的所有指令集文档。对于这个x86案例尝试一些旧的8088/86手册以及更新的,来自英特尔以及网络上丰富的操作码表。各种解释和文档可能首先会有细微的文档错误或差异,其次一些人可能会以不同的,更易理解的方式呈现信息。

其次,如果这是你的第一个反汇编程序,我建议避免使用x86,这很难。由于你的问题意味着变量字长指令集很难,为了使远程成功的反汇编程序,你需要遵循执行顺序的代码,而不是内存顺序。所以你的反汇编程序必须使用某种方案,不仅解码和打印指令,还解码跳转指令和标记目标地址作为指令的入口点。例如ARM,是固定的指令长度,你可以编写一个从ram开头开始的ARM反汇编程序并直接反汇编每个单词(当然它不是arm和thumb代码的混合)。拇指(不是thumb2)可以这种方式进行反汇编,因为只有一种32位指令,其他一切都是16位,并且可以在简单的状态机中处理一种风格,因为这两个16位指令显示为成对。

你无法拆解所有东西(使用可变长度指令集),并且由于一些手动编码或故意策略的细微差别,以防止反汇编你执行顺序执行代码的前端代码可能有什么我会发生碰撞,例如上面的说明。假设一条路径将您带到0x69作为指令的入口点,并且您从那里确定这是一条7字节指令,但是在其他地方说有一条分支指令,其目的地计算为0x2f是指令的操作码,尽管非常聪明的编程可能会带来类似的东西,反汇编程序更有可能导致反汇编数据。例如

clear condition flag
branch if condition flag clear
data

反汇编程序不知道数据是数据,没有额外的智能,反汇编程序不会意识到条件分支实际上是无条件分支(如果条件清除,条件清除和分支之间的不同分支路径上可能有许多指令)因此它假设条件分支之后的字节是一条指令。

最后,我赞赏你的努力,我经常鼓励编写简单的反汇编程序(假设代码非常短,有意编写代码),以便很好地学习指令集。如果你不把反汇编程序置于必须遵循执行顺序的情况下,而是它可以按内存顺序进行(基本上不在指令之间嵌入数据,将其放在最后或其他地方,只留下要拆解的指令串) )。理解指令集的操作码解码可以使你在低级和高级语言的平台编程方面做得更好。

简短的回答,英特尔曾经发布过,可能仍然是,处理器的技术参考手册,我仍然有我的8088/86手册,一个用于电器的硬件,一个用于指令集的软件和如何有用。我有486,可能是386。 Igor的答案中的快照直接类似于英特尔手册。因为指令集随着时间的推移已经发展了很多,所以x86充其量只是一个困难的野兽。同时,如果处理器本身可以遍历这些字节并执行它们,那么您可以编写一个程序,它可以执行相同的操作但解码它们。区别在于你可能不会创建一个模拟器和任何由代码计算的分支,而在代码中没有显示你将无法看到的分支,并且该分支的目标可能不会显示在你的字节列表中拆卸。

答案 2 :(得分:2)

尽管x86指令集非常复杂(无论如何它都是CISC),我看到很多人在这里试图理解它的尝试令人沮丧,但我会说相反:它仍然可以被理解,你可以了解为什么它是如此复杂,以及英特尔如何设法从8086到现代处理器多次扩展它。

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

例如,每个操作码前面可以有0到4个前缀字节,这是可选的。通常你不必担心它们。它们用于改变操作数的大小,或作为转义码转换到"二层"带有现代CPU扩展指令的操作码表(MMX,SSE等)。

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

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

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

ModR/M字节之后,可能会有另一个可选字节(取决于寻址模式),称为SIBS cale I ndex B ase )。它用于更奇特的寻址模式,以编码缩放因子(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位指令。但是如果该位置1(即操作码是奇数),则这是一个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位寄存器。我把奇怪的操作码称为" long"版本,因为根据CPU及其当前操作模式使用16/32位版本。当它以16位模式工作时,奇数("长")操作码意味着16位寄存器,但当它工作在32位模式时,奇数("长")操作码表示32位寄存器。它可以通过在整个指令前加66前缀(操作数大小覆盖)来翻转。偶数操作码("短" 1)总是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位"长"东西)。您可以在此操作码地图中看到更多这些我已经制作过一次用于快速参考和手工组装/拆卸的东西: enter image description here (它还不是一个完整的表,有些操作码丢失了。也许我有一天会更新它。)

正如你所看到的,算术&逻辑指令大多位于表格的上半部分,而左侧和右侧则位于表格的上半部分。它的右半部分遵循类似的布局。数据移动指令位于下半部分。所有分支指令(条件跳转)都在行7*中。还为B*指令保留了一个完整行mov,这是将立即值(常量)加载到寄存器中的简写。它们是所有单字节操作码,紧接着是立即数,因为它们在操作码中对目标寄存器进行编码(它们由该表中的列号选择),其三个最低有效字节(右)最近的)。它们遵循相同的寄存器编码模式。第四位是"短" /"长"选择一个。 你可以看到你的imul指令在表中,正好在69位置(呵呵......; J)。

对于许多说明,在"短/长"之前的位; bit,是对操作数的顺序进行编码:ModR/M字节中编码的两个寄存器中的哪一个是源,哪一个是目的地(这适用于带有两个寄存器操作数的指令)。

至于ModR/M字节的寻址模式字段,请按以下步骤解释:

  • 11是最简单的:它对寄存器到寄存器的传输进行编码。一个寄存器由三个下一位(reg字段)编码,另一个寄存器由该字节的其他三位(R/M字段)编码。
  • 01表示在此字节之后,将出现一个字节的位移。
  • 10表示相同,但​​使用的位移是四字节(在32位CPU上)。
  • 00是最棘手的:它意味着间接寻址或简单的置换,具体取决于R/M字段的内容。

如果存在SIB字节,则由100位中的R/M位模式发出信号。对于32位仅位移模式,还有一个代码101,它根本不使用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位而不同。)

62RegR/M个字节。在二进制文件中,它是0110 001001 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

答案 3 :(得分:1)

这不是机器代码指令(由操作码和零个或多个操作数组成)。

这是文本字符串的一部分,它翻译为:

$ echo -e "\x69\x62\x2f\x6c\x64\x2d\x6c"
ib/ld-l

显然是字符串"/lib/ld-linux.so.2"的一部分。

答案 4 :(得分:0)

如果您不想转换操作码表/手册,那么从其他项目中学习总是有帮助的,比如开源反汇编程序bea-engine,您可能会发现甚至不需要创建你自己的,取决于你做了什么。