7

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

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

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

4

1 回答 1

7

传统的(单字节)前缀与您所说的操作码字节不同,因此状态机可以记住在到达操作码字节之前看到了哪些前缀。

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

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


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

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

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

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

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

在 legacy / compat 模式下,当 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 的博客文章Stop the instruction set war from 2009(在宣布 AVX 后不久,在第一个支持它的硬件之前)有一张剩余未使用编码空间的表格,用于未来的扩展,以及一些关于它被“分配”给 AMD、英特尔的注释,或通过。

相关/例子


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

(这实际上与前缀无关,但总的来说,了解规则如何适用于奇怪的情况有助于准确理解事情的工作原理。)

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

幸运的是,编译器生成的代码不会那样做,所以简单的静态反汇编(例如,通过objdump -dor ndisasm,与 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 

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

我从@Ira Baxter 对 Canassemble ASM code result in more than one possible way(偏移值除外)的回答中了解到这个技巧?

于 2021-08-23T21:42:07.953 回答