【问题标题】:How does one opcode byte decode to different instructions depending on the "register/opcode" field? What is that? [duplicate]一个操作码字节如何根据“寄存器/操作码”字段解码为不同的指令?那是什么? [复制]
【发布时间】:2018-12-25 12:14:42
【问题描述】:

我如何确定字节数组在机器代码中将转换成什么?

我知道,如果我在开头看到 0f,它是一个 2 字节指令,但我看到了其他前缀,并且在我的 x64 调试器的一些反汇编中,我看到了奇怪的交互,例如 48 83 C4 38,我可以在操作码参考中看到48 表示操作数是 64 字节。

但是83 说它可以是 7 条不同的指令,具体取决于名为“注册/操作码字段”的字段......什么?

谁能解释一下处理器如何使用这些字节来确定背后的逻辑:

  1. 运行了什么指令
  2. 指令使用的寄存器和/或地址(如果有)

【问题讨论】:

  • 大部分是 How to read the Intel Opcode notation 的副本,但那里的答案并没有详细说明这一点。
  • How to interpret the opcode manually? 涵盖了解码 ModRM。 what does opcode FF350E204000 do? 是另一个副本,以 FF /6 push r/m64 为例。 x86 OpCode Instruction Decoding 是另一个副本。我在site:stackoverflow.com x86-64 register/opcode field 上发现了所有这些谷歌搜索
  • 一些处理器的内部相当于多个“查找表”来处理操作码、r/m 字段、rex 前缀……。
  • @peter cordes 经过一些学习后,我了解到我的问题实际上是 100% 与 How to read the Intel Opcode notation 重复,我对 stackoverflow 的尊重不足以研究我应该做什么虽然这一点,但由于我看到你经常打开并且可能会看到这个,所以我就我应该做什么征求你的意见(例如,我是否将此问题标记为重复并关闭它还是什么?)
  • @bcvdgfdagfewafdsaf:我会把我的答案移到那里,然后将其作为副本关闭。感谢您让我知道您同意这是重复的,而不是扩大这个问题以涵盖更多 old_timer 回答的内容。

标签: assembly x86-64 disassembly machine-code


【解决方案1】:

0x48 是一个 REX 前缀,其中 W 字段设置为 1,表示 64-bit 操作数大小。 (不是 64 字节)。

许多指令立即版本的操作码,包括 83,使用 ModR/M 字节中的 3 位 /r 字段作为 3 个额外的操作码位。英特尔的第 2 卷手册记录了这一点,我认为附录中的操作码表包含了它。

这就是为什么大多数原始 8086 立即数指令,如 and r/m, imm 仍然只允许 2 个操作数,不像 shrd eax, edx, 4imul edx, [rdi], 12345 两个 ModRM 字段都用于编码操作数,以及由操作码。 SHRD/SHLD 并添加了 386 和 imul-immediate was added with 186。不幸的是,copy-and-AND (and eax, edx, 0xf) 不可编码,但至少 x86 可以使用 LEA 进行复制和添加/sub。


每条指令都有自己的文档,例如add (html extract of the vol2 manual),为ADD r/m64, imm8 显示类似
REX.W + 83 /0 ib 的编码,这就是你所拥有的。

diagram of the ModRM bit fields from wiki.osdev.org

  7                           0
+---+---+---+---+---+---+---+---+
|  mod  |    reg    |     rm    |
+---+---+---+---+---+---+---+---+

0xc4 = 0b11000100,所以 reg 字段 = 0。因此我们的操作码是 83 /0,在 Intel 的表示法中。

ModRM 的其余字段是:

  • mode = 0b11,因此 rm 字段编码的是寄存器操作数,而不是寻址模式的基址寄存器。
  • rm = 0b100。 reg #4 = SPL/SP/ESP/RSP。 (在这种情况下,RSP 因为它是 64 位操作数大小)。请参阅英特尔手册,或 https://wiki.osdev.org/X86-64_Instruction_Encoding#Registers 获取表格。

所以指令是add rsp, 0x38

ndisasm -b64 同意:

$ cat > foo.asm
db 0x48, 0x83, 0xC4, 0x38
$ nasm foo.asm     # create a flat binary with those bytes, not an object file
$ ndisasm -b64 foo
00000000  4883C438          add rsp,byte +0x38

