指令解码器如何区分前缀和主要操作码之间的区别?

Dan*_*ano 5 x86 assembly cpu-architecture machine-code instruction-encoding

我正在尝试围绕 x86 指令编码格式。我阅读的所有资料仍然使这个主题变得混乱。我开始有点理解它,但我无法理解的一件事是 CPU 指令解码器如何区分操作码前缀和操作码。

我知道指令的整个格式基本上取决于操作码(当然在操作码中定义了额外的位字段)。有时指令没有前缀,操作码是第一个字节。解码器怎么知道?

我假设指令解码器能够分辨出差异,因为操作码字节和前缀字节不会共享相同的二进制值。因此解码器可以判断字节中唯一的二进制数是指令还是前缀。例如(在本例中,我们将坚持使用单字节操作码)REXLOCK前缀不会与架构指令集中的任何操作码共享相同的字节值。

Pet*_*des 5

传统(单字节)前缀与您所说的操作码字节不同,因此状态机只能记住它看到的前缀,直到到达操作码字节为止。

0f2 字节操作码的转义字节并不是真正的前缀。它必须与第二个操作码字节相邻。因此,在 a 之后0f任何字节都是操作码,即使它是类似的东西f2,否则将是前缀。(这也适用于 SSSE3 及更高版本的后续0f 3a0f 382 字节转义,或编码这些转义序列之一的 VEX/EVEX 前缀)。

