

深入浅出指令编码之三:64位计算
source link: http://abcdxyzk.github.io/blog/2013/12/23/assembly-opcode/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入浅出指令编码之三:64位计算
2013-12-23 17:55:00
http://www.pediy.com/kssd/pediy10/77824.html
AMD 在x86体系的32位计算扩展为64位计算,这是通过什么来实现的?它是怎样设计的?具体细节是什么?这就是这一节要讲解的。
一、硬件编程资源
了解现在processor提供编程资源是很重要的,对要进一步学习提供材料,下面分别讲解x86的编程资源和x64的编程资源。
1、x86的32位编程资源
● 8个32位通用寄存器:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI
这些寄存器还可分解为8个8位寄存器:AL、CL、DL、BL、AH、CH、DH、BH
和8个16位寄存器:AX、CX、DX、BX、SP、BP、SI、DI
● 6个段寄存器:ES、CS、SS、DS、FS、GS
● 32位的EFLAGS 标志位寄存器
● 32位的指令指针寄存器EIP
● 8个64位MMX寄存器
● 8个128位XMM寄存器
● 还有就是32位的寻址空间(Virtual Address Space)
2、x64的64位编程资源
● 32位通用寄存器被扩展至64位,除了原有的8个寄存器,新增8个寄存器,共16个通用寄存器:RAX、RCX、RDX、RBX、RSP、RBP、RSI、RDI、R8、R9、R10、R11、R12、R13、R14、R15
● 保留了原有的6个寄存器,但是作用被限制
● 32位的标志寄存器被扩展为64位的标志寄存器RELAGS
● 8个64位MMX寄存器不变
● 新增8个XMM寄存器,共16个XMM寄存器
● 还有就是64位的寻址空间(Virtaul Address Space)
二、寄存器编码(或者说ID值)
● 16个64位通用寄存器是: 0000 ~ 1111,也就是:0 ~ 15
8个32位通用寄存器是: 000 ~ 111 也就是:0 ~ 7
● 6个段寄存器的编码是:000 ~ 101 也就是:0 ~ 5
● MMX寄存器编码是: 000 ~ 111 也就是:0 ~ 7
● 16个XMM寄存器编码是: 0000 ~ 1111 也就是:0 ~ 15
8个XMM寄存器编码是:000 ~ 111 也就是:0 ~ 7
所谓寄存器编码是寄存器对应的二进制编码,按顺序来定义,看下面的表格:
RAX/ES/MMX0/XMM0 -> 0000
RCX/CS/MMX1/XMM1 -> 0001
RDX/SS/MMX2/XMM2 -> 0010
RBX/DS/MMX3/XMM3 -> 0011
RSP/FS/MMX4/XMM4 -> 0100
RBP/GS/MMX5/XMM5 -> 0101
RSI/MMX6/XMM6 -> 0110
RDI/MMX7/XMM7 -> 0111
R8/XMM8 -> 1000
R9/XMM9 -> 1001
R10/XMM10 -> 1010
R11/XMM11 -> 1011
R12/XMM12 -> 1100
R13/XMM13 -> 1101
R14/XMM14 -> 1110
R15/XMM15 -> 1111
RAX ~ RDI 与 EAX ~ EDI 的编码是相同的,这里有一个情况是,EAX ~ EDI的编码是3位,为什么RAX~RDI的编码却是4位呢?这就是下面要讲到的REX prefix会将寄存器编码进行扩展。
三、 开启64位计算的基石(REX prefix)
AMD64体系的64位计算是这样设计:操作数的Default Operand-Size是32位,而Address-Size是固定为64位的,这里就引发3个问题要解决的:
● 问题1:当要访问是64位的寄存器时,那么必须要有一种机制去开启或者说确认访问的寄存器是64位的。
● 问题2:而要访问的内存操作数寄存器寻址的话,那么也必须要去开启或确认寄存器是64位的以及访问新增寄存的问题。
● 问题3:如何去访问新增加的几个寄存器呢?那么也必须要有方法去访问增加的寄存器?
那么在64位Long模式下,为什么不将操作数的Default Operand-Size设计为64位呢?那是由于体系限制,本来AMD64就是在 x86的基础上扩展为64位的。x86体系当初设计时就没想到有会被扩展到64位的时候。所以在Segment-Descriptor(段描述符)里就没 有可以扩展为64位的标志位。DS.D位只有置1时是32位,清0时为16位,这两种情况。
AMD在保持兼容的大提前下,只好令谋计策,AMD的解决方案是:增加一个64位模式下特有Prefix,以起到扩展访问64位的能力。这就是 REX prefix。
1、REX prefix 的具体格式及含义
REX prefix的取值范围是:40 ~ 4F(0100 0000 ~ 0100 1111),来看下原来opcode取值范围的40 ~ 4F的是什么指令:
Opcode为40 ~ 47在x86下是inc eax ~ inc edi 指令,48 ~ 4F在x86下是dec eax ~ dec edi 指令。
在64位模式下,40 ~ 4F 就已经不是指令而变身为 prefix了。
1.1 REX prefix字节的组成部分:
● bit0:REX.B
● bit1:REX.X
● bit2:REX.R
● bit3:REX.W
● bit4 ~ bit7:此域固定为0100,也就是高半字节为4。
★ REX.W域是设定操作数的大小(Operand-Size),当REX.W为1时,操作数是64位,为0时,操作数的大小是缺省大小(Default Opeand-Size)。这就解决了访问64位寄存器的问题。
★ REX.R域是用于扩展ModRM字节中的R(Reg)域,ModRM中的Reg域除了对Opcode的补充外,是用来定义寄存器的编码,即寄存器 值。REX.R将原来3位的寄存器ID(000 ~ 111)扩展为4位(0000 ~ 1111),这就解决了访新增寄存器的问题。
★ REX.X域是用于扩展SIB字节中的Index域,SIB中的Index域是指明Index 寄存器的编码,即ID值。这就解决了寄存器寻址内存中使用新增寄存器的问题。
★ REX.B域是用于扩展ModRM字节中的r/m域和SIB中的Base域,SIB中的Base域指明Base寄存器编码即ID值。这就解决了寄存器寻址内存中使用新增寄存器的问题。
★ REX.B域的另一个作用是:若指令中没有ModRM和SIB,也就是在Opcode中直接给出寄存器ID值,REX.B起到扩展寄存器的作用。
1.2、下面使用几个例子来说明问题:
例1:指令 mov eax, 1
这条指令的Default Operand-Size是32位,在32位下它的机器编码是:b8 01 00 00 00(其5个字节)若改成64位编码时,变成 mov rax, 1。
此时,它的机器编码是 48 b8 01 00 00 00 00 00 00 00 (共10个字节)
在这里48 就是 REX prefix字节,即:0100 1000 它的各个域值是:REX.W = 1,定义操作数是64位的,REX.R = 0、REX.X = 0、 REX.B = 0 这条指令不需要ModRM和SIB字节,所以RXB域都为0。
这里有个值得思考的地方,若 REX.W域为0时,这条指令的操作数是32位的,也就是说,机器编码:40 b8 01 00 00 00(其6个字节)是与 b8 01 00 00 00结果一样的,都是mov eax, 1
例2:指令:mov rax, r14
这是一条常见64位指令,源寄存器是r14,目标寄存器是rax 它的机器编码是:
4c 89 f0(共3个字节)
在这个编码里4c是REX prefix,89是opcode,f0是ModRM。
REX Prefix的值是4c (0100 1100),其中REX.W = 1,REX.R = 1,XB都为0。
ModRM的值是F0(11-110-000),Mod=11,Reg=110, R/M = 000,在这里先不讲ModRM的含义,在后面的章节再详述。在这条指令里,Reg表示源操作数r14的ID值。
r14是新增加寄存器,所以需要REX.R进行扩展,得出最终寄存器的ID值,1+110 = 1110,这是r14寄存器的ID值,从而得出正确的编码。
例3:回到序言里的例子:mov word ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678
作为例子,我将它改为64位指令,如下:
mov qword ptr [rax + rcx * 8 + 0x11223344], 0x12345678
操作数大小变为64位,而base 寄存器和index寄存器都改为64位,disp(offset)和imme(值不变),为啥不变?在以后的章节会有详述。
好,现在来看看指令怎么译:
(1) REX.W: 要置为 1 以扩展64位大小。
(2) REX.B: 由于base不是新增的寄存器,所以置为 0
(3) REX.X: 由于index 也不是新增的寄存器,所以置为 0
(4) REX.R: 源操作数和目标作数不是寄存器,所以置为 0
所以,REX prefix就等于 48(0100 1000)
故,整条指令编码是:48 c7 84 c8 44 33 22 11 78 56 34 12(共12个字节)
例4:我将上面的例子再改一改,变为:mov qword ptr [r8 + r9 * 8 + 0x11223344], 0x12345678
那么,看看这指令怎么译:
(1) REX.W:置1,使用64位大小
(2) REX.B:base寄存器是r8,是新增寄存器,所以置为1
(3) REX.X:index寄存器是r9,是新增寄存器,所以置为1
(4) REX.R:操作数中没有寄存器,所在置为0
所以,REX prefix就等于(0100 1011)4b
故,整条指令编码是:4b c7 84 c8 44 33 22 11 78 56 34 12(共12个字节)
例5:看看这条指令 mov r8, 1
(1) REX.W:置1
(2) REX.B:访问Opcode中的寄存器ID值,它是新增寄存器,所为置1
(3) REX.X:置0
(4) REX.R:置0
所以,REX是 49(0100 1001)
故整条指令编码是:49 b8 01 00 00 00 00 00 00 00
2、REX prefix补充说明
(1)关于顺序:REX一定是在x86 prefix之后,而在Opcode之前。
(2)关于冲突:当x86 prefix和 REX prefix同时出现,而又出现冲突时,REX的优先权要优于 x86 prefix,
举个例子:指令 mov r8, 1
若出现以下编码怎么办:66 49 b8 01 00 00 00 00 00 00 00 既有66 又有49,那么结果66会被忽略,也就等于:49 b8 01 00 00 00 00 00 00 00。
而对于 66 b8 01 00 00 00 00 00 00 00 这个编码来说:会被解析为:mov ax, 1
去掉了49这个REX prefix操作数被调整为 16 位。
(3)关于原来Opcode码,由于40 ~ 4F被作为 REX prefix,那么原指令inc reg/dec reg,只能使用 FF/0 和 FF/1 这两个Opcode了。
(4)缺省操作数大小(Default Operand-Size)
64位绝大部分缺省操作数是32位的,但有一部分是64位的,依赖于rsp的寻址和短跳转(near jmp/near call)是64位的。
如下指令:push r8
REX值是41(0100 0001),即REX.W为0,使用default opearnd-size
它的编码是 41 ff f0
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK