1

Intel x86 规范指出,使用来自同一组的多个指令前缀会导致未定义的行为。在实践中,Pentium III Coppermine CPU 在这种情况下是如何反应的?遗憾的是我没有芯片可以测试。

4

1 回答 1

6

尽管您已经知道这一点,但为了清楚起见,我将首先说明它。x86 指令最多可以有 4 个前缀(每个来自不同的组),它们会改变处理器对指令的解释。来自英特尔 IA-32 架构手册,第 2A 卷,第 2.1 节:

2.1 保护模式、实地址模式和虚拟 8086 模式的指令格式

Intel 64 和 IA-32 架构指令编码是图 2-1 所示格式的子集。指令由可选的指令前缀(以任何顺序)、主操作码字节(最多三个字节)、由 ModR/M 字节和有时是 SIB(Scale-Index-Base)字节组成的寻址形式说明符(如果需要)组成,位移(如果需要)和立即数据字段(如果需要)。


图 2-1。Intel 64 和 IA-32 架构指令格式

2.1.1 指令前缀

指令前缀分为四组,每组都有一组允许的前缀代码。对于每条指令,只能从四个组(第 1、2、3、4 组)中的每一个中包含最多一个前缀代码。组 1 到 4 可以以任何相对于彼此的顺序放置。

  • 第 1 组
    • 锁定和重复前缀:
      • LOCK 前缀使用 F0H 编码。
      • REPNE/REPNZ 前缀使用 F2H 编码。重复非零前缀仅适用于字符串和输入/输出指令。(F2H 也用作某些指令的强制前缀。)
      • REP 或 REPE/REPZ 使用 F3H 编码。重复前缀仅适用于字符串和输入/输出指令。F3H 也用作 POPCNT、LZCNT 和 ADOX 指令的强制前缀。
    • 如果满足以下条件,则使用 F2H 对绑定前缀进行编码:
      • CPUID.(EAX=07H, ECX=0):EBX.MPX[bit 14] 已设置。
      • BNDCFGU.EN 和/或 IA32_BNDCFGS.EN 已设置。
    • 当 F2 前缀位于 near CALL、near RET、near JMP 或 Near Jcc 指令之前(请参阅英特尔® 64 和 IA-32 架构软件开发人员手册第 1 卷的第 17 章“英特尔® MPX” ) .
  • 第 2 组
    • 段覆盖前缀:
      • 2EH—CS 段覆盖(保留与任何分支指令一起使用)。
      • 36H—SS 段覆盖前缀(保留与任何分支指令一起使用)。
      • 3EH—DS 段覆盖前缀(保留与任何分支指令一起使用)。
      • 26H - ES 段覆盖前缀(保留与任何分支指令一起使用)。
      • 64H—FS 段覆盖前缀(保留与任何分支指令一起使用)。
      • 65H—GS 段覆盖前缀(保留与任何分支指令一起使用)。
    • 分支提示(不再使用;保留)
      • 2EH - 未采用分支(仅与 Jcc 指令一起使用)。
      • 3EH - 采用分支(仅与 Jcc 指令一起使用)。
  • 第 3 组
    • 操作数大小覆盖前缀使用 66H 编码(66H 也用作某些指令的强制前缀)。
  • 第 4 组
    • 67H - 地址大小覆盖前缀。

LOCK 前缀 (F0H) 强制执行确保在多处理器环境中独占使用共享内存的操作。有关此前缀的说明,请参见第 3 章“指令集参考,AL”中的“LOCK—断言 LOCK# 信号前缀”。

重复前缀 (F2H, F3H) 导致对字符串的每个元素重复一条指令。这些前缀只能用于字符串和 I/O 指令(MOVS、CMPS、SCAS、LODS、STOS、INS 和 OUTS)。保留在其他 Intel 64 或 IA-32 指令中使用重复前缀和/或未定义的操作码;此类使用可能会导致不可预知的行为。

一些指令可能使用 F2H,F3H 作为强制前缀来表达不同的功能。

