一 : 序

在开篇讲述学习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 值或者说是displacement 值。


所以,这个机器码最终被译为: 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节  认识编码序列

 

如上图所示:
这个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],此时是不需要调整缺省段寄存器