【讨论】:

  • 你真的加倍努力! +1
  • How to read the Intel Opcode notation 上对介绍部分进行了小幅扩展后重新发布,并将此问题作为副本关闭。我想我会把这个答案留在这里而不是删除,因为它仍然回答了这个问题。
【解决方案2】:

这取决于具体的架构,不仅仅是 x86-64,而是实际的芯片供应商。您可以检查例如intel's guide for architecture software developers

这有一整章专门介绍字节码中命令的语法,然后是每个可用命令的另一章。这是图2.1,给你一个想法:

取自上述手册。例如,如果您使用 ARM,这将会改变。

这是人们可能需要数年时间学习才能“流利阅读”字节码的东西,因此仅略读它只能让您大致了解语法或定位特定事物的良好资源。

【讨论】:

  • 这没有用,它没有给我任何新信息,也没有真正回答我的问题。
  • @bcvdgfdagfewafdsaf 确实如此!请参阅 ModR/M 字节的详细说明,其中第 5-3 位标记为“Reg/Opcode”,即该字段可以是寄存器操作数或另外 3 位操作码。
【解决方案3】:

我在一页上看到字母,字母a,这可能是许多不同的单词,它后面的字母是n。这可能是一个,并且,答案,任意数量的单词,所以我继续。

x86 和那个时代的其他机器代码以这种方式工作,尤其是直接派生它的指令集。

首先也是最重要的,如果你只是拿走程序的所有字节然后跳到中间,这没有任何意义,很容易走错脚“the quick brown fox”“thequickbrownfox ” “ickbrow”那是什么?处理器根据指令集的规则启动和继续,处理器相当愚蠢,它遵循处理器手册中定义或至少记录的规则。只要程序员和工具创建了一个正确构造的程序,它就不会丢失,如果是这样,那是程序员/工具的错,而不是处理器的错。处理器将开始将操作码字节解码为操作码字节。该字节可以是整个指令,也可以只是基于特定字节的一小部分。如果是分数,那么第一个字节加上它后面的字节可能决定整个指令或者是一个分数。

CISC 尤其是操作码本身以及部分下一个字节可能包含也可能不包含表示相关内容的位。在像 mips 或 arm 或其他特定的 RISC 中,0000 表示寄存器 0,0001 表示寄存器 1,依此类推。但是在一些 CISC 指令中,即使不是很多,也没有一点可以区分寄存器 x 和寄存器 y,寄存器 a 和寄存器 b。必须在表格中查找整个操作码才能知道它的含义。

x86 是一个可变长度指令集,一些指令是一个字节,没有其他操作数,其他指令需要更多字节,然后可能紧随其后。想要将立即值 0x12345678 移动到寄存器 EAX 中,而不查看任何文档会说它是 5 或 6 字节指令,或者是说立即加载到 ax 中的操作码,或者说立即加载的字节和另一个说这是 ax,然后是立即数的四个字节。

mov eax,0x12345678
mov ebx,0x12345678
mov ecx,0x12345678
mov edx,0x12345678

Disassembly of section .text:

00000000 <.text>:
   0:   b8 78 56 34 12          mov    eax,0x12345678
   5:   bb 78 56 34 12          mov    ebx,0x12345678
   a:   b9 78 56 34 12          mov    ecx,0x12345678
   f:   ba 78 56 34 12          mov    edx,0x12345678

原来是 5 个字节。虽然这些字节的位可能会直接解码到四个寄存器之一,但不太可能,因为这些指令集不是这样设计的。

您可能过于复杂了,遗憾的是,英特尔和其他 x86 文档不如其他一些供应商好。但它实际上只是一个流程图,相当容易解码第一个字节根据其定义告诉您是否正在寻找另一个字节,下一个字节指示您是否需要进一步查看等等。您不会像解码 mips 或 arm 或其他设计不同的东西那样解码 x86。它们都有一个解码,说查看这些位并确定指令或确定我是否需要更多位,但是 x86 以一种方式执行,mips 以另一种方式执行,arm 以另一种方式执行。各有利弊。

像 x86 一样的 CISC 虽然更像是一个流程图,但第一个字节告诉您转到第 X 页,该页要么有完整的答案,要么它说获取下一个字节并基于此转到附录 X 中的第 Y 页。

