3. REX prefix


上一页 返回目录 下一页

REX prefix 是实现 x64 平台的 64 位计算的手段,通过 REX prefix 来扩展访问 64 位资源。这里就 REX prefix 进行探讨。

关于 64 位扩展技术和 REX prefix 的讨论,可参见另一篇文档:《x64 体系 64 位扩展技术的实现》 http://www.mouseos.com/x64/extend64.html


1. 先看看 x64 扩展的 64 位编程资源

x64 体系扩展和新增了编程资源:

  1. 32 位通用寄存器被扩展至 64 位,并且除了原有的 8 个寄存器,又新增 8 个寄存器。共 16 个通用寄存器:rax、rcx、rdx、rbx、rsp、rbp、rsi、rdi、r8、r9、r10、r11、r12、r13、r14、r15
  2. 保留了原有的 6 个段寄存器,但是作用被限制
  3. 32 位的标志寄存器被扩展为 64 位的标志寄存器 rflags
  4. 8 个 64 位 MMX 寄存器不变(mmx0 - mmx7)
  5. 新增 8 个 XMM 寄存器,共 16 个 XMM 寄存器(xmm0 - xmm15)
  6. 64 位的寻址空间(Linear Address Space):00000000_00000000 - FFFFFFFF_FFFFFFFF

x64 体系在 64 位寻址空间实际上只实现了 48 位 virtual address 寻址空间,高 16 位被保留起来,用作符号扩展。


2. 寄存器编码(或者说 ID 值)

表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 byte2 bytes4 bytes8 bytes 宽度下的名称和编码:

注意表格中的几个特别之处:

  1. x64 完善了整个寄存器编码体系,增加了 splbplsildil 这 4 个 1 byte 寄存器,在上图蓝色标注处。
  2. 由于 mmx 和 sreg(segment register)并没有新增,所以对于 mmx 和 sreg 来说,使用 0000 - 0111 和 1000 - 1111 的结果都是一样的

上图所示:ah - bhspl - dil 编码一样,那么 processor 是如何识别它们呢?

扩展位
100
101
110
111
---
ah
ch
dh
bh
0
spl
bpl
sil
dil

实际上 spl, bpl, sil 以及 dil 寄存器仅在 64 位模式下有效的,它们需要扩展位进行扩展,这个扩展位为 0

看下面的几个例子:

  1. 指令:mov al, ah
  2. 这个很容易理解,这条指令的机器码是:8a c4

  3. 指令:mov al, spl
  4. 那么,再来看一看这条指令,是如何译为机器码的?指令中使用了 spl 寄存器,这个寄存器仅在 64 位模式是有效的。这条指令的机器码中的 Opcode 和 ModRM 与上一条是完全一样的,但是需要增加了 REX prefix 来确定 spl 寄存器,最终结果为:40 8a c4
    这是基于:ah = 000,而 spl = 0000(需要 REX prefix 进行扩展)

  5. 指令:mov al, bl
  6. 最后,看一看这条指令的机器码是多少?
    事实上,在 Opcode 相同的提前下,这条指令在 x64 下可以有两种译法:


    扩展位
    000
    001
    010
    011
    ---
    al
    cl
    dl
    bl
    0
    al
    cl
    dl
    bl

    可以看出,在 000 - 011 编码中,即使增加了扩展位,它们还是一样的。

  7. 我们再来看看这两条指令,结果会是什么?
  8. 它们的寄存器编码都是一样的,显然有一条必定是错误的。
    第一条指令中,spl 寄存器与 ch 寄存器产生了冲突,冲突自来于:spl 需要 REX 进行扩展,而 ch 寄存器在 REX 的扩展下将会变成 bpl 寄存器,因此,在 REX prefix 的前提下是不存在 ch 寄存器的,这正是第二条指令的结果,所以第二条指令是正确的。


3. REX prefix --- 开启 64 位计算的基石

上面几个例子中提到的 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 主要作用是:


4 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
---- - - - -
 4   W R X B
    
     | | | |
     | | | |
     | | | +---------------> B: 用来扩展 SIB.base, ModRM.r/m 以及 Opcode.reg
     | | |
     | | +-----------------> X: 用来扩展 SIB.index 域
     | |
     | +-------------------> R: 用来扩展 ModRM.reg 域
     |
     +---------------------> W: operand width 标志位,当 W = 0 时使用 default operand size
                                                       当 W = 1 时使用 64 位 operand size


4.1 REX.W 的含义

  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 位的。


4.2 REX.R 的含义

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
---- - - - -            -- --- ---
 4   W R X B           mod reg r/m

       |                    |
       |                    |
       +--------------------+
           1 + 010 = 1010

源操作数 = REX.R + ModRM.reg = 1 + 010 = 1010

指令中的 REX.R = 1 它扩展了 ModRM.reg 域,使寄存器的 ID = 1010(r10寄存器编码),使得能够访问源操作数 r10 寄存器。


4.3 REX.X 的含义

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
---- - - - -            --  ---  ---
 4   W R X B         scale index base

         |                   |
         |                   |
         +-------------------+
            1 + 010 = 1010

 

index 寄存器:REX.X + SIB.index = 1 + 010 = 1010

地址寻址中的 index 寄存器是新增的 r10 寄存器,通过 REX.X 进行扩展从而访问新增的 index 寄存器。


4.3 REX.B 的含义

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 部分:


4.3.1 扩展 SIB.base

下面这条指令:

mov rax, [r8 + rcx * 8]

这条指令的 memory 操作数的 base 寄存器是 r8,它需要 REX.B 进行扩展,它的指令编码是:

它的 REX prefix 与 SIB 字节的结构图如下:

REX = 49                       SIB = c8


0100 1 0 0 1                11  001  000
---- - - - -                --  ---  ---
 4   W R X B             scale index base

           |                          |
           |                          |
           +--------------------------+
                 1 + 000 = 1000

 

base 寄存器:REX.B + SIB.base = 1 + 000 = 1000

base 寄存器的编码经过扩展后是 1000 它是 r8 寄存器。


4.3.2 扩展 ModRM.r/m

下面这条指令:

mov r10, rax

这条指令的 ModRM.reg 提供源操作数寻址,而 ModRM.r/m 提供目标操作数寻址,目标寄存器 r10 需要 REX.B 进行扩展,它的指令编码是:

它的 REX prefix 与 ModRM 结构图如下:

REX = 49                     ModRM = c2


0100 1 0 0 1                11 000 010
---- - - - -                -- --- ---
 4   W R X B               mod reg r/m

           |                        |
           |                        |
           +------------------------+
                 1 + 010 = 1010

 

目标操作数:REX.B + ModRM.r/m = 1 + 010 = 1010

目标操作数 r10 寄存器的编码经过 REX.B 扩展为 1010


4.3.3 扩展 Opcode.reg 域

在一部分指令的 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
---- - - - -                  ---- - ---
     W R X B                       w reg

           |                          |
           |                          |
           +--------------------------+

                REX.B + Opcode.reg = 1010

 

4.4 REX prefix 结构总结

