【问题标题】:Finding number of operands in an instruction from opcodes从操作码中查找指令中的操作数数量
【发布时间】:2011-08-03 10:17:10
【问题描述】:

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

69 62 2f 6c 64 2d 6c

必须对应:

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

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

它基于 Intel 的 i386 指令集。

【问题讨论】:

  • 反汇编器适用于哪个指令集?
  • 在英特尔的手册中查找?
  • 哦,对不起,我忘了提。英特尔的 i386 指令集。我马上添加。
  • 查看了intel.com/Assets/PDF/manual/325383.pdf 和 AFAIK,它没有描述具有 3 个操作数的 imul 操作是如何编码的。
  • 你确定它们是操作码吗?它们被解释为 ASCII 码——ib/ld-l——表明它们可能是别的东西。

标签: assembly x86 disassembly opcode


【解决方案1】:

虽然 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 位。)

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 谢谢,我完全忽略了这张桌子。 :-(
    • 如何从 0x69 操作码的 32 位立即数解码 16 位立即数?
    • @dwlech:根据当前的默认操作数大小(16 位或 32 位),操作码以 0x66 为前缀(操作数大小覆盖前缀)。例如。在 32 位模式下,69 .. 表示 IMUL r32, r/m3266 69 .. 表示 IMUL r16, r/m16
    【解决方案3】:

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

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

    您将无法反汇编所有内容(使用可变长度的指令集),并且由于某些手动编码的细微差别或故意防止反汇编您按执行顺序遍历代码的代码可能有我会称为碰撞,例如您上面的说明。假设一条路径将您带到 0x69 作为指令的入口点,并且您从中确定这是一条 7 字节指令,但是说在其他地方有一条分支指令,其目的地计算为 0x2f 作为指令的操作码,尽管非常聪明的编程可能会完成类似的事情,反汇编程序更有可能被引导反汇编数据。例如

    clear condition flag
    branch if condition flag clear
    data
    

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

    最后我为你的努力喝彩,我经常鼓吹编写简单的反汇编程序(假设代码很短,故意制作的代码)来很好地学习指令集。如果您不将反汇编程序置于必须按照执行顺序执行的情况,而是可以按内存顺序执行(基本上不要在指令之间嵌入数据,将其放在末尾或其他地方,只留下要反汇编的指令字符串)。了解指令集的操作码解码可以让您更好地为该平台编程,无论是低级语言还是高级语言。

    简短的回答,英特尔曾经出版过处理器的技术参考手册,也许现在还在出版,我仍然有我的 8088/86 手册,一份用于电气材料的硬件手册,一份用于指令集的软件手册,以及如何使用有用。我有一个 486,可能还有一个 386。伊戈尔答案中的快照直接类似于英特尔手册。因为指令集随着时间的推移发生了如此大的变化,所以 x86 充其量只是一个困难的野兽。同时,如果处理器本身可以遍历这些字节并执行它们,您就可以编写一个可以做同样事情但对它们进行解码的程序。不同之处在于您可能不会制作模拟器和任何由代码计算的分支,并且在您无法看到的代码中不明确,并且该分支的目标可能不会显示在您的字节列表中拆卸。

    【讨论】:

    • 非常感谢,我会尽力弄清楚并尝试完成我的反汇编程序。
    【解决方案4】:

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

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

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

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

    【讨论】:

    • 你可能是对的。但这仍然是一个解释问题。字节只是字节。正确解释它们取决于您(或 CPU)。我猜 OP 只是试图反汇编一些随机数据,看看他的反汇编程序会显示什么,然后弄清楚 why 会这样。这是了解反汇编程序如何工作以及指令如何编码的好方法。所以还是值得一试。
    • 有一次我尝试对指令编码格式进行逆向工程,所以我只写了一个简单的脚本来用后续数字填充文件,看看我的反汇编程序将如何解释它们。后续数字是一种合理的方法,因为这样您就可以看到位序列中的简单变化如何反映在反汇编指令中,并且很有可能只有一小部分指令会发生变化。例如。尝试使用88 C088 CF,然后使用89 C089 CF,你会发现寄存器寻址的模式。
    【解决方案5】:

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

    【讨论】:

    • 我这样做是为了通过编写反汇编程序来牢牢掌握汇编的基础知识。 :-)
    • @Hrishikesh Murali:我想说你最好在汇编中编写一些东西,IMO 反汇编程序更适合那些想要编写代码生成器/JIT 的人。但祝你好运:)
    猜你喜欢
    • 2021-11-10
    • 2011-07-03
    • 1970-01-01
    • 2021-04-01
    • 2021-09-05
    • 2020-04-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多