标 题: 深入浅出指令编码之一 二 三
作 者: mik
时 间: 2008-11-26,15:40:45
链 接: bbs.pediy/showthread.php?t=77508

一 : 序

在开篇讲述学习x86指令编码之前,先给2个例子看看,相当于,学习C语言经典的第一节课。
引用:
main()
{
        printf(“hello,world!”);
}



一、汇编代码译为机器码

例子1:在当前32位机器,32位系统下,有如下汇编指令:
mov word ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678

分析这条汇编码:
这是一条 mov 指令,目标操作数是 mem, 源操作数是 imme, 

注意:我特地将操作数的大小定为是word(2个字节),而不是 dword,
源操作数故意定为0x12345678,这个dword大小的立即。


对应的机器编码是:26 66 c7 84 c8 44 33 22 11 78 56

现在,我对这个机器码略为解释一下:

26:  在指令序列里是:prefix部分,作用是调整内存操作数的段选择子
66:  在指令序列里是:prefix 部分,作用是调整操作数的缺省大小
C7:  在指令序列里是:Opcode部分,是mov指令是操作码
84:  在指令序列里是:ModRM值,定义操作数的属性
C8:  在指令序列里:SIB值定义内存操作数的属性
44332211: 在指令序列里是: displacement 值
7856:  在指令序列里是:immediate值

至于为什么会译为这个机器编码,在以后的章节里再学习



二、将机器码译为汇编码



例2:随便一个机器码如:FF 15 D4 81 DF 00

粗略分析一下:

FF:这个字节是个具有 Group属性的 Opcode码,它进行什么操作需要依赖于 ModRM字节的 Reg 域.。换句话来说,FF 并不是完整独立的 Opcode码,它要联合 ModRM才能确定具体的操作。

15:这个是ModRM 字节,Mod 域为 00   Reg 域为 010  RM 域为 101。 其中Reg域被FF作为确定具体操作码的参考。

   FF / 010 :最终确定为:Call 指令,
   Mod 域以及RM域确定操作数的属性,这是一个内存操作数是且是个 offset 值或者说是dis
placement 值。


所以,这个机器码最终被译为: call dword ptr [00DF81D4]



在这个序言里举2个例子,作为对学习x86指令编码的一个感性认识。以后的章节里逐一剖析x86指令编码的来龙去脉。

在下面的章节里,我将会结合实例学习指令编码格式,这里依然拿序言里的例子来看看。

mov dword ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678

所不同的是:将 word ptr 这个内存操作数指示字改回 dword ptr,这是个具有典型指令编码
意义的指令。它的encode(机器编码)是:
26 c7 84 c8 44 33 22 11 78 56 34 12
这个 encode 共12个字节。

好!现在继续往下讲解。




第1节  认识编码序列

instruction_form.jpg下载此附件需要消耗2Kx,下载中会自动扣除。 

如上图所示:
这个x86_x64 体系的 General-Pupose Instruction(通用体系指令)的编码格式,记住这个
编码序列很重要,这是解析指令编码的基石。

   这个编码序列分为Legacy Prefix、REX prefix、Opcode、ModRM、SIB、Displacement以及 Immediate 7个部分。
   实际上,将功能组别,我将这个指令序列分为4个部分,分别为:Prefix、Opcode、ModRM/SIB、Disp/Imme


●  Prefix(前缀):
AMD推出x86扩展64位技术时,增加了一个用于扩展访问64位数据的 REX prefix,而x86的prefix 是 Legacy prefix。
在x86模式下,REX prefix是无效的。但是,在x64的64位下 Legacy prefix 是有效的。

●  Opcode(操作码):
大多数通用指令Opcode是单字节,最多是2字节,但是对有些Float指令和SSEx midea指
令来说是3个字节的。

●  ModRM/SIB:
ModRM字节实际意义为:mod-reg-rm,按 2-3-3 比例划分字节,SIB 意即:Sacle-Index-Base 也是按2-3-3比例划分字节。
这两个字节用来修饰指令操作数。

●  Disp/Imme:
Displacement最大可为8个字节64位,当然8个字节的displacment只有在x64平台下才会有,displacement也可理解为 offset。同样immediate最大可为8个字节,同样在x64下台才会有的。
需要注意的一点是:displacement 和 immediate都是符号数(single),在32位下,小于32位被符号扩展至32位,在64位下,小于64位会被符号扩展64位。


对照上面的encode来看:
26 c7 84 c8 44 33 22 11 78 56 34 12

(1) 26是prefix,这是segment-override prefix,指明是ES段选择子
(2) c7是Opcode,表明这个指令是 mov reg/mem, imme
(3) 84是ModRm,即:10-000-100。
(4) c8 是 SIB,即:11-001-000
(5) 44332211 是disp,是32位displacement值
(6) 78563412 是 imme,是32位immediate值