名称
描述
--
[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 对指令的解码。

 

5.探索 REX prefix 设计方案

x64 体系的 64 bit 环境中:操作数的 Default Operand-Size 是 32 位,而 Default Address-Size 是 64 位的。

因此,在怎么设计 64 位的计算方案时,有 2 个问题要解决的:

  1. 当要访问是 64 位的寄存器时,那么必须要有一种机制去开启或者说确认访问的寄存器是 64 位的
  2. 如何去访问新增加的几个寄存器呢? 那么也必须要有方法去访问增加的寄存器?

那么在 x64 的 64 位模式下,为什么不直接将操作数的 Default Operand-Size 设计为 64 位呢?

主要是基于指令编码的原因:

  1. 由于原有 x86 体系中使用 3 bit 来表示 registers 编码,新增的 registers 则需要通过扩展编码来访问
  2. 考虑和平衡 16 位、32 位以及 64 位 operand size 的使用。若在 64 位模式下,default operand size 设计为 64 位,那么如何使用 16 位和 32 位的 operand size? 仅使用 operand size override prefix(66H prefix)不可能转换到这 2 种 operand size

这种情况下,REX prefix 就应运而生,它解决了:

所以,经过考虑下,新增 REX prefix 来使用 64 位 operand,而使 default operand size 为 32 位。这样设计比将 default operand size 定为 64 位,然后还是要新增另一个 prefix (多此一举之嫌)要好多了。

这样一来,也解决了前面提到的 64 位计算的 2 个问题。

x64 的 64 模式下,default address size 设计为 64 位

64 位模式下 default address size 是 64 位,可以使用 67H prefix(address size override)进行调整为 32 位 address,基于这个设计,当指令的地址是 64 位,不需要为地址操作数提供 REX prefix。


看看下面几个例子:

1. mov dword ptr [rax], 1

这条指令的 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 00REX.W = 1

c7 这个 opcode 指令描述为:MOV Ev, Iz,这意味着 immediate 最大的宽度是 32 位。因此,即使是 64 位的操作数,immediate 依然是 32 位宽


2. 指令 mov qword ptr [r8], 1

同前面所述,不同的是:地址中使用了 r8 作为 base 寄存器,它的 encode 是 1000 它需要提供 REX prefix(REX.W = 1 & REX.B = 1

最终编码为:49 c7 00 01 00 00 00


3. mov qword ptr [r8 + r10 * 8 + 0x0c], rax

这条指令的 memory 操作数中使用了 [base + index * scale + disp8] 的寻址模式,地址中 base 寄存器和 index 寄存器都使用了新增的寄存器,那么它需要:

REX prefix 的值为 4BH,指令的最终编码为:4b 89 44 d0 0c

4. mov dword ptr [eax], 1

这条指令地址中使用了 32 位地址模式,由于 default address size 是 64 位,因此需要使用 67H prefix(address size override)进行 override 为 32 位地址

最终编码为:67 c7 00 01 00 00 00

5. mov qword ptr [eax], 1

与上一条指令所不同的是:它使用了 64 位操作数,address size 同样还是 32 位,因此需要同时提供 67H prefix 以及 REX prefix

最终编码为:67 48 c7 00 01 00 00 00

 

5.1 64 位计算方案的解决之道

  1. 使用 REX.W 来解决访问 64 位操作数的能力。在 REX.W = 1 就开启了 64 位计算能力,使用 64 位操作数
  2. 使用 REX.R、REX.X 以及 REX.B 来访问新增的 8 个寄存器的能力。如:ModRM.reg = 000 而 REX.R = 1,组合的寄存器 ID 为 1000,这是寄存器 r8

下面使用几个例子来说明解决之道

1. mov eax, 1

这条指令的 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

2. mov rax, r14

这是一条平常 64 位指令,它是需要 ModRM 来进行寻址的。源寄存器是 r14,目标寄存器是 rax

REX.W = 1, REX.X 将会被忽略,最终机器码是: 49 8b c6(共3个字节)


3. mov word ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678

这条指令的 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


4. mov r8, 1

这条指令的寄存器寻址嵌在 Opcode 中,Opcode.reg = 1000,REX prefix 应该:

Opcode.reg 将需要扩展,REX.R 以及 REX.X 会被忽略,你可以置它们为 1 或者为 0,并不影响指令的解析,指令编码结果是:49 b8 01 00 00 00 00 00 00 00


5.2 解开 REX prefix 迷惑

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 同时出现了,实际上它们作用起了冲突

冲突出现时,最终会作用于 REX prefix,66H prefix 的作用被忽略。

在 64 位模式下,由于 40 - 4F 被作为 REX prefix,那么原 inc/dec 指令,只能使用 FF /0FF /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


上一页 返回目录 下一页


版权所有 mik 2008 - 2014