有多少x86指令? (2016)

2021-04-22 11:55:18

令人惊讶的是,给出一个很好的答案(在本文中提出的问题)。这取决于你的统计方式,细节很有趣(无论如何)。

不要离开你挂起:英特尔有一个官方x86编码器/解码器库,名为xed。根据英特尔的XED,就像这种写作一样,有1503个定义的x86指令(“Xed Lingo的iClasses”),从AAA到XTest(这也包括AMD特定的扩展,顺便说一句)。直截了当,对吧?

嗯,这取决于你想要的依据。例如,根据XED,添加和锁添加是不同的“指令类”。许多装配程序员将考虑锁定前缀和锁添加添加添加的前缀,而不是一个不同的指令,而是xed不同意。实际上,为了执行目的,所以当前x86s。原子添加从常规添加中添加了非常不同的东西。前缀在其他地方杂耍的前缀:例如,来自rep movsd的movsd(复制一个32位字)不同的“指令类”(块复制多个32位字)? xed说是的。但它不能在所有上下文中以这种方式处理所有前缀。例如,操作数大小前缀(0x66)将在32位寄存器上运行的整数指令转换为在其较低的16位半部上运行的等效指令,但与REP或Lock前缀不同,XED不会将其视为单独的指令课程。如果你不同意这些选择中的任何一个选择,你的伯爵会出现不同。

这一切都取决于我们如何定义指令。它是一种独特的助记符吗?让我们首先查看我上面引用的文章所说的是到目前为止最常见的X86指令,占总样品集的33%:mov。因此,让我们在英特尔架构手册中查找MOV。 ...有3个不同的顶级条目? “mov-move”,“移动到/从控制寄存器移动”,“移动到/从调试寄存器移动”。后者与“常规”mov中的速度充分不同,以评估自己的文档页面,它们具有完全不同的指令编码(即使与常规mov的相同编码块),它们是特权指令,含量低于用户模式代码甚至允许执行它们。因此,它们也非常罕见,并且可能占测试样本的约0%。并且,肯定,XED将它们计为单独的指令类(MOV_CR和MO​​V_DR)。

因此,这些指令可能被称为MOV,但它们是奇怪的,特殊的雪花,以及从处理器的角度来看,它们在编码空间的不同部分和不同的规则中完全不同的指令。调用他们的mov基本上是官方英特尔汇编语言中的句子糖。

在句法糖的主题上:一些助记符只是别名。例如,SAL(移位算术留)是SHL的长期别名(左移)。两者都只是班次; “算术”和“逻辑”左移之间没有区别,如算术和逻辑右移之间的左移,但英特尔手册列表SAL(与SHL发生的编​​码相同)和我的所有X86汇编者曾经使用过接受它。在官方英特尔语法中,我们同时沿另一个方向错误地错误,因为至少两个助记符被分配了两次:我们已经看到了MOVSD的“复制”变体(它没有明确的操作数),但也有MOVSD “移动标量双”(始终有两个显式操作数),这是一个完全不同的指令(XED调用它movsd_xmm来消除歧义,并且CMPSD发生同样的问题)。

还有SSE比较cmpsd(两个操作数!)和CMPPS。 XED将它们计数为每个指令。但它们有一个8位立即常量字节,指定要执行的比较类型。但是拆卸通常不会产生难以读取的CMPSD XMM0,XMM1,2;它们将拆卸作为伪指令CMPLESD的指令(将标量与较小或相等的比较)相反。所以cmpsd一个指令(只有带有立即操作数的基本操作码),它是8(对于8种不同的标准比较模式),或其他东西?

这令人凌乱。 & t语法对救援?嗯,它解决了我们的一些问题,但也引入了新的问题。例如,AT& T将后缀添加到助记符中以区分不同的操作宽度。仅添加到x86-64 AT&amp的addb(16位“单词”),addl(32位“长字”)和AddQ(64位“Quadwords”)和ADDL(32位“长字”)和ADDQ(64位“Quadwords”)和AddQ(64位“Quadwords”)和AddQ(34位“Quadwords”)中的内容。 ; t语法。我们是否将这些单独算作?根据英特尔语法,没有。根据XED指令类,也没有。但也许我们认为这些明显足以毕竟分别计算?或者我们决定如果我们的定义取决于组装语法的选择,那么有几个,那么也许它不是一个非常自然的。机器做了什么?

注意我还没有指定机器的哪个部分。这也是棘手的。我们会在那里有一点。

但是,首先,指令字节。让我们来看看现在真实的手动输入:“mov-move”。如果您在当前的英特尔架构软件开发人员手册中检查页面,您将发现它列出不少于三十四个编码(并非所有这些编码;我会到达那个)。其中一些是具有特殊编码的更特殊的,特权操作(即,往返段寄存器)。这一次,XED似乎没有考虑分部寄存器加载和商店要特别,并将它们陷入普通的旧MOV,但我认为它们是不同的,机器认为它们足以在编码中提供特殊的OPCODE字节不用于其他任何东西,所以让我们称之为那些不同的东西。

这让我们留下了30“常规”的动作。哪些不规则:其中10个是在进行自己的事情,涉及在rax(64位模式下)寄存器的内存和不同部分之间移动,所有这些都具有特殊的绝对寻址模式(“Moffs”),它显示在内这些指示和我的知识无处可去。这些指令存在,并且再次存在,几乎没有任何使用它们。它们在16位模式下有机有用,但不再有用。

累加器寄存器的这种特殊性是X86中的重复主题。 “OP(AL / AX / EAX / RAX),某些东西”有自己的编码(通常是较小的)和各种怪癖,可以回到8086天的许多说明。因此,即使Asssembly程序员可能会考虑在测试EBX,128和Test EAX,128同一指令(以及XED指令类列表上的同一指令!),这些具有不同的操作码和不同的尺寸。因此,在装配列表中看起来相同的很多东西实际上是为了这种相当随机的原因。记在脑子里。但回到我们的mov!

其余20个列出的MOV变体分为四个不同的类别,每个类别有5个条目。这四类是:

“Load-ISH” - 从内存或其他相同大小的寄存器移动到8/16 / 32/64位寄存器。

“Store-ISH” - 从8/16/32/64位寄存器移动到相同尺寸或​​内存的另一个寄存器。

“存储 - 立即-ISH” - 将整数常数存储为8/16 / 32/64位存储器位置或寄存器。

所有处理器都有一些相当于前三个(“存储立即”存在于某些CPU架构中,但也有许多没有它)。加载/存储体系结构通常具有显式加载和存储指令(因此名称),并且每个人都有某种方式加载立即加载(大型立即常量通常需要多个指令,但不在x86上)并将一个寄存器的内容移动到另一个寄存器。 (虽然后者并不总是一个专用的指示。)除了我们的“负载ISH”和“存储-ISH”指令也支持“存储到”和“从”寄存器(特别是有两个编码寄存器寄存器movs的鲜明方法),这并不是那么显着。它确实解释了为什么MOV在X86代码中如此常见:“加载”,“存储”和“加载立即”是所有非常常见的指令,而且MOV归载全部,所以您当然可以看到它们的所有信息。

无论如何,我们有四个操作数大小和四个类别。那么为什么每个类别有五个列出的编码?好的,所以这有点尴尬。 X86-64有16个通用寄存器。您可以访问它们为16个完整的64位寄存器。对于所有16个寄存器,您可以从(或写入)其低32位半部读取。写入低32位半零点(即它将高距离设置为零)。对于所有16个寄存器,您可以从(或写入)其低16位季度。写入寄存器的低16位四分之一的寄存器不会零延伸;寄存器的剩余位保存,因为这是用于执行的32位代码,并且AMD决定在某种原因被指定为64位X86时保留该行为。并且对于所有16个寄存器,您可以从(或写入)其低8位八(最低字节)读取。编写低字节再次保留所有更高的字节,因为这是32位模式所做的。到目前为止和我在一起?伟大的。因为现在是奇怪的时候。在16位和32位模式中,您还可以访问A,B,C和D寄存器的位8到15作为AH,BH,CH和DH。和x86-64模式仍然让你这样做!但由于编码的频闪,只有在指令上只有在没有rex前缀(它是用于将可寻址寄存器计数从8到16扩展到的前缀)上的rex前缀。

因此,X86-64实际上具有共有20个可寻址的8位寄存器,在3个不相交的集合中:AL通过DL,可用于任何编码。 AH到DH,只有在指令上没有REX前缀,只能访问它。剩余的12寄存器的低8位,只有在存在REX前缀时才能访问。

这个Quirk是为什么英特尔列出了两次的所有8位变体:一次没有rex和一个带有rex的rex,因为它们可以访问寄存器空间略微不同的部分!好吧,但肯定的是,除此之外,我们必须有4个不同的操作码,对吗?一个用于移动字节,单词,双字,quadword?

不。当然不是。实际上,在每个类别中,有两个不同的操作码字节:一个用于8位访问的,一个用于“大于8位”。这可以回到8086,这是一个16位机器:“8位”和“16位”都是所需的所有区别。然后,386伴随着,需要一种编码32位目的地的方法,我们已经提到了已经提到的操作数大小前缀字节。在32位模式(在此处手中,细节有点复杂),用于表示16位现在默认为32位的指令,并且获取实际16位笔记需要操作数大小前缀。我已经提到了64位模式添加了自己的一组前缀(rex),并且该rex前缀用于将现在的默认值-32位“字”指令升级到64位宽度。

因此,即使英特尔列出了每个组中的指令的5种不同的编码,所有这些都具有稍微不同的语义,只有2个操作码与它们相关联:“8位”或“不是8位”。其余部分通过前缀字节处理。正如我们(现在)所知,那么有很多不同类型的MOV,这些MOV都有非常不同的东西,所有这些都在XED“指令类”下落下。

也许指令类是使用错误的指标? Xed有另一个,叫做“iforms”的更精细的东西,它分别考虑了不同的指令子类型。例如,对于刚刚讨论的MOV,我们得到了这个列表:

XED_IFORM_MOV_AL_MEMb = 804,XED_IFORM_MOV_GPR8_GPR8_88 = 805,XED_IFORM_MOV_GPR8_GPR8_8A = 806,XED_IFORM_MOV_GPR8_IMMb_C6r0 = 807,XED_IFORM_MOV_GPR8_IMMb_D0 = 808,XED_IFORM_MOV_GPR8_MEMb = 809,XED_IFORM_MOV_GPRv_GPRv_89 = 810,XED_IFORM_MOV_GPRv_GPRv_8B = 811,XED_IFORM_MOV_GPRv_IMMv = 812,XED_IFORM_MOV_GPRv_IMMz = 813,XED_IFORM_MOV_GPRv_MEMv = 814,XED_IFORM_MOV_GPRv_SEG = 815,XED_IFORM_MOV_MEMb_AL = 816,XED_IFORM_MOV_MEMb_GPR8 = 817,XED_IFORM_MOV_MEMb_IMMb = 818,XED_IFORM_MOV_MEMv_GPRv = 819,XED_IFORM_MOV_MEMv_IMMz = 820,XED_IFORM_MOV_MEMv_OrAX = 821,XED_IFORM_MOV_MEMw_SEG = 822,XED_IFORM_MOV_OrAX_MEMv = 823,XED_IFORM_MOV_SEG_GPR16 = 824,XED_IFORM_MOV_SEG_MEMw = 825,

正如您所看到的,该列表基本上与指令编码的方式匹配,其中8位任何内容被视为单独的指令,但是通过前缀的大小覆盖。因此,基本上是XED IFORMS的规则:如果它是一个单独的指令(或单独的编码),它会得到一个新的iform。但只需修改现有指令的大小(例如,将MMX指令扩展到SSE,或者通过前缀字节更改MOV的大小)。

那么如果我们将不同的iforms视为截然不同?事实证明,偶数6000。是所有这些吗?不可以。XED不包括一些无证指示(除了刚刚决定制作官方的英特尔的几个未记录的指示)。如果您查看英特尔手册,您将找到好奇的“UD2”,所定义的“未定义的指令”,该“未定义的指令”在架构上保证生成“无效的操作码”异常。顾名思义,这不是第一个。它的老同事“UD1”的一半存在,但不是正式的。由于UD1的语义与从未被定义的开始完全相同。是否是非定义和非正式地保证的非指令,因为它从未处于从未处于指令集中以x86指令开始的情况下的那样依赖于指令?就此而言,UD2本身是否自身,定义的未定义指令,算作指令?

但回到那些IFORMS:6000指令,呵呵?这些都必须在解码器中处理?这一定是可怕的。

好吧,没有。并不真地。我的意思是,这并不愉快,但这不是世界的尽头。

首先,让我们谈谈X86首先被解码:所有x86 CPU,你可能会与每循环进行交互(和执行)多个指令进行交互。想想这意味着什么:我们有一个(积极的!)可变长度编码,我们连续获取指令。这些芯片可以解码(给定正确的代码)每个时钟周期的指令。这是如何运作的?它们是可变的长度!我们可能知道我们在此循环中查看的第一个指令的位置开始,但CPU如何知道开始解码第二个,第三和第四个指令的位置?当您的指示是固定的大小时,这很简单,但对于x86,它们肯定不是。我们确实需要快速决定(在一个周期内),因为如果我们需要更长时间,我们不知道我们当前的“捆绑”结束的最后一个指令,我们不知道在哪里恢复解码下一个周期!

您在4GHz时钟周期(其所有0.25ns)中没有足够的时间来完全解码4 x86指令。就此而言,您甚至没有接近足够的时间来“完全解码”(究竟是什么意思是模糊的,我不会试图在这里精确地)。进行两种基本方法:第一个是简单的,不要这样做!尽量避免所有费用。保留额外的预涂层信息(例如在指令缓存中标记指令的位置),或者完全保留单独的解码缓存,例如Intels UOP缓存。这有效,但在运行当前缓存的代码时,它并没有帮助您。

这将我们带来了两个:处理它。而这样做的方式是非常蛮力。保持即将到来的指令字节的队列(这与分支目标预测和其他事物有关)。只要那里有足够的空间,你就只需继续为另外的16(或任何其他)指令字节并将它们扔进队列中。

然后,对于该队列中的每个字节位置,您假装x86指令在该字节上开始,并确定它是多长时间的。只是长度。无需知道指令是什么。无需知道操作数是什么,或者存储所赋予这些操作数的字节,或者是否是无效的编码,或者如果它是我们不允许执行的特权指令。这个阶段没有这么重要。我们只是想知道“假设这是一个有效的指示,它的长度是多少?”。但是,如果我们向队列添加16个字节,我们需要16个并行需要16个,以确保我们跟上并获得每个可能的起始位置的指令长度。如有必要,我们可以在多个周期中送出这些预涂层;我们只是继续提前提出。

一旦我们的队列充分满足,我们知道它的大小估计了每个位置,那么我们决定了指令边界的位置。这是跟踪的舞台。它抓住了从当前指令的位置开始的16个队列条目(或其他),然后只需“通过”。 “第一条指令表示从有5个字节开始的大小,好的;这意味着第二条指令在字节5,队列条目表明一个人的3个字节;好的,第三条指令以字节8,6字节开始“。在该阶段没有计算,只需在小尺寸表中的“表查找”,我们只是花了几个循环计算。

这是这样做的方式。如上所述,非常严厉的力量,但它有效。但是,如果您需要16个Predecoders(当您确实维持16个字节/循环的获取率),那么您真的希望这些是愚蠢和简单,因为您可能会逃脱。这些东西肯定不关心6000个不同的imorms。他们只是眯着眼睛,足以弄清楚尺寸,然后留下剩下的时间。

幸运的是,如果你看看实际的Opcode地图,你会看到这并不是那么糟糕。有大量的操作码,所有这些都基本上是相同的大小和操作数,只有不同的操作,我们根本不关心这个阶段。

这种模式几乎存在。例如,查看Opcode映射顶部附近的常规整数的常规整数的常规块。这些都看起来(和工作)非常相似于CPU。其中大多数具有基本相同的编码(除了几个不同的Opcode位除外)和相同的操作数模式。事实上,解码器真的不关心它是否是一个或,添加,CMP或XOR。对于装配语言编程器,编译器或反汇编程序,这些是非常不同的指令。对于CPU指令解码器,这些都是相同的指示:“Alu的东西 - 或者 - 其他犬歌不在乎”。其中哪一个执行将仅在稍后决定(并且可能只在该操作使其到ALU本身之后)。解码器关注的是,它是否是一个具有直接操作数的ALU指令,或者如果它有内存操作数,以及内存操作数看起来像什么样的。这些说明在这些问题的答案总是相同的中,方便地组织。当然,由于这仍然是x86,但显然它可以工作。

说明实际上没有一次性地解码,在一个大的“切换语句”,之后,他们转到不相交的芯片从来没有再次见面。这不是这些东西的建造。在不同的指令之间存在大量相似性,并且“了解指令”的“理解”是分发的,而不是集中的。

例如,出于大多数指令解码器的目的,SSE2指令ADDPS,SUBP,MUSDD和DIVPD都与之相同。它们是FP ALU指令,它们接受相同类型的操作数,所有这些都在同一个地方。

其中一些说明是如此类似的是,它们几乎肯定永远不会完全得到“解码”。例如,对于IEEE浮点数,减法实际上只是添加第二操作数的符号位的添加。如果您查看操作码选项卡

......