Intel 最新发布的 AVX 指令集,在指令编码方案中最主要的也最重要改变就是推出了 VEX 编码规则。可以预见 VEX 编码规则将会统治着未来 x86/x64 体系编码方案相当长的一段时间。
这里主要围绕 VEX 编码规则进行较为深入的解析,为什么会包括探讨 XOP 编码呢?应该要清楚地认识到 XOP 编码是从 VEX 编码基础而来的,它们方案虽然有区别,大致上是一致的。实质上换个角度来看,XOP 指令可以说是 AVX 指令集的子集(当然是属于 AMD 的子集)或者是补充。
看看下面这个图,描述了 AVX 和 XOP 指令的整体格式:

实际上和通用指令(GPI)格式几乎一样,只是多了一个额外的扩展前缀(extended prefix)部分,然而就是这个 extended prefix 是整个 AVX 和 XOP 指令的基石,它的作用是非常重要的,不可缺少的。对于 AVX 指令来说这部分是 2 bytes 或者是 3 bytes 的 VEX prefix,而对于 XOP 指令来说,它是 3 bytes 的 XOP prefix,注意这里也不存在 REX prefix,它将被集成到 VEX prefix(XOP prefix)里。
针对在不同的指令环境:通用指令,SSE 系列指令以及 AVX 指令(及其子集,包括 XOP 指令)中 Legacy prefix 有不同的使用方式和意义。
通用指令中可以使用全部的 legacy prefix 集,包括下面:
在 SSE 系列指令中,部分原有的 prefix 将被重新定义,LOCK prefix 将不可用,部分 legacy prefix 依然得到保留并且意义不变,它们是:
很明显这部分 legacy prefix 只能是用于 memory 操作数的,它的意义和通用指令中的 legacy prefix 完全一致。
部分 legacy prefix 被重定义,演变为其它类型的 prefix,它们是:
这些 prefix 意义在于修饰 SSE 指令的 opcode 码,使用 opcode 码有多个操作。
在 AVX 指令集及其子集中,SSE 系列指令的 SIMD prefix(66H, F3H 和 F2H)不再使用,LOCK prefix 不可用,只剩下:
它们依然可以使用在 memory 操作数上。这里也不存在 REX prefix,它与 SIMD prefix 一样被集成在 VEX prefix 里
因此下面的 Legacy prefix 出现在 AVX 指令里,将会产生 #UD 异常:
在通用指令和 SSE 系列指令中,可由 escape opcode(或称 escape prefix)组成 2 或 3 个 opcode 码,这些 escape opcode 是:
在 AVX 指令集系统中同样也已经被集成在 VEX prefix 里。
这部分是 VEX prefix 或者 XOP prefix,VEX prefix 可以是 2 bytes 或 3 bytes,XOP prefix 只使用 3 bytes。
VEX prefix(包括 XOP prefix)的主要角色:
VEX prefix 另一大作用是也提供对操作数的寻址和控制操作数的大小:
这部分是指令真正的 opcode 码,可是这个 1 byte 的 opcode 码并不能唯一决定指令的真正操作属性,在原有的 SSE 指令上 opcode 必须结合 escape opcode 或者 SIMD prefix 来确定指令具体的操作,而在 AVX 系列和 XOP 系列指令上,opcode 必须结合 VEX prefix 或 XOP prefix 来确定指令的具体操作。
大部分原有的 SSE 指令都实现了 VEX 编码版本(AVX 版本),它们被重新用 VEX 编码规则进行编码,这表示,大部分原指令即有 SSE 版本也有相应的 VEX 版本,下面这个表格是一个例子:
SSE 版本 |
opcode |
VEX 版本 |
opcode |
addps |
0F + 58 |
vaddps |
VEX.mmmmm = 1,VEX.pp = 0 |
58 |
|||
addpd |
66 + 0F + 58 |
vaddpd |
VEX.mmmmm = 1,VEX.pp = 1 |
58 |
|||
addss |
F3 + 0F + 58 |
vaddss |
VEX.mmmmm = 1,VEX.pp = 2 |
58 |
|||
addsd |
F2 + 0F + 58 |
vaddsd |
VEX.mmmmm = 1,VEX.pp = 3 |
58 |
可以看到,这一系列指令的 opcode 码都是 58H,加上 escape opcode 和 SIMD prefix 修饰后,衍生出一系列的操作,它们的 escape opcode 都是 0FH,不同的只是 SIMD prefix
在原有的 SSE 指令中,escape opcode 和 SIMD prefix 都是明码提供的,AVX 指令采用了 VEX 编码方案后,escape opcode 和 SIMD prefix 已经集成在 VEX prefix 中,我们在这里初步看到了 VEX 编码规则的威力,其最大的威力是:通过这种编码方案很容易扩展出新的指令集。
在 AVX 指令中,这些组部分与通用指令中的 ModRM,SIB 与 Displacement 意义是完全一致的,详情请参阅 http://www.mouseos.com/x64/index.html 的相关内容。
在 AVX 和 XOP 指令中,这个 immediate 值只有 1 byte,因此记作 imm8,它提供第 4 个操作数的寻址:
VEX prefix 与 XOP prefix 结构是相同的,可是某些地方的意义不一致,下面是 VEX prefix 与 XOP prefix 的结构:

VEX prefix 分为 3-bytes 和 2-bytes 两个版本:
XOP prefix 只有 3-bytes 的,以 8F 字节开头。
3-bytes VEX prefix 组成部分如下:
|
C4 + [RXBmmmmm] + [WvvvvLpp]
|
2-bytes VEX prefix 组成部分如下:
|
C5 + [RvvvvLpp]
|
在上面的 byte 1 和 byte 2 中每个字母占 1 bit,因此:VEX.mmmmm 域 5 bits 宽,VEX.vvvv 域 4 bits 宽,VEX.pp 域 2 bits 宽,其余的域 1 bit 宽。
事实上这就是 VEX prefix 集成 x64 编码中的 REX prefix 的体现:
它们的作用是和 REX.WRXB 大致相同的,如下示意:
|
byte 1 byte 2
|
关于 REX.WRXB 的作用,详情请参阅:http://www.mouseos.com/x64/doc4.html
在 AVX 指令中 VEX.W 遭遇了 3 个情境:
现阶段 VEX.W 的作用是保留为兼容原有 SSE 指令,绝大多情况下 VEX.W 应该置为 0
然而对于 XOP.W 来说,它的意义与 VEX.W 是不同的:XOP.W 控制指令操作数的寻址
表1:XOP 中 4 个操作数

在 4 个操作数的 XOP 指令中,XOP.W 主要控制 src2 和 src3 操作数,在由 imm8[7:4] 和 ModRM.r/m 寻址之间作选择。
表2:XOP 中的 3 操作数

由于只有 3 个操作数,XOP.W 将控制 src(src1 操作数)和 count(src2 操作数),由 ModRM.r/m 与 XOP.vvvv 之间作选择。当 XOP 指令只有 2 个操作数时,XOP.W 需置为 0,不需要 XOP.vvvv 进行寻址,XOP.vvvv 必须置为 1111b
在 VEX prefix 中的 VEX.RXB 同样是起了扩展作用,可是 VEX.RXB 的扩展与 REX.RXB 的扩展是存在较在差别:
以 REX.R 的原码扩展为例:
以 VEX.R 的反相扩展为例:
反相扩展的结果是:VEX.RXB 取反,然后扩展相应的域,下面的两个表格列出了所有反相扩展的值:
表1:VEX.RB 的反相扩展

表2:VEX.XB 的反相扩展

