PSA:实际硬件行为优先于供应商文档。或者,正如他们所说的…信任但要验证。
最近有读者抱怨英特尔和AMD如何无法以相同的方式实施SGDT和SIDT指令。 AMD文档指出,这些指令将忽略任何操作数大小的前缀,并始终存储完整的32位基地址。另一方面,英特尔文档指出,SGDT / SIDT使用16位操作数大小存储24位基址,第4个字节清零,而使用32位操作数大小则存储所有32位基址。
真是一团糟,对不对?贫穷的开发人员应该如何编写可在所有CPU上使用的代码,为什么AMD发明自己的东西呢?然而现实却有所不同……
事实要简单得多,也要合乎逻辑:AMD并未发明不兼容的行为,AMD处理器的行为与英特尔CPU相同。这是否表示AMD文档不正确?当然不是-只是英特尔的文档是一本精美的小说,而AMD的直接文档则描述了AMD和Intel处理器自1985年以来所做的事情以及一直以来所做的事情。
难以置信?也许。但是,数十年来,英特尔在这方面的文档一直从矛盾变为错误。据OS / 2博物馆所能确定的那样,英特尔的32位CPU在执行SGDT / SIDT指令时总是忽略操作数大小前缀,并存储GDT / IDT基数的完整32位。尽管文档进行了更改,但CPU的行为从未改变。
测试一小部分随机处理器可能不是令人信服的证明,但是至少有一个相当广泛的软件依赖于始终存储完整的32位,否则将无法正常工作。
微软自己的Win32s(测试版1.30c)包含在WIN32S16.DLL内的16位代码段中执行SGDT的代码(名称清楚地表明了位数)。但是CPU在启用分页的情况下运行,并且GDT映射到2GB以上的虚拟地址,即始终设置高位。如果SGDT指令清除了最高字节,则Win32s将崩溃:
若要触发崩溃,可能需要在启用了32位磁盘访问或32位文件访问的情况下为工作组运行Windows 3.11(当两者均关闭时,崩溃似乎没有发生;打开两者中的任何一个引发崩溃)。
当SGDT仿真遵循Intel文档时,Win32s就会中断,如上所示。当然,解决方法是模拟英特尔处理器的功能,而不是英特尔文档中的内容。
Windows 3.11 for Workgroups + Win32s 1.30c + Freecell并不是一个非常奇特的组合。它早在1990年代就可在Intel CPU上运行,如今,当使用硬件虚拟化并且SGDT之类的指令直接由CPU执行(未仿真)时,它仍可在Intel处理器上运行。
那么虚构的SGDT / SIDT文档是从哪里来的呢?可以合理地假设文档是在实现完成之前编写的,但事实并非如此。 1986年的Intel 386参考手册以及Intel的1992 i486 PRM简直就是精神分裂症。
LGDT和LIDT指令分别将线性基本地址和极限值从内存中的六字节数据操作数加载到GDTR或IDTR中。如果将16位操作数与LGDT或LIDT一起使用,则该寄存器加载了16位限制和24位基数,并且不使用六字节数据操作数的高8位。如果使用32位操作数,则加载16位限制和32位基数;否则,将加载32位基数。六字节操作数的高八位用作高阶基地址位。
SGDT和SIDT指令始终存储在六字节数据操作数的所有48位中。对于80286,在执行SGDT或SIDT之后,高八位未定义。对于80386,对于16位操作数和32位操作数,高8位均写入高8位地址位。如果LGDT或LIDT与16位操作数一起使用以加载SGDT或SIDT存储的寄存器,则高8位存储为零。
上面简要介绍了32位Intel处理器的实际行为。但是,等等,还不是全部。 SGDT / SIDT指导文档说:
SGDT / SIDT将描述符表寄存器的内容复制到操作数指示的六个字节的存储器中。寄存器的LIMIT字段分配给有效地址的第一个字。如果操作数大小属性为32位,则将接下来的三个字节分配给寄存器的BASE字段,并且将第四个字节写入零。最后一个字节是不确定的。否则,如果操作数大小属性为16位,则将为寄存器的32位BASE字段分配接下来的四个字节。
如果未引用高8位中的值,则SGDT / SIDT指令的16位形式与80286兼容。如果操作数大小属性为16位,则80286在这些高位中存储1,而在80386中存储0。 iAPX 286程序员参考手册中的SGDT / SIDT指令将这些位指定为未定义。
敏锐的读者已经注意到SGDT / SIDT文档毫无意义,它声称16位指令存储32位基数,而32位指令存储24位基数。也许是一个简单的错字,或者暗示整个事情都是可疑的。
当然,更大的问题是LxDT和SxDT文档完全矛盾。 SGDT文档说操作数大小很重要,但是LGDT文档说(对于SGDT)无关紧要。至少有一个错误。
如果操作数大小属性为16位,则接下来的三个字节分配给寄存器的BASE字段,而第四个字节未定义。否则,如果操作数大小属性为32位,则为后四个字节分配寄存器的32位BASE字段。
现在,这只是不正确的,但不再是显而易见的废话。仍然存在一个矛盾,SGDT / SIDT文档在主体中声称16位操作数的大小使第四个BASE字节未定义,而兼容性说明说它是用零写的。而且486 PRM仍有LGDT / LIDT和SGDT / SIDT指令文档相互矛盾。为了改善程序员的困惑,486 PRM还为SGDT / SIDT提供了以下伪代码:
没错,该伪代码是正确的,紧随其后的文本还显示了其他内容!相信什么,这就是问题……(答案:CPU本身!)
奔腾文档(订购号241430-004,1995年)提供了与486 PRM相同的自相矛盾信息。
Pentium Pro文档(1995年12月,订购号232691-001)改进了某种方式。 LGDT / LIDT指导文档不再描述SGDT / SIDT行为。现在,SGDT / SIDT文档中的伪代码与文本匹配。奇怪的是,当伪代码本应为“ IF指令为SIDT”时,它以“ IF指令为IDTR”开头。同样,让我们用一个简单的错字来概括一下。
无论如何,Pentium Pro SGDT / SIDT文档与其自身保持一致!好极了!但这始终是错误的...哎呀。再说一次,十分之二还不错吧?
截至撰写本文时,最新的英特尔SDM(325383-062,2017年3月)仍然显示相同的错误信息。在此过程中,SGDT和SIDT被分开记录了。可能是这样的结果,SGDT的伪代码现在以无意义的“ IF指令为SGDT”语句开头-嗯,您认为SGDT还有什么其他指令? (有类似的SIDT冗余伪代码。)有关64位支持的信息是在几年前添加的,并进行了一些修订,并记录了新的CR4.UMIP位。当涉及到16位操作数时,描述和伪代码与1995年以来一样错误。
整个情况看起来像是由于SxDT和LxDT指令之间的不对称引起的误解。英特尔和AMD都同意,对于LGDT和LIDT,操作数大小会有所不同,如果使用16位操作数大小,则只会加载基地址的24位(高位为零)。如果考虑到这一点,则SGDT / SIDT不需要做任何特殊的事情。
纯16位软件将始终以GDT / IDT基数的高8位为零运行,因为LGDT / LIDT无能为力,因此SGDT / SIDT只需要按原样存储实际的32位基数,无论操作数大小如何。换句话说,对于纯16位代码,SGDT存储24位基址加一个零字节或32位基址都没有关系,结果将是相同的。
但是,如果16位代码在32位环境中运行,则SGDT / SIDT将存储完整的32位基数。这似乎合乎逻辑并且很有用,因为存储32位地址的24位会损坏数据(请参见Win32s)。
看到这些年来这些文档是如何出错的非常有趣。 最初,它是部分正确的,但是不一致,并且不一致之处已得到解决,很遗憾,这些错误信息删除了正确的信息,只保留了错误的文档。 有时,英特尔给人一种明显的印象,即处理器架构师和文档编写者之间的对话不多。