Via C3处理器确实允许这样的操作,在启用MSR并执行未记录的
0F 3F
指令以激活
https://en.wikipedia.org/wiki/Alternate_Instruction_Set后,不强制执行通常的特权(环0)与非特权(环3)保护。 (不幸的是,Via Samuel II默认启用了允许此功能的MSR设置。他们也没有记录它,因此操作系统不知道他们应该关闭该功能。其他Via CPU默认禁用。)
请参阅Christopher Domas在DEF CON 26的演讲:
GOD MODE UNLOCKED Hardware Backdoors in redacted x86。
他还开发了一个AIS(Alternate Instruction Set)汇编程序:
https://github.com/xoreaxeaxeax/rosenbridge,以及用于激活它(或关闭漏洞!)的工具。
在运行
0F 3F
(跳转到EAX)后,AIS指令在4字节的RISC指令前面编码有3字节前缀。(与现有的x86指令编码不同,例如它接管了LEA和Bound,但您可以通过混合使用RISC和x86指令来做其他事情。)
AIS(备用指令集)使用类似RISC的固定宽度32位指令;因此,我们已经知道并非所有可能的uop都可以编码为RISC指令。计算机将x86指令解码为单个uop,例如6字节的
add eax,0x12345678
(带有32位立即数)。但是,32位指令字没有足够的空间容纳32位常量
和操作码和目标寄存器。因此,它是一种备用的类RISC ISA,仅限于后端可以执行的子集以及其RISC解码器可以从32位指令中解码的内容。
(相关: 处理器是否可以支持多个ISA?(例如:ARM + x86)讨论了这样做的一些挑战,不仅仅是一个噱头,比如具有实际性能期望的完整ARM模式,以及ARM所需的所有寻址模式和指令。)
uops不如实际的ARM或PowerPC好
@jalf的答案涵盖了大部分原因,但有一个有趣的细节没有提到:内部类似RISC的核心并非设计用于运行像ARM/PPC/MIPS这样的指令集。x86税不仅体现在耗电量高的解码器上,而且在某种程度上贯穿整个核心。也就是说,不仅仅是x86指令编码;每个具有奇怪语义的指令都是如此。
(除非那些笨拙的语义使用多个uops处理,这样你就可以只使用一个有用的uop。例如,对于带有原始uops的shl reg,cl
,你可以省略在移位计数为0
时不修改FLAGS的不方便要求,这就是为什么shl reg,cl
在Intel SnB系列上是3个uop,因此使用原始uops将是很好的选择。如果没有原始uops,你需要BMI2 shlx
来进行单个uop移位(完全不影响FLAGS)。)
假设英特尔创建了一种操作模式,其中指令流不是x86,而是直接映射到uops的指令。同时,每个CPU型号都有自己的ISA用于此模式,因此它们仍然可以随意更改内部结构,并使用最少量的晶体管公开它们以进行此备用格式的指令解码。
可以推测,您仍然只有相同数量的寄存器,映射到x86体系结构状态,因此x86操作系统可以在上下文切换时保存/恢复它,而无需使用CPU特定的指令集。但是,如果我们放弃这个实际限制,是的,我们可以有更多的寄存器,因为我们可以使用保留给微码的隐藏临时寄存器。
如果我们只是有备用解码器而没有对后续流水线阶段(执行单元)进行任何更改,这个 ISA 仍然会有许多 x86 的特点。它不会是一个非常好的 RISC 架构。没有单个指令会非常复杂,但一些其他的 x86 疯狂也仍然存在。
例如:像 cvtsi2sd xmm0,eax 这样的 int->FP 转换合并到 XMM 寄存器的低元素中,因此对旧寄存器值有一个(错误的)依赖性。即使 AVX 版本只需为要合并的寄存器取一个单独的参数,而不是零扩展到 XMM/YMM 寄存器。这显然不是通常想要的,所以 GCC 通常会做一个额外的 pxor xmm0,xmm0 来打破先前使用 XMM0 的依赖关系。同样,sqrtss xmm1,xmm2 合并到 xmm1。
再一次强调,没有人想要这个(或者说在极少数情况下他们可能会模拟它),但SSE1是在奔腾III时代设计的,当时英特尔的CPU将XMM寄存器处理为两个64位半部分。将其零扩展到完整的XMM寄存器将使每个标量浮点指令增加一个额外的uop,但打包的浮点SIMD指令已经是2个uop。但这是非常短视的,不久之后P4就有了全宽度的XMM寄存器。(尽管在放弃P4后返回P6核心的Pentium-M和Core(不是Core2)仍然具有半宽度的XMM硬件)。因此,英特尔在P-III上的短期收益对于编译器和未来必须运行具有额外指令或可能存在假依赖性的代码的CPU来说是长期的痛苦。
如果要为RISC ISA制作全新的解码器,可以让它挑选x86指令的部分作为RISC指令来暴露,这可以在某种程度上减轻核心的x86专业化。
指令编码可能不是固定大小的,因为单个微操作能够包含大量数据,这比所有指令都具有相同大小更加合理。单个微操作可以添加32位立即数和一个使用两个寄存器和32位位移地址模式的内存操作数。(在Sandy Bridge及以后版本中,只有单寄存器寻址模式可以与ALU操作混合微操作)。
微操作非常大,并且与固定宽度的ARM指令非常不相似。固定宽度的32位指令集一次仅能加载16位立即数,因此加载32位地址需要一个load-immediate低半部分/ loadhigh-immediate对。x86无需执行该操作,这有助于避免由于只有15个通用寄存器而限制常数保留在寄存器中的能力而变得糟糕。(15个通用寄存器比7个要好得多,但再次增加到31个帮助较少,我认为一些仿真发现。RSP通常不是通用寄存器,因此更像是15个通用寄存器和一个堆栈。)
TL;DR摘要:
总之,这个答案归结为“x86指令集可能是编程必须快速运行x86指令的CPU的最佳方式”,但希望能阐明原因。
前端和后端中的内部uop格式
参见微融合和寻址模式,其中介绍了在英特尔CPU上前端和后端uop格式所代表的差异之一。
注1:微码使用一些“隐藏”的寄存器作为临时寄存器。这些寄存器与x86架构寄存器一样被重命名,因此多uop指令可以乱序执行。
例如,在英特尔CPU上,xchg eax,ecx
的解码为3个uop(为什么?),我们最好的猜测是这些是类似于MOV的uop,执行tmp = eax; ecx = eax; eax = tmp;
。按照这个顺序,因为我测量了dst->src方向的延迟约为1个周期,而另一种方式则为2个周期。而且,这些移动uop不像常规的mov
指令;它们似乎不适合零延迟mov消除的候选。
另请参见http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/,其中提到尝试实验性地测量PRF大小,并需要考虑用于保存体系结构状态(包括隐藏寄存器)的物理寄存器。
在前端解码器之后,但在将寄存器重命名到物理寄存器文件的发出/重命名阶段之前,内部uop格式使用类似于x86寄存器编号的寄存器编号,但有足够的空间来寻址这些隐藏寄存器。
uop格式在乱序核心(ROB和RS)内部略有不同,也称为后端(发出/重命名阶段之后)。Haswell中,int / FP物理寄存器文件每个都有168个条目,因此uop中的每个寄存器字段需要足够宽以寻址那么多个寄存器。
由于重命名器在硬件中存在,我们最好使用它,而不是直接将静态调度的指令馈送到后端。因此,我们可以使用一组寄存器,其大小与x86体系结构寄存器+微代码临时寄存器相同,不会更大。
后端设计用于与避免WAW / WAR风险的前端重命名器一起工作,因此即使我们想要,也不能像顺序CPU那样使用它。它没有互锁来检测这些依赖关系; 这由发出/重命名处理。
如果我们能够在不经过发射/重命名阶段(现代英特尔流水线中最狭窄的点,例如Skylake上的4个宽度与后端的4个ALU + 2个加载 + 1个存储端口相比)的情况下将uops输入到后端,那会很不错。但是,如果这样做,我认为您无法静态调度代码以避免寄存器重用并覆盖仍需要的结果,如果缓存未命中导致加载停顿了很长时间。
因此,我们基本上需要将uops馈送到发射/重命名阶段,可能只绕过解码,而不是uop缓存或IDQ。然后,我们可以获得正常的乱序执行和合理的危险检测。寄存器分配表仅设计将16个加上一些整数寄存器重命名为168个条目的整数PRF。我们不能期望硬件将更大的逻辑寄存器集重命名为相同数量的物理寄存器;这需要一个更大的RAT。