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

How does an instruction decoder tell the difference between a prefix and a primary opcode?

我正在努力思考 x86 指令编码格式。我阅读的所有资料仍然使这个主题令人困惑。我开始有点理解它了,但我难以理解的一件事是 CPU 指令解码器如何区分操作码前缀和操作码。

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

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

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

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

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


解码器必须为此(以及其他)知道当前模式;例如 x86-64 删除了 1 字节的 inc/dec reg 操作码用作 REX 前缀。 (). We can even use this difference to write polyglot machine code that runs differently when decoded in , or even distinguish all 3 mode sizes.

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

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

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

多字节 VEX 和 EVEX 前缀在 32 位模式下并不那么简单。 例如,VEX 前缀在 64 位以外的模式下与 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 前缀中的某些位被反转,所以 c4c5 之后字节的前 2 位将始终是 1 在 32 位模式下而不是 0。 那是 ModRM 中的“模式”字段,11 表示注册)。

(有趣的事实:VEX 前缀在 16 位实模式下无法识别,显然是因为某些软件使用了 LES / LDS 的无效编码作为故意陷阱,在#UD 异常处理程序中进行了整理。VEX不过,前缀 在 16 位 protected 模式下被识别。)


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

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

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

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

Agner Fog 的博客文章 Stop the instruction set war 发表于 2009 年(AVX 发布后不久,在第一个支持它的硬件出现之前)有 table 剩余未使用的编码 space 用于将来的扩展,以及关于它被“分配”给 AMD、Intel 或 Via 的一些说明。

相关/示例

  • How to tell the length of an x86 instruction?(包括我的回答)有一些关于 x86 机器码的更多细节。
  • https://codegolf.stackexchange.com/questions/133486/find-an-illegal-string/133622#133622(在codegolf.SE - 最短的字节序列,如果不跳过肯定会#UD错误。它必须足够长,不能被CPU 作为 mov r64, imm64 的立即数。)
  • - 在错误的位置开始解码并将另一条指令的中间解码为其他内容的示例。

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

(这 与前缀没有 真正相关,但总的来说,了解规则如何适用于奇怪的情况有助于理解事情的确切运作。)

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

幸运的是 如此简单的静态反汇编(例如通过 objdump -dndisasm,与 IDA 相反)找到了与实际 运行 程序相同的指令边界会.

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

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

有关此示例,请参阅我对 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 

可以使用消耗更多后期字节的操作码来做到这一点,比如3D <dword> cmp eax, imm32。当 CPU 看到一个 3D 操作码字节时,它会抓取接下来的 4 个字节作为立即数。如果您稍后跳转到这 4 个字节,它们将被视为 prefix/opcodes 并且一切都会正常工作(性能问题除外),无论这些字节之前如何被解码为指令的不同部分。 CPU 必须保持一次解码和执行 1 条指令的错觉,而不是性能。

我从@Ira Baxter 在 Can assembled ASM code result in more than a single possible way (except for offset values)?

上的回答中了解到这个技巧