如果您查看操作码映射,单字节前缀和操作码之间没有不明确的条目。(例如http://ref.x86asm.net/coder64.html,并注意 2 字节 0F .. 操作码是如何单独列出的)。


解码器必须知道这个(和其他事情)的当前模式;例如,x86-64 删除了inc/dec reg用作 REX 前缀的 1 字节操作码。(在 x86-x64 中不同或完全删除的 x86 32 位操作码)。我们甚至可以使用这种差异来编写在32 位与 64 位模式下解码时运行不同的多语言机器代码,甚至可以区分所有 3 种模式大小

x86 机器码是一个不能自同步的字节流(例如,ModRM 或立即数可以是任何字节)。CPU 总是知道从哪里开始解码,无论是跳转目标还是前一条指令结束后的字节。这是指令的开始(包括前缀)。

内存中的字节只是字节,只有在被 CPU 解码时才成为指令。(虽然在普通程序中,简单地从该.text部分的顶部反汇编确实会给你程序的指令。自修改和混淆的代码是不正常的。)

AVX / AVX-512:与操作码重叠的多字节前缀

多字节 VEX 和 EVEX 前缀在 32 位模式下并不那么简单。例如,在 64 位以外的模式中,VEX 前缀与 LES 和 LDS 的无效编码重叠。(LES 和 LDS 的操作码c4c5操作码在 64 位模式下总是无效的,除了作为 VEX 前缀。) https://wiki.osdev.org/X86-64_Instruction_Encoding#VEX.2FXOP_opcodes

在传统/兼容模式下,当 AVX(VEX 前缀)和 AVX-512(EVEX 前缀)时,没有任何空闲字节不是操作码或前缀,因此扩展的唯一空间是作为操作码的编码仅对有限的 ModRM 字节集有效。(例如 LES / LDS 需要内存源,而不是寄存器 - 这就是为什么有些位在 VEX 前缀中被反转,因此c4or之后的字节的前 2 位c5将始终1处于 32 位模式而不是0。这就是“模式” ModRM 中的字段,11表示寄存器)。

(有趣的事实:VEX前缀不是在16位实模式的认可,显然是因为一些软件使用的LES / LDS同样无效编码为故意的陷阱,要在#UD异常处理程序整理出来VEX前缀。在16认可-位保护模式,虽然。)


AMD64 通过删除像 AAM 这样的指令以及 LES/LDS(以及用作 REX 前缀的一字节inc/dec reg编码)释放了几个字节,但是 CPU 供应商继续关心 32 位模式并且没有添加任何扩展仅在 64 位模式下可用,它可以简单地利用那些免费的操作码字节。这意味着找到将新指令编码塞入 32 位机器代码中越来越小的间隙的方法。(通常通过强制前缀,例如rep bsr=lzcnt在具有该功能的 CPU 上,这会产生不同的结果。)

因此,支持 AVX/BMI1/2 的现代 CPU的解码器必须查看多个字节来决定这是有效 AVX 或其他 VEX 编码指令的前缀,还是在 32 位模式下是否应解码为 LES 或LDS。(我想看看指令的其余部分来决定是否应该#UD)。

但是现代 CPU 无论如何都要一次查看 16 或 32 个字节以并行查找指令边界。(然后将这些指令字节组提供给实际的解码器,再次并行。) https://www.realworldtech.com/sandy-bridge/4/

AMD XOP使用的前缀方案也是如此,它很像 VEX。

Agner Fog 的博客文章从 2009 年开始停止指令集战争(在 AVX 宣布后不久,在第一个支持它的硬件之前)有一个剩余未使用的编码空间表,用于未来的扩展,以及一些关于它被“分配”给 AMD、英特尔的说明,或通过。

相关/例子


机器码技巧:以多种方式解码相同的字节

(这与前缀并没有真正的关系,但总的来说,了解规则如何应用于奇怪的情况可以帮助理解事物的工作原理。)

软件反汇编程序确实需要知道起点。如果混淆代码混合了代码和数据,并且实际执行跳转到如果您只是假设您可以按顺序解码而不跟随跳转时不会得到的位置,这可能会出现问题。

幸运的是,编译器生成的代码不会这样做,所以简单的静态反汇编(例如,通过objdump -dndisasm,而不是 IDA)找到了与实际运行程序相同的指令边界。

这对于运行混淆的机器代码来说不是问题;CPU 只做它告诉它的事情,在你告诉它跳转到的地方之前从不关心字节。在运行/单步执行程序的情况下反汇编是一件困难的事情,尤其是有可能自我修改代码并跳转到一个天真的反汇编者会认为是早期指令的中间部分。

混淆的机器代码甚至可以以一种方式对指令进行解码,然后跳回到该指令的中间位置,将后面的字节作为操作码(或前缀 + 操作码)。如果您这样做,带有 uop 缓存或在 I-cache 中标记指令边界的现代 CPU 运行缓慢(但正确),因此它更像是一个有趣的代码高尔夫技巧(以牺牲速度为代价的极端代码大小优化)或混淆技术.

有关此示例,请参阅我对Golf a Custom Fibonacci Sequence 的codegolf.SE x86 机器代码答案。我将摘录与 CPU 在循环回 后看到的内容一致的反汇编cfib.loop,但请注意,第一次迭代的解码方式不同。所以我只使用循环外的 1 个字节而不是 2 个字节来有效地跳到第一次迭代开始的中间。有关完整说明和其他反汇编,请参阅链接的答案。

0000000000401070 <cfib>:
  401070:       eb                      .byte 0xeb      # jmp rel8 consuming the 01 add opcode as a rel8
0000000000401071 <cfib.loop>:
  401071:       01 d0                   add    eax,edx
# loop entry point on first iteration, jumping over the ModRM byte (D0) of the ADD
    (entry on first iteration):
  401073:       92                      xchg   edx,eax
  401074:       e2 fb                   loop   401071 <cfib.loop>
  401076:       c3                      ret 
Run Code Online (Sandbox Code Playgroud)

可以使用消耗更多后续字节的操作码执行此操作,例如3D <dword> cmp eax, imm32. 当 CPU 看到一个3D操作码字节时,它会抓取接下来的 4 个字节作为立即数。如果您稍后跳转到这 4 个字节,它们将被视为前缀/操作码,并且无论这些字节之前如何被解码为指令的不同部分,一切都将相同(性能问题除外)。除了性能之外,CPU 还必须保持一次解码和执行 1 条指令的假象。

我从@Ira Baxter 关于 Canassembled ASM code result in more than one可能的方式(偏移值除外)的回答中了解到了这个技巧

  • 我还有关于 x86 编码的其他问题,这些问题不适合这个特定问题,例如 EVEX VEX 和 SSE 是什么。我只会将此问题标记为已解决,并发布有关**那些**其他事情的更多问题。我再次感谢大家花时间帮助我理解这种复杂的指令格式! (2认同)