表2 中的 VEX.XB 扩展是用来内存操作数的寻址上,从这两个表我们可以知道,在 32 位模式下,仅能使用其中的一半寄存器。
VEX.RXB 执行的是反相扩展,然而 VEX.vvvv 执行的是反相编码,我们将会在后面看到 VEX.vvvv 的更彻底的反相编码!
在 AVX 指令中大部分都是 3 个 operands 的指令,VEX.vvvv 提供对寄存器操作数的寻址:
必须注意的是:当不需要使用 VEX.vvvv 进行寻址时,VEX.vvvv 必须置为 1111b 值。这种情况下,只需要 ModRM.reg 和 ModRM.r/m 进行寻址就足够了。
4 bits 宽的 VEX.vvvv 提供的是反相编码。下面看看 VEX.vvvv 的反相编码表:
可以看到,在 VEX.vvvv = 1111b 实际等于原码的 0000b,这时对应的寄存器是 xmm0/ymm0 寄存器。
上面所看到的反相扩展以及反相编码,是经过研究设计后的必然结果,我们看看为什么会这样设计。
首先我们来看看 VEX prefix 中的 byte 0,它们是:
这两个字节被用来作为 VEX prefix 的首字节,需要解决的首要问题是如何避免冲突!在原来的指令里 C4 是 LES 指令的 opcode 码。
下面这条指令:
|
les edi, [eax]
|
它的指令指令编码是:
|
les edi, [eax] |
les 指令所需要的操作数必须是 memory 操作数,它存放着 far pointer(mem16:16 或 mem16:32),所以,我们不能这样:
因此,VEX 编码正是利用了这个无效操作数的错误来设计的:
现在,我们就找到了设计的关键:在 32 位模式下,必须让 C4 后面的字节 ModRM.mod = 11b,从而产生了 VEX.RXB 的反相扩展设计方案,以下面这条指令为例:
|
vaddps ymm1, ymm2, [r10]
|
它的指令编码是:C4 C1 6C 58 0A
|
vaddps ymm1, ymm2, [r10] 解码2: 作为 VEX prefix 时:VEX.R = 1 |
从上面可以看到,在 32 位模式下 AVX 指令的 VEX.RX 必须为 11b 才能保证正确是解码为 AVX 指令,所以:
这就是 VEX.RXB 的反相扩展的设计方案。
同样的设计原理,VEX.vvvv 在 32 位模式下必须以 1b 开头,以 vaddps 指令为例:
|
vaddps ymm1, ymm2, ymm3 |
上面使用的是 2-bytes 版本的 VEX prefix 编码规则,在 32 位模式下 C5 是 LDS 指令的 opcode 码,这里同样要求 VEX.vvvv 必须以 VEX.R 为 1b,以及 VEX.vvvv 以 1b 开头,VEX.vvvv 被设计为更彻底的反相编码,与 VEX.RXB 的仅仅反相扩展有些区别。
事实上 XOP prefix 为了兼容 VEX prefix 而选择使用反相设计,从原理上来看 XOP prefix 可以做到不需要反相设计。XOP prefix 使用 8F 作为 byte 0

