上一页 返回目录 下一页
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 位被保留起来,用作符号扩展。
表1:寄存器 ID 表
扩展位
|
寄存器
|
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 呢?在这种情况下,operand size 是 64 位的。66H prefix 将会被忽略,REX prefix 产生了作用。
第 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 |
名称
|
bit 位
|
描述
|
--
|
[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 位操作数,应该使 REX.W = 1 使用立即数会进行符号扩展到 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