REX prefix 是实现 x64 平台的 64 位计算的手段,通过 REX prefix 来扩展访问 64 位资源。这里就 REX prefix 进行探讨。
关于 64 位扩展技术和 REX prefix 的讨论,可参见另一篇文档:《x64 体系 64 位扩展技术的实现》 http://www.mouseos.com/x64/extend64.html
x64 体系扩展和新增了编程资源:
x64 体系在 64 位寻址空间实际上只实现了 48 位 virtual address 寻址空间,高 16 位被保留起来,用作符号扩展。
扩展位 |
寄存器 |
000 |
001 |
010 |
011 |
100 |
101 |
110 |
111 |
0 |
reg8 |
al |
cl |
dl |
bl |
ah/spl * |
ch/bpl * |
dh/sil * |
bh/dil * |
reg16 |
ax |
cx |
dx |
bx |
sp |
bp |
si |
di |
|
reg32 |
eax |
ecx |
edx |
ebx |
esp |
ebp |
esi |
edi |
|
reg64 |
rax |
rcx |
rdx |
rbx |
rsp |
rbp |
rsi |
rdi |
|
mmx * |
mmx0 |
mmx1 |
mmx2 |
mmx3 |
mmx4 |
mmx5 |
mmx6 |
mmx7 |
|
xmm |
xmm0 |
xmm1 |
xmm2 |
xmm3 |
xmm4 |
xmm5 |
xmm6 |
xmm7 |
|
sreg * |
es |
cs |
ss |
ds |
fs |
gs |
|||
creg |
cr0 |
cr1 |
cr2 |
cr3 |
cr4 |
cr5 |
cr6 |
cr7 |
|
dreg |
dr0 |
dr1 |
dr2 |
dr3 |
dr4 |
dr5 |
dr6 |
dr7 |
|
1 |
reg8 |
r8b |
r9b |
r10b |
r11b |
r12b |
r13b |
r14b |
r15b |
reg16 |
r8w |
r9w |
r10w |
r11w |
r12w |
r13w |
r14w |
r15w |
|
reg32 |
r8d |
r9d |
r10d |
r11d |
r12d |
r13d |
r14d |
r15d |
|
reg64 |
r8 |
r9 |
r10 |
r11 |
r12 |
r13 |
r14 |
r15 |
|
mmx * |
mmx |
mmx1 |
mmx2 |
mmx3 |
mmx4 |
mmx5 |
mmx6 |
mmx7 |
|
xmm |
xmm8 |
xmm9 |
xmm10 |
xmm11 |
xmm12 |
xmm13 |
xmm14 |
xmm15 |
|
sreg * |
es |
cs |
ss |
ds |
fs |
gs |
|||
creg |
cr8 |
cr9 |
cr10 |
cr11 |
cr12 |
cr13 |
cr14 |
cr15 |
|
dreg |
dr8 |
dr9 |
dr10 |
dr11 |
dr12 |
dr13 |
dr14 |
dr15 |
上面这个表很详细在列出来 x86/x64 平台下所有 registers 的编码,分别列出了 GPRs(General Purpose Registers)在 1 byte、2 bytes、4 bytes 和 8 bytes 宽度下的名称和编码:
上图所示:ah - bh 与 spl - dil 编码一样,那么 processor 是如何识别它们呢?
扩展位 |
100 |
101 |
110 |
111 |
--- |
ah |
ch |
dh |
bh |
0 |
spl |
bpl |
sil |
dil |
实际上 spl, bpl, sil 以及 dil 寄存器仅在 64 位模式下有效的,它们需要扩展位进行扩展,这个扩展位为 0
看下面的几个例子:
这个很容易理解,这条指令的机器码是:8a c4
那么,再来看一看这条指令,是如何译为机器码的?指令中使用了 spl 寄存器,这个寄存器仅在 64 位模式是有效的。这条指令的机器码中的 Opcode 和 ModRM 与上一条是完全一样的,但是需要增加了 REX prefix 来确定 spl 寄存器,最终结果为:40 8a c4
这是基于:ah = 000,而 spl = 0000(需要 REX prefix 进行扩展)
最后,看一看这条指令的机器码是多少?
事实上,在 Opcode 相同的提前下,这条指令在 x64 下可以有两种译法:
扩展位 |
000 |
001 |
010 |
011 |
--- |
al |
cl |
dl |
bl |
0 |
al |
cl |
dl |
bl |
可以看出,在 000 - 011 编码中,即使增加了扩展位,它们还是一样的。
它们的寄存器编码都是一样的,显然有一条必定是错误的。
第一条指令中,spl 寄存器与 ch 寄存器产生了冲突,冲突自来于:spl 需要 REX 进行扩展,而 ch 寄存器在 REX 的扩展下将会变成 bpl 寄存器,因此,在 REX prefix 的前提下是不存在 ch 寄存器的,这正是第二条指令的结果,所以第二条指令是正确的。
上面几个例子中提到的 40 这个字节就是这里要讲解的 REX prefix
REX prefix 顾名思义它是一个指令 prefix,与 legacy preifx(如: 66H prefix、67H prefix)起类似作用,可以说它是一个 operand size override,将缺省 32 位 operand size 改写为 64 位的 operand size
REX prefix 提供了对 64 位寄存器和 64 位地址的访问的手段(包括新增的寄存器), REX prefix 可以与 legacy prefix 共处,然而存在冲突的情况下,以 REX prefix 为准,而忽略 legacy prefix,显然这个冲突会来自于 66H prefix 与 REX prefix 之间。
前面提过,REX prefix 主要作用是:
REX prefix 是不定值,它的取值范围是:40 - 4F (共 16 个)
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
0 |
1 |
0 |
0 |
W |
R |
X |
B |
REX 的结构如上:REX = [0100WRXB],高 4 位固定为 4,低 4 位表述为:
在 x86 平台上,或者 x64 平台下的 compatibility (兼容)模式下 40 - 4F 这些 opcodes 代表 inc 以及 dec 指令,在 64 位模式下,40 - 4F 被重定义为 REX prefix
它的直观结构图如下:
REX prefix 结构: 0100 X X X X |
REX.W 代表 operand width(即:操作数的宽度),也就是指明 operands size(操作数大小)
--
|
REX.W = 0 时 |
REX.W = 1 时 |
operands size = |
使用 default operands size (缺省操作数大小) * 注1 |
使用 64 位操作数 |
注1:
下面这条指令:
mov eax, ebx |
它的正常编码是:89 d8 下面看看它在不同的 REX.W 和 66H prefix 下的不同表现:
第 1 条指令编码使用 REX prefix 扩展访问 64 位寄存器,REX.W = 1
第 2 条指令编码加上了 66H prefix 同时还有 REX prefix(REX.W = 1),此时一般会认为产生了冲突:是使用 64 位还是 16 位 operand size 呢?
实际上,很简单!48H 位于 66H 后面,66H 将被覆盖!也就是说:66H prefix 将会被忽略,REX prefix 产生了作用!因此:指令的 operand size 是 64 位的。
第 3 条指令编码也同样使用了 66H prefix 和 REX prefix,但是 REX.W = 0 意味着不改变原来的 operand size!
在这种情况下,REX prefix 不会与 66H prefix 产生冲突,最终的作用于 66H prefix,因此 operand size 是 16 位的。
REX.R 代表 registers 用来扩展 ModRM.reg 域,使原本 3 位的寄存器编码变成 4 位,用以访问新增的寄存器。
-- |
REX.R = 0 时 |
REX.R = 1 时 |
ModRM.reg = |
0000 - 0111 |
1000 - 1111 |
REX.R 对 ModRM.reg 域进行扩展,也就是说它提供 ModRM.reg 的扩展位。ModRM.reg 提供 register 的编码,它既可以是目标寄存器,也可以是源寄存器,这取决于指令的 opcode 的属性。因此,REX.R 用来扩展以 ModRM.reg 作为寻址的寄存器
看看下面这条指令:
mov rax, r10 |
上面这条指令的 second operand(源操作数)寄存器由 ModRM.reg 来提供,它的指令编码是:
它使用了 REX prefix(REX.R = 1),它的 REX prefix 与 ModRM 关系图如下:
REX = 4c ModRM = d0 0100 1 1 0 0 11 010 000 | | |
源操作数 = REX.R + ModRM.reg = 1 + 010 = 1010
指令中的 REX.R = 1 它扩展了 ModRM.reg 域,使寄存器的 ID = 1010(r10寄存器编码),使得能够访问源操作数 r10 寄存器。
REX.X 代表 index 用来扩展 SIB.index 域,使原本 3 位的寄存器编码,变成 4 位,用以访问新增的寄存器。
-- |
REX.X = 0 时 |
REX.X = 1 时 |
SIB.index = |
0000 - 0111 |
1000 - 1111 |
注意:
REX.X 仅扩展 SIB.index 域,也就是说它提供 index 寄存器的扩展位。
看看下面这条指令:
mov rax, [rbx + r10 * 8] |
这条指令的 index 寄存器是 r10 寄存器,它的指令编码是:
它的 REX prefix 与 SIB 结构图如下:
REX = 4a SIB = d3 0100 1 0 1 0 11 010 011 | |
index 寄存器:REX.X + SIB.index = 1 + 010 = 1010 |
地址寻址中的 index 寄存器是新增的 r10 寄存器,通过 REX.X 进行扩展从而访问新增的 index 寄存器。
REX.B 意指 base 寄存器,除用来扩展 SIB.base 域,还扩展 ModRM.r/m 域以及 opcode 码里的 reg 域。
-- |
REX.B = 0 时 |
REX.B = 1 时 |
SIB.base = |
0000 - 0111 |
1000 - 1111 |
ModRM.r/m = |
0000 - 0111 |
1000 - 1111 |
Opcode.reg = |
0000 - 0111 |
1000 - 1111 |
Opcode.reg 表示 register 编码被嵌在指令的 opcode 中,典型的指令如:mov r10, 0xffff800008001000,这条指令的寄存器编码在 Opcode 码提供。
如前面所述 REX.B 可以扩展 3 部分:
下面这条指令:
mov rax, [r8 + rcx * 8] |
这条指令的 memory 操作数的 base 寄存器是 r8,它需要 REX.B 进行扩展,它的指令编码是:
它的 REX prefix 与 SIB 字节的结构图如下:
REX = 49 SIB = c8
| |
base 寄存器:REX.B + SIB.base = 1 + 000 = 1000 |
base 寄存器的编码经过扩展后是 1000 它是 r8 寄存器。
下面这条指令:
mov r10, rax |
这条指令的 ModRM.reg 提供源操作数寻址,而 ModRM.r/m 提供目标操作数寻址,目标寄存器 r10 需要 REX.B 进行扩展,它的指令编码是:
它的 REX prefix 与 ModRM 结构图如下:
REX = 49 ModRM = c2
| |
目标操作数:REX.B + ModRM.r/m = 1 + 010 = 1010 |
目标操作数 r10 寄存器的编码经过 REX.B 扩展为 1010
在一部分指令的 Opcode 码里包含了 reg 域,这些 register 是不需要 ModRM 进行寻址的,下面这条指令:
mov r10, 0x1122334455667788 |
它的指令编码是:
目标操作数 r10 的编码由 Opcode 提供,下面看一看 REX prefix 与 Opcode.reg 结构图:
REX = 49 Opcode = BA 0100 1 0 0 1 1011 1 010 | | REX.B + Opcode.reg = 1010 |
名称 |
位 |
描述 |
-- |
[7:4] |
0100 |
REX.W |
[3] |
0 = default operand size; 1 = 64-bit operand size |
REX.R |
[2] |
modrm.reg = [REX.R + modrm.reg] |
REX.X |
[1] |
SIB.index = [REX.X + SIB.index] |
REX.B |
[0] |
SIB.base = [REX.B + SIB.base] modrm.r/m = [REX.B + modrm.r/m] Opcode.reg = [REX.B + Opcode.reg] |
指令中,操作数使用到 ModRM,SIB 以及 Opcode 进行寻址时,REX 可以对它们进行扩展,典型地 REX.B 对 ModRM.r/m 进行扩展,然而如果指令中并不需要 ModRM.r/m 提供寻址时,REX.B 的作用将会被忽略,0 或 1 值并不影响 processor 对指令的解码。
x64 体系的 64 bit 环境中:操作数的 Default Operand-Size 是 32 位,而 Default Address-Size 是 64 位的。
因此,在怎么设计 64 位的计算方案时,有 2 个问题要解决的:
主要是基于指令编码的原因:
这种情况下,REX prefix 就应运而生,它解决了:
所以,经过考虑下,新增 REX prefix 来使用 64 位 operand,而使 default operand size 为 32 位。这样设计比将 default operand size 定为 64 位,然后还是要新增另一个 prefix (多此一举之嫌)要好多了。
这样一来,也解决了前面提到的 64 位计算的 2 个问题。
64 位模式下 default address size 是 64 位,可以使用 67H prefix(address size override)进行调整为 32 位 address,基于这个设计,当指令的地址是 64 位,不需要为地址操作数提供 REX prefix。
看看下面几个例子:
这条指令的 operand size 是 32 位,address size 是 64 位,它是无需提供 REX prefix,最终编码为:c7 00 01 00 00 00 (无需提供 REX prefix)
在某种情况下使用 REX prefix 也是正确的,我们来看看:
然而,如果使用了 REX.B = 1 以及 REX.W = 1 对于这条指令来说,则是错误的:
--------------------------------
值得注意的是:当写成 mov qword ptr [rax], 1
从指令的使用角度来看,这条指令是没错的!但是,这条指令不会存在 64 位的立即数,编译器不可能产生 64 位的立即数值!
编译器插入 REX.W = 1 使得 32 位的立即数最终会符号扩展到 64 位,然后赋给 [rax], 目标操作数存放的结果是一个 64 位值!
然而,immediate 编码还是 32 位,最终编码为: 48 c7 00 01 00 00 00 (REX.W = 1)
c7 这个 opcode 指令描述为:MOV Ev, Iz,这意味着 immediate 最大的宽度是 32 位。因此,即使是 64 位的操作数,immediate 依然是 32 位宽
同前面所述,不同的是:地址中使用了 r8 作为 base 寄存器,它的 encode 是 1000 它需要提供 REX prefix(REX.W = 1 & REX.B = 1)
最终编码为:49 c7 00 01 00 00 00
这条指令的 memory 操作数中使用了 [base + index * scale + disp8] 的寻址模式,地址中 base 寄存器和 index 寄存器都使用了新增的寄存器,那么它需要:
REX prefix 的值为 4BH,指令的最终编码为:4b 89 44 d0 0c
这条指令地址中使用了 32 位地址模式,由于 default address size 是 64 位,因此需要使用 67H prefix(address size override)进行 override 为 32 位地址
最终编码为:67 c7 00 01 00 00 00
与上一条指令所不同的是:它使用了 64 位操作数,address size 同样还是 32 位,因此需要同时提供 67H prefix 以及 REX prefix
最终编码为:67 48 c7 00 01 00 00 00
下面使用几个例子来说明解决之道
这条指令的 Default Operand-Size 是 32 位,在 32 位下它的机器编码是:b8 01 00 00 00
64 位下使用 64 位寄存器,它的语法元素变成: mov rax, 1
此时,它的机器编码是 48 b8 01 00 00 00 00 00 00 00
而这里的 48 就是 REX prefix 字节,它的各个域值是:REX.W = 1,使用的操作数是 64 位的,REX.RXB 都为 0
这是一条平常 64 位指令,它是需要 ModRM 来进行寻址的。源寄存器是 r14,目标寄存器是 rax
REX.W = 1, REX.X 将会被忽略,最终机器码是: 49 8b c6(共3个字节)
这条指令的 operand size 是 16,address size 是 32,那么:
作为例子,我将它改为 64 位指令,如下:
mov qword ptr [rax + rcx * 8 + 0x11223344], 0x12345678 |
这条指令 operands size 变为 64,address size 变为 64。 它的 base 寄存器和 index 寄存器都改为 64 位,其它不变。
由于使用了 64 位 operand size,需要 REX prefix:
这里既不需要 66H prefix 也不需要 67H prefix,它的 encode 是:48 c7 84 c8 44 33 22 11 78 56 34 12
下面这条指令变为:
mov dword ptr [rax + rcx * 8 + 0x11223344], 0x12345678 |
现在它的 operand size 变为了 32 位,而 address size 是 64 位,现在我们得知它使用的是 default operand size(缺省的操作数大小),我们可以有如下方法表达:
所以,通常的译法是:c7 84 c8 44 33 22 11 78 56 34 12,或者不寻常的做法:40 c7 84 c8 44 33 22 11 78 56 34 12,当然你还可以使 REX.R = 1,这个 REX.R 会被必略,REX prefix 变成了 44H
最后,这条指令再变为:
mov qword ptr [r8 + r9 * 8 + 0x11223344], 0x12345678 |
情况变得更加有趣了,64 位的 operand size,并且使用了 r8 base 寄存器,r9 index 寄存器,我们必须:
base 和 index 寄存器都要被扩展,至于 REX.R 无所谓,会被忽略,REX prefix 是:
这个指令编码结果是:4b c7 84 c8 44 33 22 11 78 56 34 12
这条指令的寄存器寻址嵌在 Opcode 中,Opcode.reg = 1000,REX prefix 应该:
Opcode.reg 将需要扩展,REX.R 以及 REX.X 会被忽略,你可以置它们为 1 或者为 0,并不影响指令的解析,指令编码结果是:49 b8 01 00 00 00 00 00 00 00
当 66H prefix 与 REX prefix 同时出现的情况下:66H prefix 用于调整为 16 位 operand,而 REX.W = 1用于扩展为 64 位 operand,那么,66H prefix 的作用将被忽略
mov r8, 1 |
若 processor 解码器在读指令时,遇到以下编码怎么办?
66 49 b8 01 00 00 00 00 00 00 00 |
66H prefix 和 REX prefix 同时出现了,实际上它们作用起了冲突
66H prefix 将 operand size 改写为 16 位
冲突出现时,最终会作用于 REX prefix,66H prefix 的作用被忽略。
在 64 位模式下,由于 40 - 4F 被作为 REX prefix,那么原 inc/dec 指令,只能使用 FF /0 和 FF /1 这两个 Opcode 了
inc rax |
这条指令的编码为: 48 ff c0
64 位模式下,大部分指令缺省操作数是 32 位的,部分指令是 64 位的,它们是:
push r8 |
上面这条指令它的 default operand size 是 64 位,那么它的 REX prefix 构造将会是:
REX.W = 0 表示使用 default operand size,REX.B = 1 用来扩展 ModRM.r/m ,它的编码是 41 ff f0
call rax |
上面这条指令它的 default operand size 也是 64 位,可是它的寄存器操作数并不需要 REX prefix 进行扩展,因此它并不需要 REX prefix,它的编码是:ff d0
--------------------------------------------------------------------------------------------------------------------------------
★★★ 注:有两类指令的 default operand size 是 64 位的。
关于 64 位下哪些指令是 default operand size 是 64 位的,详细的讨论在 《解开 64 位下 operand size 的迷惑》 一文: http://www.mouseos.com/x64/puzzle03.html