OK,这节内容到现在,必须要掌握编码格式的序列。下一节将详细对每个组成部分进行讲解
第二节 深入了解Prefix

      在GPI(General-Purpose Instruction)指令里,Legacy Prefix 在整个编码序列里起了对内存操作数进行修饰补充作用,在这里我称呼它为x86 prefix,这样比较直观。x86 prefix主要起了三个作用:调整、加强、附加。REX prefix只是起将操作扩展64位的作用。
    要彻底了解x86 prefix,必须清楚了解3个很重要的上下文环境:缺省operand-size和缺省 addess-size 环境、编译器上下文环境以及当前执行上下文环境。


一、调整改变操作数
      x86指令编码会根据上面提到的3个上下文环境而对操作数的位置、大小以及地址进行调整改变。这里操作数特指是内存操作数。出现调整的情形,这是因为:
(1)  指令的操作数大小可以为:8位、16位、32位以及64位
(2)  操作数的位置因段选择子而不同。
(3)  操作数的地址大小可以为:16位、32位以及64位



1、  调整操作数的大小(66H prefix  ------ Default Operand-Size Override)
       66h 这个prefix 作用是改变操作数的大小,为什么改变?根据什么来改变。这就是根据上面提到过的3个很重要的上下文环境:
●  缺省操作数大小
●  编译器编译环境
●  当前执行环境
这3个环境是有机结合起来的,是个整体。


1.1、  缺省操作数大小(Default Operand-Size)
     对于实模式环境下,操作数的Default Operand-Size是16位,在32位保护模式下,操作数的Default Operand-Size是32位,在64位Long模式下Default Operand-Size也是32位大小。
     当在保护模式下,读取16位值时,则须作出调整,同样在实模式下,读取32位值时,测须作出调整。
     记住以上两段话,并理解它。

以下举2个例子加以说明:
例1:在32位保护模式下,指令:mov ax, [11223344h]
      在Microsoft的语法里,在内存操作数前一般要加指示字 word ptr,指明操作数的大小:mov ax, word ptr [11223344h] 实际上,在这条指令里,这个指示字不是必须的,加指示字只是比较直观。
    但有些情况是必须要加的,如:mov dword ptr [11223344], 1
      例1这条指令里,绝大多数编译器会编译为以下机器编码encode:
    66 a1 44 33 22 11 
     在这个encode里,66 是prefix,a1 是opcode,44332211是displacement或者说mem-offset
     66 改变了缺省的操作数大小,将32位调整为16位


例2:在16位实模下,同样一条指令:mov eax, [11223344]
      同样一样指令,只是目的操作数大小不同,在16位实模式下,这条指令将被编译器编译为:66 67 a1 44 33 22 11
      在这个encode里,66 prefix将16位缺省操作数调整为32位大小,67这也是prefix,但它是调整 Addess-Size preifx 将16位地址调整为32位地址。其余的字节和例1的完全一样。


1.2、  编译器编译上下文环境
     所谓“编译器上下文”,是指编译器编译目标平台上下文环境。说明白点就是:编译器为什么机器编译代码,是编译为16位代码,还是编译为32位代码或者是编译为64位代码?
     例如操作系统的引导初始化代码部分是16位的,现在绝大多数OS是32位的,因此,在当前系统下写引导代码,则需要求编译器编译为16位实模式代码。
     因此,你不得不写16位代码,编译器根据情况将32位操作和地址调整至16操作数和地址。但在大部分情况下,不需要作调整,直接生成16位代码即可。



1.3、  当前执行环境
      Processor处理什么模式下,这是程序员需要考虑的问题,从而通过代码体现出来,编译器根据代码生成相应的代码。
    一个很典型的例子就是:当16位初始化代码完以及保护模式系统数据结构初始化完成后开启保护模式,然后需要从16位代码跳转至32位代码。
    由于在一个汇编程序里同时存在16位和32位代码,所以,程序员在汇编级代码里应指出16位与32位分界线。编译器正确同样生成16位和32位代码。这里当然是通过Operand-Size的调整和Address-Size的调整,即:66H 和 67H prefix。
    此时,程序的脑海里,应存在这样一个概念,在运行16位代码时,processor当前处于实模式状态,跳转至32位保护模式代码时,processor当前处于保护模式状态。
    这就是processor当前执行上下文环境。


1.4、  对缺省操作数大小的深入说明
      通过,3个上下文环境来决定什么时候使用Operand-Size Override prefix。就缺省操作数大小(Default Operand-Size)这个环境来说,深入一点,其实就是,段选择子所对应的段描述符的D/B标志位所指出的。
    缺省的数据段内存操作数是基于DS段寄存器(选择子)的,也就是说,内存操作数的缺省大小其实就是DS Selector所指出的Data-Segment Descriptor中的D标志位(DS.D),当DS.D为1时,缺省操作数大小为32位,DS.D为0时,缺省操作数大小16位。



2、  调整地址大小(67H prefix  -----  Address-Size Override)
       Address-Size和Operand-Sizeg一样,也有缺省地址大小(Default Address-Size),当需要改变地址大小的时候,也需要使用67H prefix来进行调整,所不同的是,Default Address-Size不需要从Descriptor里获取。直接定义:
    在16实模式下Default Address-Size为16位,32位保护模式下Default Address-Size为32位,64位Long模式下Default Address-Size为64位。
    同样,记住上面这段话。

