x64 体系 64 位扩展技术的实现

返 回


这里主要探讨 x64 体系指令集上 64 位扩展技术的实现,关于 x64 体系 64 位架构扩展技术的讨论,请参见 【x86 & x64 沉思录】 中的相关文档。


1. x64 体系的扩展资源

从 32 位扩展到 64 位变化比从 16 位扩展到 32 位要大,x64 体系下的改变主要是:

表1:通用寄存器编码表

寄存器编码
16
32
64
000
ax
eax
rax
001
cx
ecx
rcx
010
dx
edx
rdx
011
bx
ebx
rbx
100
sp
esp
rsp
101
bp
ebp
rbp
110
si
esi
rsi
111
di
edi
rdi
1000
r8w
r8d
r8
1001
r9w
r9d
r9
1010
r10w
r10d
r10
1011
r11w
r11d
r11
1100
r12w
r12d
r12
1101
r13w
r13d
r13
1110
r14w
r14d
r14
1111
r15w
r15d
r15

1.1 64 位的寄存器

x64 体系的寄存器扩展为 64 位。如上表所示,32 位宽的通用寄存器 eax ~ edi 扩展成为 rax ~ rdi

1.2 64 位的地址

64 位模式下的缺省地址是 64 位的。

1.3 新增的寄存器与寻址模式

上表中的蓝色部分是 x64 下新增的通用寄存器 r8 ~ r15,如上表所示这些 64 位的寄存器还可以有 16 位和 32 位宽的相应寄存器。64 位模式下还增加了 [rip + disp32] 的寻址模式。


2. 访问 64 位的寄存器

x64 设计其中一个原则是:从 32 位的基础上平滑地扩展为 64 位。基于这个原则下,x64 的最终设计方案导致在 64 位模式下,绝大部分指令的 default operand size 是 32 位。关于 x64 体系的 32 位平滑扩展到 64 位技术,我将在 【x86 & x64 沉思录】 中详细探讨。

由于指令的 default operand size 是 32 位。因此,必须要有一个方法去访问 64 位的数据和地址。 x64 指令体系新增 REX prefix 用来支持 64 位访问。

REX prefix:


0100 X X X X
---- - - - -
     | | | |
     | | | +---------> 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

REX prefix 的取值范围是 40 ~ 4F


2.1 访问缺省的 operand size

REX.W 标志位用来指示指令的 operand size 使用 default operand size 还是 64 位。

下面这条指令:

mov eax, [r8]

指令的 目标/源操作数 都是 32 位的,它使用 default operand size,但是地址模式是 64 位的,并且 base 寄存器是新增 r8 寄存器。将 base 寄存器设为 r8 寄存器是为了 使用 REX prefix 这条指令的编码是:

41 8b 00

它的 REX prefix41,下面看看 REX prefix 的结构:

REX = 41

0100 0 0 0 1
---- - - - -
  4  W R X B

     |
     +----------------> REX.W = 0:使用 default operand size    

指令编码使用了 REX.W = 0 指令的 operand size 是 32 位的。


2.2 使用扩展的 64 位寄存器

下面这条指令:

bits 64
    mov eax, ebx

使用 32 位的 operand,这时候它并不需要 REX prefix,因为它不访问 64 位的寄存器,它的指令编码是:

89 D8

如果改为使用 64 位的寄存器:

bits 64
    mov rax, rbx

它需要利用 REX prefix 进行 64 位的扩展访问,它的指令编码是:

48 89 D8

这个 48 字节就是 REX prefix,这个 REX prefix 结构如下:

0100 1 0 0 0
---- - - - -
 4   W R X B


REX.W = 1 说明使用 64 位的 operand size
REX.R,REX.X 以及 REX.B 都为 0 这是说明指令不需要访问扩展的寄存器


2.3 访问新增的 64 位寄存器

REX.R,REX.X 以及 REX.B 用来访问新增的 64 位寄存器和 64 位地址中的新增的 base 和 index 寄存器。

如下代码:

bits 64
    mov rax, r9
    mov r10, r15


2.3.1 第 1 条指令的源操作数是新增的 r8 寄存器,目标操作数是原有扩展为 64 位的 rax 寄存器

对于第 1 条指令 mov rax, r9 它的机器编码是:

这 2 个机器编码都是正确的。而大多数编译趋向生成 4c 89 c8 (使用 89 opcode 码)

来看看 4c 89 c8 这个编码中的 REX prefix 与 ModRM 结构:

REX = 4c                             ModRM = c8


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

       |   |                           |    |
       |   |       0 + 000 = 0000      |    |
       |   +---------------------------|----+
       |                               |
       |        1 + 001 = 1001         |
       +-------------------------------+

REX.R 扩展 ModRM.reg 域产生 4 位的寄存器编码。这个编码就是 r9 寄存器。 而 REX.B 扩展 ModRM.r/m 域,同样产生 4 位的寄存器编码,这是 rax 寄存器的编码值。

89 Opcode 表达为:MOV Ev, Gv

E 类型的 opernad 由 ModRM.r/m 提供,而 G 类型的 operand 由 ModRM.reg 提供,因此:第 1 种编码方式:

8b Opcode 表达为:MOV Gv, Ev,它与 89 Opcode 的 operands 正好相反,那么它的编码: 49 8b c1

