奔腾III CPU如何处理来自同一组的多个指令前缀?

Dav*_*own 1 x86 assembly intel x86-emulation

英特尔x86规范规定,使用同一组中的多个指令前缀会导致未定义的行为.实际上,Pentium III Coppermine CPU在这种情况下是如何做出反应的?可悲的是,我没有芯片可以测试.

Cod*_*ray 5

虽然你已经知道了这一点,但我首先要说清楚它.x86指令最多可以有4个前缀(每个前缀来自不同的组),这些前缀会改变处理器对指令的解释.从英特尔IA-32架构手册,第2A卷,第2.1节:

2.1受保护模式,实地址模式和VIRTUAL-8086模式的指令格式

Intel 64和IA-32架构指令编码是图2-1中所示格式的子集.指令由可选指令前缀(按任意顺序),主操作码字节(最多3个字节),寻址形式说明符(如果需要)组成,包括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编码.Repeat-Not-Zero前缀仅适用于字符串和输入/输出指令.(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前缀在近CALL,近RET,近JMP或近Jcc指令之前时(参见英特尔®64 和IA-32架构软件开发人员手册第1卷第17章"英特尔®MPX" ) .
  • 第2组
    • 段覆盖前缀:
      • 2EH-CS段覆盖(保留用于任何分支指令).
      • 36H-SS段覆盖前缀(保留用于任何分支指令).
      • 3EH-DS段覆盖前缀(保留用于任何分支指令).
      • 26H-ES段覆盖前缀(保留用于任何分支指令).
      • 64H-FS段覆盖前缀(保留用于任何分支指令).
      • 65H-GS段覆盖前缀(保留用于任何分支指令).
    • 分支提示(不再使用;保留):
      • 2EH-Branch未被采用(仅用于Jcc指令).
      • 采用3EH分支(仅与Jcc指令一起使用).
  • 第3组
    • 操作数大小覆盖前缀使用66H编码(66H也用作某些指令的强制前缀).
  • 第4组
    • 67H-地址大小覆盖前缀.

LOCK前缀(F0H)强制执行确保在多处理器环境中独占使用共享内存的操作.有关此前缀的说明,请参见第3章"指令集参考,AL"中的"LOCK-Assert 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组:load(MOV)指令上的多个段覆盖前缀.

    同样,最后一个前缀是唯一重要的前缀.所有其他人都被忽略了 而且,这似乎是所有x86处理器的标准配置.

    (我没有费心去测试分支提示前缀,无论是单独使用还是与段覆盖前缀结合使用,因为除了Pentium 4之外,所有处理器都会忽略这些分支提示.)

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

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

    各种来源在线确认这是所有x86处理器的标准行为.

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

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

总结:实际上,除了来自特定组的最后一个前缀之外的所有前缀都将被忽略.在指令上遇到的最后一个前缀是生效的那个.所有先前的冗余或无意义前缀都将被忽略.对于所有 x86处理器而言似乎都是如此,这意味着仿真代码不需要针对任何特定的生成/微架构特殊情况执行此行为.但是,在一个上下文中无效的前缀可能会被重新用于在将来的处理器上具有某些含义,因此需要注意这一点.

如果可能的话,为了避免头痛,您可以考虑将这种解释性工作卸载到您的解码器.具体来说,一个由Intel,Intel 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的调试器进行测试.