以下,也举几个例子来说明

例1:16位实模式下,序言里的指令:mov dword ptr [eax+ecx*8+0x11223344], 0x12345678
      由于在16位下,但该指令是32位operand-size以及32位address-size,也就是说既要调整default operand-size也要调整default address-size。所以,应加上66调整operand-size,再加上67调整address-size,最终的encode为:
    66 67 c7 84 c8 44 33 22 11 78 56 34 12

例2:在32位模式下,指令:mov eax, [11223344] 
      对该指令,编译器不会产生16位代码,所以,我们手工编译该指令,得出encode:
    67 a1 44 33 22 11
这条指令是不对的,用 67 调整为16位地址,那么在汇编码来看,它将是:
    mov ax, [3344]
      它的地址将被截断为16位,即,地址:0x3344,多出 22 11 两个字节属下条指令边界了,同时,目标操作数被改变为 ax
     除非,这样编码 66 67 a1 44 33  那么,结果是 mov eax, [3344]

3、  调整段选择子(段寄存器)
    对于大多数内存操数据来说,缺省以DS为段基址的。常见的是:DS段基址,SS段基址。
   
   来看看下面的代码片段:

Foo: 
     push ebp
     mov ebp, esp
     lea eax, [ebp-0xc]                     ;  int *p = &i;
     mov dword ptr [eax], 0                 ;  *p = 0
     ... ...
     mov esp,ebp
     pop ebp
------------------------------------------
[ebp-0xc]:这个内存操作数缺省是基于 SS 段的
[eax]:    这个内存操作数缺省是基于 DS 段的。

因此,正确的语义应该要这样才对:

lea eax, [ebp-0xc]
mov dword ptr ss:[eax], 0               ; 将DS 改变为SS,这才是正确的逻辑

    为什么一般程序都不会这么写呢? 那是因为,现代的操作系统都是采用平坦的内存模式,即:SS=DS=ES=FS=GS,所以对 [eax] 这个操作数不需调整其结果是正确的。

    那么,我们真要对 [eax] 内存操作数进行调整为:mov dword ptr ss:[eax], 0
      这样的话,会产生下面的encode:
    36 c7 00 00 00 00 00

     其中,36 也就是prefix,是SS segment-override的prefix

   好啦,每个段寄存都有它对应的 prefix,下面列出每个段寄存器的prefix:
CS: 2E
DS: 3E
ES: 26
FS: 64
GS: 65
SS: 36

     当需要进行调整段寄存器时,就使用以上的segment-override prefix。但有些指令的缺省
段寄存是ES,典型的如movsb这些串操作指令
    movsb 指令的实际意义是: movs byte ptr es:[edi], byte ptr ds:[esi],此时是不需要调整缺省段寄存器
第三章 指令编码核心
  这部分内容较比,花了多点时间去写,所以比较仔细。
  本章节讲解的核心是Opcode、ModRM以及SIB,这三者的已经是紧密结合,无论分开谁单独来讲都不能透切的了解x64的指令编码体系。
第一节 学会看 Opcode 表

  怎么去看指令的opcode,获得指令opcode编码,是很有学问的。

一、从指令参考页里看opcode 码

  可能许多人喜欢从指令参考页里查看opcode,下面的图是AMD文档中对于指令参考页的描述:
 指令参考页.jpg下载此附件需要消耗2Kx,下载中会自动扣除。
从指令参考页里可以得出以下信息:
(1)  指令助记符mnemonic
(2)  指令的Operand属性
(3)  指令的Opcode码
(4)  指令的描述。

  这确实可以得到想要的Opcode码,还是Operand数以其属性。下面摘录了mov指令一部分的参考页:

指令参考mov.jpg下载此附件需要消耗2Kx,下载中会自动扣除。
 

  从这里看出,这个Opcode 8B 有几种操作数形式,reg <- reg、reg <- mem,operand-size 可以是16/32/64。

  不过这并不是了解Opcode码的好地方,指令参考页主要是对指令的操作进行相应的描述。对掌握Opcode码不是那么直观和透彻。



二、掌握Opcode 表

  只有学会看Opcode表才是正道,Opcode表是一个全面的透彻的总结表,又可以说十分细致。Intel和AMD的文档中均提供了Opcode表,Opcode表有One-byte Opcode表、Two-byte Opcode表和X87 Opcode表等。

1、Opcode表上的基本元素


  Opcode表上描述的范围是00 ~ FF,即1个字节共256个值,每1值描述不同的属性,包括:
●  绝大部分代表1个Opcode码。
●  26、2E、36、3E、64、65、66、67、F0、F2、F3则是prefix。
●  OF指示2个字节的Opcode。
●  还有一部分是Group指令,由ModRM中的reg来决定。这部分还包括了x87 float指令的Opcode码。
  每个Opcode码还附有相应的Operand属性,Operand属性是用来描述Operand的,包括Operand寻址类型及Size。

看看mov指令8B Opcode 表是怎样的:
 
movopcode.jpg下载此附件需要消耗2Kx,下载中会自动扣除。

  上图圆圈所示是Opcode 8B,它对应的是mov的mnemonic,表格中的Gv, Ev是描述这个Op
code码所对应的指令的Operand属性。表示:
(1)  两个Operands分别是:目标操作数Gv,源操作数Ev
(2)  Gv表示:G是寄存器操作数,v是表示操作数大小依赖于当前代码的Default Operand-Size,也就是CS.D,可以是16位,32位以及64位。
(3)  Ev 表示:E 是寄存器或者内存操作数,具体要依赖于ModRM,操作数大小和Gv一致。

  4个字符便可以很直观的表示出:操作数的个数以及寻址方式,更重要的信息是这个Opcode的操作数需要ModRM进行解析。
  要看懂Opcode表必须学会分析和理解Operand属性字符,Intel和AMD的Opcode表前面都有对Operand属性字符很仔细清晰的定义和说明。

记住以下两点:
(1)  Operand属性都有两组字符来定义,前面的一组大写字母是Operand类型,后面一组小定字母是Operand 大小。
如:Gv 这里G是Operand类型,表示是General-Purpose Register(GPR)通用寄存器,也就是rax~r15共16个。这是有别与Segment Register、XMM寄存器等。v是Operand大小,这个Size是依赖于当前的Default Operand-Size。

(2)  操作数是直接编码在Opcode中,这些操作数是寄存器。这些寄存器的ID值已在Opcode中,而无需指出。Operand Size是依赖于当前Default Operand-Size。

以下举两个例子。
(1)  以典型的Jmp Jz为例,它的Opcode是E9,Operand属性是Jz,J是代表基于EIP的相对寻址,也就是说,操作数寻址是偏移量(Offset)加上EIP得出。z则表示Operand-Size是当前Default Operand-Size,这个Operand-Size是不能被override的,也就是说不能被加66h Prefix来调整Operand-Size。

(2)  另一个典型的例子是call Ev,它的Opcode是FF,这个Opcode是个典型的Group Opcode,为什么会定义为Group,下面的将会有阐述。操作数的寻址是典型的ModR
M寻址,更严格地讲是ModRM中的r/m寻址。E既可是GPR也可以是Mem。它同样是v属性的Operand-Size。


下面列举一些常见的Operand属性字符,详述请参考Intel或AMD手册。
(1)、Operand类型字符
E:GPR或Mem,依赖于ModRM中的r/m域
G:GPR具体ID依赖于ModRM中的reg域
I:Immediate直接在指令encode中
J:EIP相对寻址操作数,EIP+offset
O:绝对寻址和Immediate一样,直接在指令encode中


(2)、Operand大小字符
b:One-byte
d:four-byte(doubledword)
q:eight-byte(quadword)
v:16位、32位、64位依赖于Default Operand-Size,可被66h Prefix改写
z:16、32、64位依赖于Default Operand-Size,不可被66h改写

通过查Opcode表能迅速得出该指令的Opcode及Operand详情。


2、透析Opcode的编码规则
  prefix与Opcode共享00~FF的空间,由于Prefix部分是可选的,当CPU取指单元从ITLB加载指令L1-Icache和prefetch buffer,预解码单元通过prefix自已的ID值来解析prefix。如读入66h时是解析为prefix而不是Opcode。同样,读入0F时被解析为是2个字节的Opcode中的第1个字节。
  Opcode的operand寻址一部分是在Opcode码直接中指定,一部分是依赖ModRM给定,还有一部分不依赖ModRM给定。直接中Opcode指定的operand寻址的是GPR寻址,如:inc e
ax指令(Opcode是40h),还有串指令,如loads等。
  有2个Operands数的Opcode,有三种情况,一种是:一个Operand由Opcode指令指定,另一个operand却不依赖于ModRM。这种情况下,一个operand必定是GPR,另一个不依赖于ModRM的operand必定是Immediate和Displacement。看看以下两个Opcode:
  指令mov rax, Iv 它的Opcode是B8,目标操作数是由Opcode中指定的GPR(rax),源操作数不依赖于ModRM的Immediate,在这种情况下,这个Immediate可以不受4字节限制,它可以是64位的值,即:mov rax, 0x1122334455667788是完全正确的。
  又如指令mov rax,Ov 它的Opcode是A1,目标操作数由Opcode指定的rax寄存器,源操作数是不依赖于ModRM的displacement值,同样在这种情况下,这个displacement可以不受4字节限制,它可以是64位值,即:mov rax, qword ptr [0x1122334455667788] 是完全正确的。

  第二种是:两个操数都由ModRM提供寻址,这种情况下,这两个操作数要么两个都是寄存器寻址。要么是一个是寄存器,一个是内存寻址。那么,ModRM中reg提供具体的寄存器寻址,r/m提供另一个寄存器寻址或内存寻址。这种指令很常见,如:mov eax,ecx 它的操作
数由ModRM提供寻址。大多数编译器会将ecx由reg提供寻址,即:reg = 001。而eax则由r/m提供寻址,即:r/m = 000,这样,ModRM的值就是:11-001-000 也就是c8。所以:mov eax, ecx指令的机器编码是:89 c8

  第三种是:一个操作数由ModRM提供寻址,另一个操作数则不由ModRM提供寻址,那么这个Opcode必定是Group属性Opcode。因为ModRM的reg将会用来协同Opcode来确定最终Opcode。这种指令也很常见,如以下这条指令很常见:mov dword ptr [eax-0xc], 1 它是operand属性字符是:Ev,Iz 这是很典型的Group属性Opcode,目标操作数由ModRM的r/m提供寻址,源操作数则是立即数,这种情况的Immediate则要受到4字节的限制,若是:mov qdword ptr [rax- 0x0c], 0x1122334455667788 则是错误的。最终它的机器编码是:
C7 40 f4 01 00 00 00



  要深入掌握Opcode规则,必要记住上述2点:一是Opcode表的基本元素,学会分析Opera
nd属性字符,二是理解上面所讲的operand寻址模式。


三、x87指令、3Dnow指令、64-media and 128-media指令介绍

1、x87指令介绍
  Opcode范围从D8 ~ DF是x87 float指令Opcode,实际上x87指令的Opcode是2个字节的,D8~DF是主字节,ModRM是补充字节,协助主字节。也可以说它是Group属性的Opcode,但x87的ModRM比Group属性Opcode中的ModRM是更进一步协助关系。
  x87指令绝大部分是float寄存器(st0~st7)寻址的,实际是与mmx0~mmx7寄存器共用物理寄存器。
  在ModRM的mod=11模式下,r/m能提供8个Opcode,它们全是寄存器寻址,而在mod != 11 模式下,x87指令可以提供内存操作数寻址。

  像以下这条常见的x87 float指令:
(1)fstp st(1)   ;将st(0)值复制到st(1)到,并置stack顶为st(1)
它是寄存器寻址,ModRM中的mod = 11 提供寄存器寻址模式,reg = 011 提供fstp opcode,r/m = 001 提供寄存器ID,所以,这条指令的编码是:dd d9

(2)fstp dword ptr [eax]     ;将st(0)值复制到 [eax],并置stack顶为st(1)
它是内存寻址,ModRM的mod = 00 提供无disp内存寻址,reg = 011 提供fstp mem32 opcode,r/m = 00 提供 [eax] 内存寻址。所以,这条指令的编码是:d9 18


2、3DNow 指令
  AMD 设计的3Dnow别开生面,实际上3个Opcode的指令集,前2个Opcode是0F 0F,这两个Opcode是起引导为3Dnow的作用,第3个Opcode是主Opcode 但是这个Opcode却又不跟在第二个Opcode后面。实际,第3个Opcode改由Immediate来充当,这个Immediate值是固定为1个字节的。
  它是编码序列是:0F 0F  ModRM  SIB  displacement  Immediate
  AMD在最新的SSE5指令也沿用了这种模式,只是更进一步增加编码序列单元,这将在以后章节讲述。
  下面举1个例子来示范:指令pfcmpge mmx1, qword ptr [eax]
  这条指令的operand助记符是:Pq,Qq  实际上与 Gv, Ev 情况一样,只不过这里寄存器由GPR变为mmx寄存器,操作数却固定为64位。
  它的Opcode码是:90  也就是imme为90,由ModRM的reg提供P的寻址,r/m提供Q的寻址。故 mod = 00,reg = 001, r/m = 000
  所以,这条指令的编码是:0F 0F 08 90


3、64-media与128-media指令
  一部分指令是3个Opcode一部分是2个Opcode,而3个Opcode则是有一个prefix来充当。这是Intel设计的编码架构,若是AMD设计的编码则极大可能不同,AMD设计的风格是使用后面的Imme来充当第3个Opcode,而Intel则是使用prefix,这里可以看出AMD与Intel风格的不同。
这些指令使用66h、F2以及F3 prefix 作为第1个Opcode,0F作为第2个Opcode。
举1个例子:movntdq xmmword ptr [rax], xmm0
   这是一条复128位的指令,从xmm0复制到 [rax] 内存上,它采用66 0F前导,第3个Opcode是E7,ModRM提供寻址,mod = 00,reg = 000,r/m = 000
  所以,这条指令的编码是 66 0F E7 00 



四、  强悍的AMD SSE5指令集
  AMD设计3Dnow与SSE5指令集的目的很明显,想摆Intel的制约,反过来想引导Intel向他自已靠拢。特别是SSE5指令集。SSE5指令集的数量不少,有八九十个之多。实事上AMD推出的x86_64指令集就成功的制约了Intel,Intel不得不跟随AMD脚步。SSE5能不能再引导Intel向他靠还得拭目以待。

1、  AMD SSE5指令集编码的特点:
(1)  SSE5指令操作数可以增加到4个。这个4个操作数可以全是寄存器操作数,这是通过增加DREX字节来实现的。
(2)  SSE5指令编码有3个Opcode,其中2个是引导Opcode和1个主导Opcode。
(3)  SSE5指令编码中有3个字节来定位寻址操作数。在原有的ModRM和SIB的基础上,增加了Drex字节来寻址。
(4)  无需prefix以及REX prefix进行修饰。
(5)  目标操作数固定为寄存器


2、  SSE5指令编码序列,如下:

0F 24(或0F 25)+Opcode3+ModRM+SIB+DREX+Displacment+Immediate 

●  0F 24 和 0F 25 是引导Opcode,这两个Opcode在原来是无效。
●  Opcode3是主导Opcode,定性SSE5的操作
●  ModRM与SIB意义和原来一致
●  DREX意即:Dest+REX,DREX.dest是定义目标操作数,REX的含义和REX prefix一致。
●  Displacement含义和以前一致。
●  Immediate含义有些改动,在SSE5指令里只有1个字节大小。


3、Opcode3的结构
位       含义
7 ~ 3            Opcode码
2               Oc1,它与DREX的Oc0组合起来控制操作数
1 ~ 0            OPS,它定义操作数的大小


4、DREX的结构
位       含义
7 ~ 4            Dest域,即目标操作数的ID值
3               Oc0,与Opcode3的Oc1组合起来控制操作数
2               即REX.R
1               即REX.X
0               即 REX.B


5、控制操作数
  Opcode3.Oc1+DREX.Oc0 组合共2位值控制操作数分配
  对于4个操作数的指令来说,例:fmaddps dest, src1,src2,src3 其含义如下:

值             含义
00             dest = DREX.dest,src1 = DREX.dest,src2 =&
               src3 = ModRM.r/m


01             dest = DREX.dest, src1 = DREX.dest, src2 = ModRM.r/m 
               src3 =&

10             dest = DREX.dest,src1 =&,src2 = ModRM.r/m
               src3 = DREX.dest

11             dest = DREX.dest,src1 = ModRM.r/m,src2 =&
               src3 = DREX.dest

像:fmaddps xmm1, xmm2, xmmword ptr [rax], xmm1 这条指令的机器编码是:
0F 24 04 10 10

若:fmaddps xmm1,xmm2,xmmword ptr [rax+0x11223344], xmm1 
则是:0F 24 04 90 10 44 33 22 11






第二节 ModRM寻址



  本节将讲解另一个重点:ModRM寻址。这个ModRM寻址非常重要。是理解x86和x64平台上指令Operand的关键。
  记住,理解透彻是最关键。若真的不能理解,能背出也不错了。


一、ModRM的含义
  ModRM字节的组成部分为:mod-reg-r/m 三个部分,mod为2位,reg与r/m是3,组成2-3-3的比例。

1、mod:寻址模式。
  2位组成4种寻址模式,总的来说,只有两种寻址模式,就是:内存寻址模式和寄存器寻址模式。mod = 11时指出寄存器寻址模式,mod = 00 ~ 10 时指出内存寻址模式:
  mod = 00,定义 [register] 间接寻址,无displacement值。
  mod = 01,定义 [register + disp8],有8位displacemnet 偏移值。
  mod = 10,定义 [register + disp32],有32位displacement偏移值。

2、reg:寄存器ID值
  3位组成8个寄存器ID值,从 000 ~ 111,对应于 RAX、RCX、RDX、RBX、RSP、RBP、RSI以及RDI。这个ID值可以被REX prefix扩充为4位,范围从 0000 ~ 1111可表示16个寄存器。


reg域的另一含义是对Opcode的补充,对分为一组Opcode的进行选择(Group属性)。


3、r/m:意即register / memory。 提供对registers或memory的寻址,也用来表示寄存器ID,当是registers时是寄存器ID值。当是memory时是寄存器间接寻址中的寄存器ID值。当mod != 11 时,r/m 表示 [rax] ~ [rdi],REX prefix用来扩充寄存器ID值。

这里有2个设计上的问题:
(1) 如果像这条指令:mov eax, [eax+ecx*2+0x0c] 在这条指令里eax是base寄存器,ecx是Index寄存器,2是scale,还有一个displacement 
  这种内存寻址是base+index*scale+disp。这需要SIB字节来进行确定,那么ModRM必须要有一个手段来引出后续的SIB字节。在 [rax] ~ [rdi] 的范围里,Intel选择了原来应属于 [rsp] 的值用来引出SIB,一是因为 [rsp] 并不常用吧。二是因为 rsp 设计为 stack top指针,专用于stack top指针。
  原来属于 [rsp] 的领域对应的,r/m是100,这个领域被 [SIB] 替代了,事实上在16位机器原本是没有SIB字节的,base+index*scale+disp这种寻址是后来才增加的。16位的ModRM上是没有SIB引导域。

(2) 如果内存寻址中没有base和index,只有disp的话,如:mov ebx, [0x11223344],这种直接寻址方式,在设计上ModRM还必须为提供这个模式。
  Intel又作出修改,选择了原来属于 [rbp] 模式的领域提供给 [disp],选择 [rbp] 让给 [disp],是因为 rbp 原本意图就是设计为 stack基址指针。[rbp] 寻址一般都要加上一个偏移量,也就是基于stack frame指针的偏移量,即 [ebp + disp] 这种寻址模式在 mod = 01 或 mod = 10 中给出。

所以,在最终的mod =00上,r/m 寻址设计上,如下表格:
r/m                 内存寻址
000                 [rax]
001                 [rcx]
php指什么
010                 [rdx]
011                 [rbx]
100                 [SIB]   ---- 原本对应 [rsp]
101                 [disp]   ---- 原本对应 [rbp]
110                 [rsi]
111                 [rdi]

而在mod = 01及02上,r/m 的 101域上,设计为 [rbp + disp8] 及 [rbp + disp32],提供了对 [rbp+偏移量] 的支持,这样也符合原本设计的语义。


4、16位寻址下的ModRM
  在16位寻址下是不支持base+index*scale这种寻址模式的,这样在编码序列里就无需提供SIB字节了。但还是有限地支持基址+变址寻址模式,基址寄存器只有2个,就是bx 和bp。变址寄存器也只有2个,就是si和di。
  基于上述设计,基址+变址寻址的组合只有4个,也就是:[bx+si]、[bx+di]、[bp+si] 以及 [bp+di]。它们对应r/m域就是000 ~ 011。
那么这4个寄存器的间接寻址就是:[si]、[di]、[bp] 以及 [bx],对应于r/m的100 ~ 111,同样如上述,bp 是stack frame 指针,一般使用需加 disp(offset),所以 [bp] 让位给 [disp] 解决直接寻址的问题。

所以,16位的mod = 00 下,ModRM最终设计方案是:
r/m                 内存寻址
   000                 [bx+si]
001                 [bx+di]
010                 [bp+si]
011                 [bp+di]
100                 [si]
101                 [di]
110                 [disp]     ------ [bp] 让位给 [disp]
111                 [bx]

mod = 01、10 以及 11 的情形下如前如述。


5、64位寻址下的ModRM
  在64位下,ModRM的含义与32位一致,改进的只是在原来基础上增加了8个GPRs,通过REX prefix进行对新增的寄存器进行访问。
  寄存器的ID取值为:0000 ~ 1111。由REX.R以及REX.B位进行扩展访问。




二、结合Opcode来看寻址模式及Opcode的定位
  Opcode定义指令的执行码,用于执行什么操作,对于操作数寻址上,Opcode结合ModRM
来定义操作数,这是一个经过反复琢磨推敲的过程,而最终又影响到Opcode的定位。下面讲讲怎么影响到Opcode最终定位。

3、  一个操作数的Opcode定位
  操作数要么就是registers,要么就是memory,要么就是Immediate值。如果指令只有一个操作数。
(1)  如它是register的话,Opcode是无需ModRM配合确定寻址方式的,ModRM的寻址方式的定位是定位2个操作数寻址方式的这种模式。在只有1个寄存器操作数的情况下,ModRM无用武之处。所以,在这种情冲下,寄存器操作数绝大部分是嵌在Opcode里面,它由Opcode的寄存器域指出。如常见的inc ecx、dec ecx、push eax等。那么另一部分肯定是Group属性。若是Group属性,则ModRM有用武之地了。
(2)  如它是Immediate的话,它绝对是无ModRM。直接将Immediate值嵌入指令编码里。
(3)  如它是memory的话,它绝对是Group属性,需要ModRM的reg来配合定位。那为什么不能是直接offset呢,直接offset寻址留给最常用的,最有用的Opcode,以免Opcode占位,浪费资源。


4、  两个操作数的Opcode定位
  两个操作数大部分都需 ModRM 配合定位寻址。ModRM提供的2个操作数寻址大有用武之地。

(1)2个操作数中,其中1个是寄存器的这种情况最直接简单,由ModRM的reg及r/m提供寻址。若其中1个是GPRs的情形下更简单,GPRs则直接由Opcode提供寻址。
(2)2个操作数中,没有寄存器的情形下,也就是要么是memory,要么是Immediate,它必然是个Group属性,reg域提供Opcode的定位,r/m提供内存寻址。Immediate 直接嵌入指令编码中。

5、  三个操作数的Opcode定位
  三个操作数中有一个必定是Immediate,在AMD的SSE5 指令集推出之前,x86平台是无法提供第3个非Immediate操作数的定位。直至AMD 的SSE5通过增加另一个描述操作数的字节来寻址第3个操作数。





第三节 SIB 寻址
 

   在ModRM 无法提供更多内存寻址方式时,使用SIB进行协助寻址。对内存操作数寻址提供补充定义。使用SIB进行寻址的完整模式如:[rax + ecx * 8 + 0x11223344]。
  此时,SIB字节是:scale = 11,index = 001,base = 000,组合起来是 c8


一、SIB的含义及结构
  SIB意即:Scale – Index – Base,用来定义base+index*scale+disp这种寻址模式。同样按
2-3-3比例组合。
scale 索引因子的含义:
值             含义
00             无 scale,或者:scale * 1
01       按 index * 2 比例
10             按 index * 4 比例
11       按 index * 8 比例