REX = 49                             ModRM = c1


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

       |   |                           |    |
       |   |       1 + 001 = 1001      |    |
       |   +---------------------------|----+
       |                               |
       |        0 + 000 = 0000         |
       +-------------------------------+

由这 2 个 Opcode 所描述的 operand 类型都是一样的,只是顺序不同。因此对于这类型的指令就产生了两种 编码方式


2.3.2 第 2 条指令的源操作数是新增 r15 寄存器,目标操作数也是新增的 r10 寄存器

对于第 2 条指令:mov r10, r15 它的机器编码同样有 2 种编码方式:

其 Opcode 原理和第 1 条指令是一样的。 下面看看 4d 89 fa 编码中的 REX prefix 与 ModRM 结构:

REX = 4d                             ModRM = fa


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

       |   |                           |    |
       |   |       1 + 010 = 1010      |    |
       |   +---------------------------|----+
       |                               |
       |        1 + 111 = 1111         |
       +-------------------------------+

第 2 种编码方式在这里就不再重复赘述了。

 

2.4 扩展 Opcode.reg 域

REX.B 其中一个扩展功能是对内嵌在 opcode 码内的寄存器进行扩展。 下面是这类指令的典型指令:

bits 64
    mov r10, 0x01

这类指令的典型特征是:不需要 ModRM 字节 进行 operand 寻址。

这条指令同样可以有两种编码方式,下面我们来看看生成的典型指令编码是(大多数编译器选择生成):

00000000  49BA0100000000000000  mov r10,0x1

其中 49 是 REX prefix,BA 是 Opcode 码,剩下的字节是 immediate 部分。它的 REX prefix 是:

REX = 49                       Opcode = BA

0100 1 0 0 1                  1011 1 010
---- - - - -                  ---- - ---
     W R X B                       w reg

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

                REX.B + Opcode.reg = 1010

Opocde.reg 是 010,那么 REX.B + Opcode.reg = 1 + 010 = 1010 (r10 寄存器编码) 这类指令并不多。


3. 访问 64 位地址

在 64 位模式下,default address size 是 64 位的,如果有以下指令:

bits 64
    mov rax, [eax]

那么它将产生 address size override 操作编码。生成的指令编码是:

使用了 67H prefix 进行 address size override

operand size 情形不同,在 64 位模式下 不应该使用 32 位地址,没有理由去使用 32 位地址。而应该使用 64 位的地址,上面指令应该改为:

bits 64
    mov rax, [rax]


3.1 在地址中使用新增的寄存器

在基于寄存器间接寻址模式中使用了新增的寄存器,那么 REX prefix 中相应的扩展位为 1REX.BREX.X 用来扩展地址寻址模式中的寄存器

看一看下面的指令:

mov eax, [r10 + r12 * 8]

它的编码是:

看一看 REX prefix 与 SIB 字节结构:

REX = 43                           SIB = e2

0100 0 0 1 1                    11   100  010
---- - - - -                    --   ---  ---
  4  W R X B                  scale index base

         | |                         |     | 
         | |     1 + 010 = 1010      |     |
         | +-------------------------|-----+
         |                           |
         +---------------------------+
                 1 + 100 = 1100

这条指令的寻址模式是 基址+变址,基址使用了新增的 r10 寄存器,变址使用了 r12 寄存器。因此 REX prefix 需要使用 REX.X 与 REX.B 进行扩展 index 和 base 寄存器。


3.2 使用直接地址值的情况

在使用直接地址值的情况下是不需要进行扩展的。如下

mov rax, [0x000000138fc2c000]

它的编码是:

这里的 REX.RXB 将会被忽略


3.3 使用新的地址寻址模式

在 64 位模式下新增了 [rip + disp32] 这种寻址模式:基于 rip 的偏移模式,但是对于一些编译器来说,在语法层面上来说,并不直接支持这样写法,比如:nasm,另一个例子是 yasm 它对 rip-relative 就很好地直接支持。

yasm 下,有下面的代码:

bits 64
    mov rax, [rip + 0x08]

offset_table:   
    dq 0x0
    dq 0x8
    dq 0x10

它们的编码组织起来象下面这样:

00000000  488B0508000000        mov rax,[rip + 0x08]
00000007  0000000000000000     
0000000F  0800000000000000
00000017  1000000000000000

关键在于 ModRM 字节:

ModRM = 05

  00  000  101
  --  ---  ---
 mod  reg  r/m
     
            |
            |
            +---------------> r/m = 101 提供 [rip + disp32] 寻址模式

ModRM.r/m = 101 时,在 64 位模式下,它的寻址模式是:[rip + disp32],在 ModRM 字节后面跟着 displacement 部分。

在 nasm 下,语法形式有很大差别:

bits 64
    mov rax, [rel 0xf]

offset_table:   
    dq 0x0
    dq 0x8
    dq 0x10

rel 关键字告诉 nasm 将使用 relative 的地址模式,与之相对应的是 abs 关键字,使用 absolute 地址模式

[rel 0xf] 表明要取得 [0xf] 地址,它是由 rip + 0x08 而得来的。因此 nasm 会生成 rip-relative 的编码,最终的指令编码和 yasm 是一样的。

 


版权 mik 所有,转载请注明出处