分支提示前缀(2EH、3EH)允许程序向处理器提示最可能的分支代码路径。仅将这些前缀与条件分支指令 (Jcc) 一起使用。保留对 Intel 64 或 IA-32 指令的分支提示前缀和/或其他未定义操作码的其他使用;此类使用可能会导致不可预知的行为。

操作数大小覆盖前缀允许程序在 16 位和 32 位操作数大小之间切换。任何一种尺寸都可以是默认值;使用前缀选择非默认大小。

一些 SSE2/SSE3/SSSE3/SSE4 指令和使用三字节主操作码字节序列的指令可能使用 66H 作为强制前缀来表达不同的功能。

保留 66H 前缀的其他用途;此类使用可能会导致不可预知的行为。

地址大小覆盖前缀 (67H) 允许程序在 16 位和 32 位寻址之间切换。任何一种尺寸都可以是默认值;前缀选择非默认大小。当指令的操作数不驻留在内存中时,使用此前缀和/或其他未定义的操作码被保留;此类使用可能会导致不可预知的行为。

请注意,它实际上并没有说来自同一组的多个指令前缀会导致“未定义的行为”。相反,它只是说从每个组中最多包含一个是“唯一有用的”。这使得事情变得非常不确定。

在我看来,您从规范中获得的唯一正式保证是某些特定的指令和前缀组合可能导致“不可预测的行为”或异常,并且任何超过 15 个字节的单条指令都会导致“无效的操作码” “ 例外。

这使我们可以根据经验测试每组指令中的多个前缀,否则它们会受到支持。为此,根据要求,我在 Pentium III Coppermine 1上进行了以下测试:

  1. 第 1 组:指令 ( ) 上多个REPE( F3) 和REPNE( F2) 前缀的各种组合。CMPSBA6

    只有遇到的最后一个前缀才有效;忽略它之前的同一组中的其他前缀。

    事实上,这似乎是所有 x86 处理器的标准行为,并且与 Microsoft 的反汇编程序显示代码的方式一致。前导(忽略)前缀不显示为指令的一部分。

  2. 第 2 组MOV:加载 ( ) 指令上的多个段覆盖前缀。

    同样,最后一个前缀是唯一重要的前缀。所有其他都被忽略。同样,这似乎是所有 x86 处理器的标准。

    (我没有费心去测试分支提示前缀,无论是单独还是与段覆盖前缀组合,因为这些分支提示在除了奔腾 4 之外的所有处理器上都被忽略了。)

  3. 第 3 组:多个操作数大小覆盖前缀 ( 66h)。

    重复的前缀被忽略,因此多个66h前缀与一个前缀具有完全相同的效果66h。他们不会相互抵消或任何类似的事情。

    各种在线来源证实这是所有 x86 处理器的标准行为。

  4. 第 4 组:多个地址大小覆盖前缀 ( 67h)。

    与第 3 组相同:忽略重复的前缀。

总结:实际上,除了来自特定组的最后一个前缀之外的所有前缀都被忽略。指令上遇到的最后一个前缀是生效的前缀。所有前面的冗余或无意义的前缀都将被忽略。这似乎适用于所有x86 处理器,这意味着模拟代码不需要为任何特定的代/微架构特殊情况下的这种行为。但是,在一种情况下没有影响的前缀可能会被重新用于对未来的处理器具有某种意义,所以这是需要注意的事情。

如果可能的话,为了省去你的麻烦,你可以考虑把这个解释工作交给你的解码器。具体来说,由英特尔编写的英特尔 XED 库GitHub 上的存储库)。您只需给它 1 到 15 个字节,它就会返回解码后的操作码(包括前缀)和操作数。解码是 x86 的难点,所以这应该可以为您省去很多麻烦。它实现了与这里描述的相同的算法——参见,例如这些注释这个代码

__
1具体来说,Intel Pentium III EB @ 866 MHz(系列 6,型号 8,步进 6,修订版 cC0)。这是一个 Socket 370 FC-PGA 芯片,在带有基于 Intel 815 的主板 (133 MHz FSB) 的 Compaq Deskpro EN 系统上运行。万一它很重要(显然不应该),操作环境是 Windows 2000 SP4。我使用 MASM 和 Visual Studio 的调试器进行测试。

于 2017-06-05T10:10:29.233 回答