index 域指出index寄存器的ID值,范围从 000 ~ 111。base 域指出base寄存器的ID 值,从 000 ~ 111。Index与base经过 REX prefix可以扩展为0000 ~ 1111。


二、对ModRM的补充定义
  前面提到ModRM中,ModRM.r/m = 100时,[esp] 让位给 [SIB] 提供引导 SIB 字节。这种情况下,指令使用了base+index的寻址方式。
   SIB提供了从 [base+index] 到 [base+index*8] 寻址范围,那么这里延续上一节提到的ModRM设计上的问题:[esp] 这种寻址方式将怎么解决呢?在ModRM中抛弃了 [esp] 的寻址模式,在 SIB中将得到补救。
在index = 100 时,按规则,应该是 [esp + base] 这种寻址模式吧?由于esp的特殊性,esp寄存器只能做base寄存器,以esp为基址。而不能作为index寄存器。所以:[esp+base] 这种寻址模式中 esp 被去除,那么将剩余 [base]。
故当 index = 100时,[esp+base]具体形式将取决于 base。

1、对 [esp] 寻址的补救方式
  由于ModRM中没提供 [esp] 寻址,而又确实需要 [esp] 寻址的话,在 SIB 中通过 index = 100时提供,[esp+base] 中去除掉esp,留下 [base] 寻址,所以在 base = 100时就提供了对 [esp] 的寻址。

2、重复编码
  在index = 100,去除esp后,变为 [base] 寻址,那么此时又提供了[base]的寻址方式,这样
就与d = 00 时提供了完全相同的寻址方式,所不同的是一种由d = 00直接给出,另一种通过ModRM./rm = 100然后转接到SIB,再由SIB.index = 100时SIB.base提供。
  Intel设计的时候已不能顾及这么多了,反正重码也没什么多大的害处,只是麻烦了processor的解码单元而已,但这样就显不得够严谨,话说回来,本来x86平台指令集就不是十分严谨。

3、那么当 index = 100,也就是 [base] 寻址,而 base = 101时,按规则此时应该为 [ebp] 寻址。Intel 又变变花样,在 [ebp] 时而又不是 [ebp]。
  前面提到,ModRM,当r/m = 100,也就是让位给 [SIB] 的原来属于 [esp]的地方,r/m = 101提供的是 [ebp] 寻址,当r/m = 100,而SIB.base = 101时,若还是 [ebp],就重复了。
  选择在重复 [ebp] 时改变,可能还是基于 [ebp+disp] 这个stack frame指针+偏移量用法的考虑吧。
此时 [ebp] 而不是 [ebp] 随着d而改变。d = 00时,index = 100,bas
e = 101,此时会改变 [disp32] 寻址,这和d = 00,ModRM.r/m = 101又完全一样了,提供的是 [disp32] 的寻址。
当d = 01和10时,index = 100,base = 101,提供的是 [ebp+disp8] 和 [ebp+disp32] 寻址。而这又和d = 01和10时,ModRM.r/m = 101 提供了完全相同的编码。
  换句话来说:Intel完全没有提供 [ebp] 这种寻址方式,这又验证了Intel的设计构思,就是:ebp是stack frame 指针,因此使用ebp作为基址的话,则要加上一个偏移量。[ebp+disp]这种寻址正好符合Intel的这种理念。
那么,要使用 [ebp] 这种寻址,只好变通一点:采用 [ebp + 0x0] 这种形式了。


三、总结一下SIB寻址

1、重码问题
x86指令的内存寻址有三个地方的重码现象:


(1)、[disp32] 寻址方式的重码现象
●  d = 00,ModRM.r/m = 101 提供了 [disp32] 寻址。
●  d = 00,ModRM.r/m = 100 
然后 SIB.index = 100,SIB.base = 101 提供了 [disp32] 重码寻址。

(2)、[base] 寻址方式的重码现象
●&d = 00 提供了 [base] 寻址,也就是 [eax] 之类。
●&d != 11 ModRM.r/m = 100 
然后 SIB.index = 100提供了 [base] 寻址,导致重码。

(3)、[ebp+disp8] 与 [ebp+disp32] 的重码现象
●&d = 01和10,ModRM.r/m = 101,提供了相应的 [ebp+disp8] 和 [ebp+disp32] 寻址。

●  d 01和10,ModRM.r/m = 100,
SIB.index = 100,SIB.base = 101,提供了相应的 [ebp+disp8] 和 [ebp+disp32]

2、不支持esp做为index寄存器寻址的问题
  因esp设计为stack top指针,而不支持 [esp+base] 这种寻址方式。 esp只能作为base 寻址,即:[esp]。 故 [esp+base] 被去除esp,只剩下 [base],而引发重码问题。

3、  不支持 [ebp] 寻址方式,须作为stack frame pointer 加偏移量的寻址方式。这符合设计语义。程序中使用 [ebp] 则要变为:[ebp+disp8] 或者 [ebp + disp32] 这种方式。



  x86 和 x64 平台的指令编码核心部分:Opcode、ModRM以及SIB 讲解到此为止。
  后续章节将探讨displacement和immediate话题。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。