这里主要探讨 x64 体系指令集上 64 位扩展技术的实现,关于 x64 体系 64 位架构扩展技术的讨论,请参见 【x86 & x64 沉思录】 中的相关文档。
从 32 位扩展到 64 位变化比从 16 位扩展到 32 位要大,x64 体系下的改变主要是:
寄存器编码 |
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 |
x64 体系的寄存器扩展为 64 位。如上表所示,32 位宽的通用寄存器 eax ~ edi 扩展成为 rax ~ rdi
64 位模式下的缺省地址是 64 位的。
上表中的蓝色部分是 x64 下新增的通用寄存器 r8 ~ r15,如上表所示这些 64 位的寄存器还可以有 16 位和 32 位宽的相应寄存器。64 位模式下还增加了 [rip + disp32] 的寻址模式。
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:
|
REX prefix 的取值范围是 40 ~ 4F
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 prefix 是 41,下面看看 REX prefix 的结构:
REX = 41 0100 0 0 0 1 | |
指令编码使用了 REX.W = 0 指令的 operand size 是 32 位的。
下面这条指令:
bits 64 |
使用 32 位的 operand,这时候它并不需要 REX prefix,因为它不访问 64 位的寄存器,它的指令编码是:
89 D8 |
如果改为使用 64 位的寄存器:
bits 64 |
它需要利用 REX prefix 进行 64 位的扩展访问,它的指令编码是:
48 89 D8 |
这个 48 字节就是 REX prefix,这个 REX prefix 结构如下:
0100 1 0 0 0
|
REX.R,REX.X 以及 REX.B 用来访问新增的 64 位寄存器和 64 位地址中的新增的 base 和 index 寄存器。
如下代码:
bits 64 |
对于第 1 条指令 mov rax, r9 它的机器编码是:
这 2 个机器编码都是正确的。而大多数编译趋向生成 4c 89 c8 (使用 89 opcode 码)
来看看 4c 89 c8 这个编码中的 REX prefix 与 ModRM 结构:
REX = 4c ModRM = c8
| | | | |
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
| | | | |
由这 2 个 Opcode 所描述的 operand 类型都是一样的,只是顺序不同。因此对于这类型的指令就产生了两种 编码方式
对于第 2 条指令:mov r10, r15 它的机器编码同样有 2 种编码方式:
其 Opcode 原理和第 1 条指令是一样的。 下面看看 4d 89 fa 编码中的 REX prefix 与 ModRM 结构:
REX = 4d ModRM = fa
| | | | |
第 2 种编码方式在这里就不再重复赘述了。
REX.B 其中一个扩展功能是对内嵌在 opcode 码内的寄存器进行扩展。 下面是这类指令的典型指令:
bits 64 |
这类指令的典型特征是:不需要 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 | | REX.B + Opcode.reg = 1010 |
Opocde.reg 是 010,那么 REX.B + Opcode.reg = 1 + 010 = 1010 (r10 寄存器编码) 这类指令并不多。
在 64 位模式下,default address size 是 64 位的,如果有以下指令:
bits 64 |
那么它将产生 address size override 操作编码。生成的指令编码是:
使用了 67H prefix 进行 address size override
与 operand size 情形不同,在 64 位模式下 不应该使用 32 位地址,没有理由去使用 32 位地址。而应该使用 64 位的地址,上面指令应该改为:
bits 64 |
在基于寄存器间接寻址模式中使用了新增的寄存器,那么 REX prefix 中相应的扩展位为 1,REX.B 和 REX.X 用来扩展地址寻址模式中的寄存器
看一看下面的指令:
mov eax, [r10 + r12 * 8] |
它的编码是:
看一看 REX prefix 与 SIB 字节结构:
REX = 43 SIB = e2 0100 0 0 1 1 11 100 010 | | | | |
这条指令的寻址模式是 基址+变址,基址使用了新增的 r10 寄存器,变址使用了 r12 寄存器。因此 REX prefix 需要使用 REX.X 与 REX.B 进行扩展 index 和 base 寄存器。
在使用直接地址值的情况下是不需要进行扩展的。如下
mov rax, [0x000000138fc2c000] |
它的编码是:
这里的 REX.RXB 将会被忽略
在 64 位模式下新增了 [rip + disp32] 这种寻址模式:基于 rip 的偏移模式,但是对于一些编译器来说,在语法层面上来说,并不直接支持这样写法,比如:nasm,另一个例子是 yasm 它对 rip-relative 就很好地直接支持。
在 yasm 下,有下面的代码:
bits 64 offset_table: |
它们的编码组织起来象下面这样:
00000000 488B0508000000 mov rax,[rip + 0x08] |
关键在于 ModRM 字节:
ModRM = 05 00 000 101 |
ModRM.r/m = 101 时,在 64 位模式下,它的寻址模式是:[rip + disp32],在 ModRM 字节后面跟着 displacement 部分。
在 nasm 下,语法形式有很大差别:
bits 64 offset_table: |
rel 关键字告诉 nasm 将使用 relative 的地址模式,与之相对应的是 abs 关键字,使用 absolute 地址模式
[rel 0xf] 表明要取得 [0xf] 地址,它是由 rip + 0x08 而得来的。因此 nasm 会生成 rip-relative 的编码,最终的指令编码和 yasm 是一样的。