首页
论坛
课程
招聘
[原创]深入浅出指令编码之三:64位计算
2008-12-1 02:59 12527

[原创]深入浅出指令编码之三:64位计算

mik 活跃值
4
2008-12-1 02:59
12527
第二节 64位计算
  开章明义,由于这系列只是论述指令编码,所以这里讲的只是x64提供的64位计算,而非64位编程,故此节话题用64位计算而非64位编程。 关于64位编程方面以后有机会再讲解。
  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

    关于REX prefix的讲解到此为止,整个prefix(x86 prefix以及x64 prefix)的讲解也接近尾声了。
   接着后面继续探索 Opcode 话题 :) 

第五届安全开发者峰会(SDC 2021)议题征集正式开启!

收藏
点赞0
打赏
分享
最新回复 (8)
雪    币: 0
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
adsljz 活跃值 2008-12-1 05:30
2
0
强大`~~~~~
雪    币: 2273
活跃值: 活跃值 (21)
能力值: (RANK:300 )
在线值:
发帖
回帖
粉丝
北极狐狸 活跃值 7 2008-12-1 09:47
3
0
mik速度很快啊........加油..
期待opcode
雪    币: 201
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
qtdszws 活跃值 2008-12-1 11:11
4
0
学习了

1.同样的编码,解码可能不一样
2.64位指令都得加rex前缀
雪    币: 695
活跃值: 活跃值 (55)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
mik 活跃值 4 2008-12-1 11:47
5
0
错了!

64位指令不全是都得加 rex  prefix!

我上面已经说过,有一部分指令的 default operand-size 是 64 位的,这些指令在有些情况下是不需要加 rex prefix 的。
它们是 rsp 依赖的指令以及 call/jmp 跳转指令

例1: push qword ptr [rip + 0x11223344]
这条指令是 64 位指令,default operand-size 是 64 位的。将 [rip + 0x11223344] 的 64 位操作数入栈。
它的机器编码是:ff 35 44 33 22 11

这里有另一个话题:x64 新增的 rip 编移寻址(rip-relative address),地址值=rip + 32 位 offset。

能不能 push qword ptr [0x1122334455667788] 呢?
答案是:不能!   那要怎么做呢? 

只能:
mov rax, 0x1122334455667788
push qword ptr [rax]

例2:call qword ptr [rip + 0x11223344]
这是一条 64 位指令,和 例1 的情形一样。它的 rip = [rip + 0x11223344]
它的机器编码是:
ff 15 44 33 22 11

例3:jmp $+1
这是一条 64 短跳转指令,它的 rip = rip + 1
它的机器编码是:
eb 01

---------------------------------------
很简单吧。呵呵
雪    币: 695
活跃值: 活跃值 (55)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
mik 活跃值 4 2008-12-1 12:18
6
0
补充一点,amd 新增的 rip-relative address (rip偏移寻址) 是很有用的寻址方式:

例1:看看以前的怎么获得 eip 值的?

00000000: e8 04 00 00 00                          ; call $+4
00000005: 0f 1f 40 00                                 ; 填充 4 个 nop
00000009:58                                             ; pop eax 获得 eip 值
0000000a:  ... ...                                          ; next ...

例2:看看 x64 上是如何获得 rip 值的?

00000000_00000000: 48 8d 05 00 00 00 00            ; lea rax, [rip] 获得rip
00000000_00000007: 90                                          ; nop

----------------------------------------------------------
这就是 rip-relative 寻址的其中一个妙用。 :)
雪    币: 134
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
干掉上帝 活跃值 2009-5-31 22:32
7
0
rip偏移寻址)好方便
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chdieawoy 活跃值 2009-6-4 00:17
8
0
When I was in Second Year of high school, I had a math’s teacher named Mr. Wang who really changed my life by inspiringme to love mathematics.discount fashion jewelry company from chinabeautiful jade jewelry onlinefashion silver jewelry wholesalefantasitc jewelry  good quality pearlgood quality jewelry pearl company from china
雪    币: 564
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lixupeng 活跃值 2011-3-20 21:34
9
0
学习!!!!
游客
登录 | 注册 方可回帖
返回