上面这个表格可以看出,只有当 ModRM.reg = 000 时,这个 opcode 才是有效的,因此,当 ModRM.reg 不为 000b 时(也就是大于 08H),就可以识别出 XOP prefix 了,pop 指令的操作数既可以是寄存器,也可以是内存操作数,从设计原理上来说使用 8F 作为 XOP prefix 并不一定需要对 ModRM.mod 进行限制,但是基于兼容 VEX prefix 的目的,XOP prefix 同样使用了反相编码设计。
VEX.mmmmm 里集成了 escape opcode:
而 VEX.pp 里集成了 SIMD prefix:
因此,原有的 SSE 系列指令很容易移植成 AVX 指令,通过使用 VEX 编码规则进行重新编码就形成了 AVX 指令版本,大多数 SSE 指令都有相对应的 AVX 指令形式。
|
addpd xmm1, xmm2 ; SSE2, encode: 66 0F 58 CA |
上面就是同一条指令的 SSE 版本和 AVX 版本,它们所执行的操作是完全一样的,AVX 版本使用 VEX 编码规则。
表1 VEX.mmmmm 对应表:VEX.mmmmm |
escape opcode |
00000 |
保留 |
00001 |
0F |
00010 |
0F 38 |
00011 |
0F 3A |
表2 VEX.pp 对应表:
VEX.pp |
SIMD prefix |
00 |
无 |
01 |
66 |
10 |
F3 |
11 |
F2 |
VEX.mmmmm 域仅存在于 3-bytes 的 VEX prefix 中,对于 2-bytes 的 VEX prefix 缺省是集成 0F 的,基于这个原因,部分 3-bytes VEX 的 AVX 指令可以使用 2-bytes VEX 进行编码。
我们看看上面的指令:
|
vaddpd xmm1, xmm2, xmm3
|
这条指令典型地可以译为 2-bytes VEX 版本和 3-bytes VEX 版本。
先来看看 3-bytes VEX 的版本:
|
C4 E1 69 58 CB |
现在来看看 2-bytes VEX 版本:
|
C5 E9 58 CB |
上面两种译法都是正确的,因此绝大多数编译器在可能的前提下会选择译为 2-bytes VEX 的编码,这样生成的指令编码更短。当然,很显然并不是所有的 3-bytes VEX 都可以译为 2-bytes VEX 版本,下面三种情况是不能的:
XOP 指令中的 XOP.pp 意义与 VEX.pp 是一样的,可以 XOP.mmmmm 却与 VEX.mmmmm 完全不同,从上面的节 《7.2.4.2 XOP prefix 的反相设计》中我们可以看到,在 XOP.mmmmm 要求是大于或等于 08H 的
XOP.mmmmm |
意义 |
01000 |
指令中需要提供 imm8 操作数 |
01001 |
指令中无需 imm8 操作数 |
可见,XOP.mmmmm 无不是提供 escape opcode,而是用来控制操作数,前面已经提过必须大于或等于 08H 是因为必须保证 ModRM.reg 不为 0,这是正确解码出 XOP 指令方法。
这是一条 XOP 指令,它的机器编码是:8F E8 68 EC CB 00 第 4 个操作数是 imm8,看看下面的示意:
|
8F E8 68 EC CB 00 |
现在,我们知道 XOP 是全新的指令集,它不使用 escape opcode 也不使用 SIMD prefix。
最后来看看 VEX.L 域,VEX.L 域有三种意义:
这是比较常见的指令,它是用来控制指令的操作数大小:
大部份 AVX 或 XOP 指令可以操作 128 和 256 位数据。
|
vaddpd xmm1, xmm2, xmm3 ; 128-bit |
这两条指令的差别在于使用的寄存器,看看下面的示意:
|
C5 E9 58 CB |
除了 VEX.L 不同外,其它都是完全一样的。
这类指令同样比较常见,这类指令的 mnemonic 中包括:
它们的 VEX.L 将被忽略不起作用,下面是典型的指令例子:
|
vaddsd xmm1, xmm2, xmm3
|
它的机器编码是:C5 EB 58 CB
|
C5 EB 58 CB |
这个编码中的 VEX.L 将被忽略,因此 VEX.L = 0 或 VEX.L = 1 都是正确的。
AVX2 是 AVX 指令的扩展,新增了使用 VEX-prefix 的通用指令,它们的 VEX.L 必须为 0
下面这条指令是例子:
andn rax, rbx, rax
|
它的编码是:C4 E2 E0 F2 C0
|
C4 E2 E0 F2 C0 operand1: VEX.R + ModRM.reg = 1000(rax 寄存器) |
这里的 VEX.L 必须为 0,当置 VEX.L = 1 时这个 opcode 是无效的,执行它会产生 #UD 异常。
在指令编码中,能提供操作数寻址的 4 个域是:
因此,VEX 指令能提供最多 4 个操作数。在这里对于 4 个操作数统一使用下面的术语:
目标操作数只有一个,我们可以肯定的:operand1 就是目标操作数。可是在 Intel 或 AMD 官方的手册里,关于源操作数确实有时会让人产生迷惑,这个迷惑首先来自 first source operand(第 1 个源操作数),那么哪个是 first source operand 呢?答案是:operand1 和 operand2 都有可能。
那是因为某些指令:它的 operand1 既是目标操作数,也是源操作数,也就有可能 operand1 是 frist source operand。因此这里统一使用 operand1 到 operand4 术语。
这里先来看看 VEX.vvvv 提供的操作数是哪些:
也就是:VEX.vvvv 可以提供目标操作数和源操作数进行寻址。大多数情况下,VEX.vvvv 提供 operand2 寻址,只有少量的指令需要 VEX.vvvv 提供 operand1(即:目标操作数)寻址,当 VEX.vvvv 不被使用时,必须置为 1111b 值。
下面这个表格列出使用 VEX.vvvv 来寻址的指令形式:
vpslldq xmm1, xmm2, imm8 |
vpsrldq xmm1, xmm2, imm8 |
vpsrlw xmm1, xmm2, imm8 |
vpsrld xmm1, xmm2, imm8 |
vpsrlq xmm1, xmm2, imm8 |
vpsraw xmm1, xmm2, imm8 |
vpsrad xmm1, xmm2, imm8 |
vpsllw xmm1, xmm2, imm8 |
vpslld xmm1, xmm2, imm8 |
vpsllq xmm1, xmm2, imm8 |
在整个 AVX 指令中,仅仅只有上面的 10 条指令是使用 VEX.vvvv 对 operand1(也就是:目标操作数)进行寻址的。
operand1(目标操作数) |
operand2 |
operand3 |
VEX.vvvv |
ModRM.r/m |
imm8 |
这些指令的 VEX.vvvv 作为 operand1,ModRM.r/m 作为 operand2。为什么不使用 ModRM.reg 进行寻址呢,实际上这 10 条指令是 Group 属性的 opcode 码,ModRM.reg 需要为 opcode 提供辅助识别。基于这个原因,不能使用 ModRM.reg 寻址操作数。
在 3 个操作数以及 4 个操作数的指令中,除了上述的 10 条指令外,VEX.vvvv 都作为 operand2 寻址(即:源操作数),VEX.vvvv 不会被作为 operand3,在 AVX 系列指令集中 operand3 可以是 register 或 memory 操作数,因此:operand3 必需由 ModRM.r/m 提供寻址。
ModRM.reg 提供寄存器操作数,而 ModRM.r/m 提供寄存器和内存操作数。当指令形式中存在 memory 操作数,那么必须由 ModRM.r/m 提供寻址。
operand1 |
operand2 |
operand3 |
operand4 |
ModRM.reg |
ModRM.r/m |
无 |
无 |
ModRM.r/m |
ModRM.reg |
无 |
无 |
在两个操作数的情况下并不使用 VEX.vvvv 寻址,因此 VEX.vvvv 的值必需设为 1111b,当 ModRM.r/m 出现在 operand1(目标操作数)的位置上,那么这类指令是典型的 store 类指令。
operand1 |
operand2 |
operand3 |
operand4 |
ModRM.reg |
VEX.vvvv |
ModRM.r/m |
无 |
ModRM.reg |
ModRM.r/m |
imm8 |
无 |
VEX.vvvv |
ModRM.r/m |
imm8 |
无 |
上面列出了几种情况,第 1 类指令是占了多数。没使用 VEX.vvvv 时,VEX.vvvv 必须为 1111b 值,这里的 imm8 是立即数,表示一个控制位或 mask 值。
operand1 |
operand2 |
operand3 |
operand4 |
ModRM.reg |
VEX.vvvv |
ModRM.r/m |
imm8(立即数) |
ModRM.reg |
VEX.vvvv |
ModRM.r/m |
imm8[7:4](寄存器) |
上面是 AVX 的四个操作数指令,operand4 是 imm8,有两种情况:
AVX 指令实现真正的 4 个寄存器操作数,那么在原有 x86 指令编码结构中必须要有表达第 4 个寄存器操作数的部件,既不是 ModRM 也不是 SIB 更不是 displacement,唯有 immediate 是可以利用的地方。
在 VEX 编码中的 imm8 有两种意义:
值得注意的是:imm8[7:4] 提供的是原码编码,并不像 VEX.vvvv 那样提供反相编码。
|
vblendvpd ymm1, ymm2, ymm3, ymm4
|
这条是典型的 4 操作数指令,它的编码是 C4 E3 6D 4B CB 40,如下所示:
|
C4 E3 6D 4B CB 40 |