有些房子只有一个住户,地址/位置会将您带到一个人。有些人不止一个,一旦你根据地址到达房子,那么你需要更多信息来确定你感兴趣的人或宠物。第一条信息,街道地址符合标准,但用于隔离该房屋内的人/宠物的信息符合该房屋的标准。指令的第一个字节是操作码。但是基于操作码,如果有额外的字节,那么这些字节是特定于操作码的,正如我们在上面看到的。 b8 78 56 34 12 对于 0xB8,第二个字节是立即值的一部分。有很多你可以查找第二个字节在哪里进一步解码指令

mov eax,eax
mov eax,ebx
mov eax,ecx
mov eax,edx


   0:   89 c0                   mov eax,eax
   2:   89 d8                   mov eax,ebx
   4:   89 c8                   mov eax,ecx
   6:   89 d0                   mov eax,edx

对于 0x89 操作码,在这些情况下,第二个字节不是数据,而是进一步定义指令。

确实,第二个字节的解码不仅限于该操作码,许多指令将共享这些位的相同解码,例如确定 ah,al,ax,eax,bh,bl,bx...等.这在英特尔文档以及无数其他书籍和网站中都有记录。

真正的文档是芯片本身的源代码,因为我们很少接触到我们获得的文档,这些文档通常不是由逻辑作者编写,然后可能由技术作家在每一步润色一些信息可能会丢失或令人困惑。一些供应商比其他供应商更好,他们的文档的某些版本比其他供应商更好。

x86 几乎是你想学习的最后一个指令集,拥有一个不是正当理由,对于你拥有的每一个 x86,就在那个盒子里有许多非 x86 处理器,加上你拥有的每一个 x86相当多,几十个非x86设备。如果教育和学习是目标,那么无论如何您都希望从模拟器开始,大大提高您成功的机会,并且崩溃不会造成太大的伤害。有更好的指令集,比如 msp430 和 pdp11 ,这显然是影响它的原因。手臂,拇指,后来进入 mips 及其细微差别,然后在我不会从 x86 开始的 8 位中,我会选择其他 6502 或其他东西。然后也许如果好奇 8088/8086 使用模拟器和互联网回程机器上的旧文档,那么最后是 x86,如 80386、80486 和 x86-64。首先深入 x86-64 肯定是为了痛苦,真正让人们自虐。如果你仍然觉得你必须这样做,那么这条痛苦路径中不那么痛苦的路径是从 8088/8086 开始,使用旧手册和 dosbox 或 bochs 或许多其他仿真器。一旦你打好了基础,那么他们在 32 位和 64 位的步骤中添加的内容可能更有意义,并且您不必被随着时间的推移添加的大量保护所迷惑,您可以开始干净和纯粹。

可变长度指令集的反汇编是一个需要解决的大问题,没有人解决它,因为他们不能完全解决。不可能。我曾经从反汇编程序开始学习所有新指令集。这些天我可能会做一个模拟器。获得一半成功机会的唯一方法是从有效的入口点开始。并按执行顺序解码,而不是通过二进制线性解码。那只会暴露一些代码。其余的(如果有的话)是基于数据的,您可以尝试模仿,但这也不是完美的。一方面,反汇编时的数据可能会改变运行时间。您甚至可以模拟该程序并运行它数天/数周,以发现特定指令正在查看的不同位置的各种数据值,但仍不能真正了解所有可能性。所以有些反汇编程序只是弄错了,但向您展示它好像它是正确的,而其他人则正确地告诉您,只是说我不知道​​这是什么......

如今,绝大多数二进制文件都已编译,因此数据路径大多是健全和完整的。但是从站立视频游戏日中获取一些 rom,例如小行星。你会看到类似这样的伪代码:

a = 0
if(a == 0) goto somewhere
b = 7

我们可以很容易地看到,条件分支实际上是一个无条件的反汇编,我们需要将条件分支之后的指令视为可能的执行路径。但是,您在该 rom 中发现的是,随后的指令是实际数据,然后是指令。 a 1 表示操作码字节 a 2 和 3 表示该指令的附加字节,更多伪代码

1 a = 0;
2
1 if(a == 0) goto somewhere
2
3
1 b = 7.
2
3
1
2
3

但是当我们继续解码所有所谓的有效执行路径时,我们发现

1 b = 7.
2 
3  <--- is a branch destination
1
2
3

这是一个操作码字节而不是指令中的后面字节,所以现在有一个好的反汇编程序会告诉你这个冲突。然后人类必须检查这些路径,确定哪一个是有效的 a=0.... 路径或 b = 7。假设 a = 0 并且随后的条件分支是有效反汇编的一部分,那么看起来这实际上是一个无条件分支,并且有几个数据字节或填充或随后的一些代码。这可能是故意的,因为在当时更常见的是故意扔掉反汇编程序,或者它可能是手动破解二进制文件而不是重新构建整个项目并烧毁 rom 的结果。 (继续阅读我认为是防御者,在贸易展前一天晚上然后第二天在酒店房间里破解了二进制文件)。这些字节可能是经过手工修改以绕过错误的其他指令。 6502 是一个很好的起点,如果您想编写反汇编程序,那么许多游戏 rom 没有像 z80 或 8088/8086 那样多的指令,它们通过使用第二个字节将 256 条指令的原始潜力乘以更长的列表。早期的 PIC 或 msp430 作为第一个反汇编程序会容易得多,因为它们只有一打或两条指令。 Msp430 有一个经过调试/支持的 gnu 后端(llvm 没有经过调试也不支持,因此请避免使用它),因此如果对学习指令集感兴趣,您可以轻松获得工具。

当你有一个固定的指令长度时,比如不使用 16 位指令时的 mips 或不使用 16 位拇指时的 arm。 (并且指令集说指令必须对齐(而不是 risc-v))您可以通过内存线性反汇编,您发现某些“指令”没有意义或未定义,但您只是磨练,稍后人类会将这些视为数据而不是指令,但那些指令将是有意义的。不幸的是 mips 和 arm 有辅助指令集,它们的解码方式和规则完全不同,所以你也不能简单地反汇编一个 arm 二进制文件,对于今天编译器生成的东西,你也需要按执行顺序来做,你更有可能得到大多数指令已解码,但会有一些跳转表使您的工作陷入僵局,导致代码块未正确反汇编。

因此,虽然罗嗦,但简短的回答是尽可能相信反汇编程序。如果您从已知有效的入口点按执行顺序执行指令并查看处理器的文档,这些指令很容易解码。

【讨论】:

  • 在我编辑问题使其更具体并强调它专门询问“操作码/r”字段之前,您是否开始写这篇诽谤?旧标题是垃圾,但它看起来仍然是一个相当具体的问题。 (而且我在这个关于 x86 的漫无边际的咆哮中没有看到答案。)问题中没有提到通过静态分析找到指令边界的问题,或者由可变长度指令集引起的任何其他问题.这看起来像是一个没有答案的咆哮。
  • 虽然这些字节的位可能会直接解码到四个寄存器之一,但不太可能,因为这些指令集的设计方式并非如此。 与 8080 及更早版本不同只有几个寄存器和不同版本的相同指令具有不同隐式寄存器的 ISA,当 x86 具有包含显式寄存器的单字节操作码时,它是对寄存器进行编码的操作码的低 3 位。例如mov r32, imm32 is b8 + rd,其中 rd 是目标寄存器代码。
  • 你得到了我的支持,只是为了写这堵文字墙。
  • @Peter Cordes 到目前为止,这是最好的答案,并回答了我最初提出的问题,由于它有多长,我还没有读完,但它准确地说明了我在寻找什么,如何解析字节码。当我最初写这个问题时,我并不确切知道如何提出这个问题,不幸的是,我在stackoverflow上有很多问题(不知道如何正确地写我的问题,就是这样。)但我最初的问题可能是更好地表述为“正确的反汇编程序如何将字节码解析为操作码”
  • @bcvdgfdagfewafdsaf:您对 \@kabanus 的回答发表评论说它没有给您任何新信息,所以我假设您已经了解了该图中显示的 x86-64 指令格式的基础知识,并且只是询问是否使用/r 字段作为额外的操作码位,如果您已经到了查找操作码字节的地步。这是您的问题唯一关注的问题。所以,是的,如果这个答案有帮助,那就太好了,是的,你写的问题并没有清楚地表达你想知道的内容。 (这将是解释指令长度解码的其他问题的重复。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-07-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多