在 GPI(General-Purpose Instruction)指令里,Legacy Prefix 是在整个编码序列的最前部分,起了对内存操作数进行修饰补充作用。
legacy prefix 主要有以下作用:
leagcy prefix 包括下面几类:
要彻底了解 prefix,必须要结合 3 个很重要的上下文环境:
在本节内容里将会根据上面三个环境进行讲解。
这里操作数是内存操作数。出现调整的情形,这是因为:
也就是说:指令编码因指令操作数的 operand size, address size 以及 segment 的不同而不同
在 x86/x64 平台的指令系统里有两个很重要的概念:
包括:缺省操作数大小以及缺省地址大小(default operand-size 与 default address-size)。
这里,我们说的“32 位”或者“16 位”的概念指的是:default operand size 是 32 位,或者 16 位。
      在 64 位下,情况有些特殊:
  | 
    
指令的 default operand size 是由 CS.D 标志位来决定,这个 default operand size 可由 CS.D 标志位来改变:
缺省的 operand size 和 address size 可由 66H prefix 及 67H prefix 进行改写,经过改写后的 operand size 和 address size 是指令最终的 operand size 和 address size
包括:有效的操作数大小以及有效地址大小(effective operand-size 与 effective address-size)
模式  | 
    effective operand size   | 
    effective address size   | 
  
实模式,保护模式,v8086 模式以及compatibility 模式  | 
    16, 32   | 
    16, 32   | 
  
64-bit 模式  | 
    16, 32, 64   | 
    32, 64   | 
  
在 64 位模式下,情况又有些特殊:在 64 位下支持 16 位,32 位以及 64 位的操作数大小。但是不支持 16 位的地址大小。
关于“default 与 effective”的更详细的论述,请见:default(缺省)与 effective(有效)一文
| 
       是指:  | 
    
基于这种需求,在指令编码中使用 66H prefix 来实现 operand size override ,例如:
| 
 bits 16 ; 16 位代码 mov eax, ebx 
 mov ax, bx  | 
    
上面的代码两种情况下都需要使用 66H prefix 进行改写 operand size
66H 字节这个 prefix 用来更改 operands size,当然只能在指令所支持的 effective operand-size 范围内进行调整。
66H 在 Opcode 表中就是一个 prefix,处理器在解码时识别它不是 Opcode
怎样进行 Override 以及 Override 什么? 都是有固定的规则的,这和 default operand size 以及 effective operand size 有紧密的关系
模式  | 
    default operand size   | 
    effective operand size   | 
    prefix  | 
    REX prefix  | 
    描述  | 
  
实模式,保护模式,virtual-8086 模式,compatibility 模式  | 
    16  | 
    16  | 
    ---  | 
    ---  | 
    2 种 default operand size 下的情形 | 
32  | 
    66H  | 
  ||||
32  | 
    16  | 
    66H  | 
  |||
32  | 
    ---  | 
  ||||
64-bit 模式  | 
    32  | 
    16  | 
    66H  | 
    ---  | 
    64-bit 模式下的 2 种 default operand size 情形 | 
32  | 
    ---  | 
    ---  | 
  |||
64  | 
    ---  | 
    REX.W = 1   | 
  |||
*  64  | 
    16  | 
    66H  | 
    ---  | 
  ||
64  | 
    ---  | 
  
在 64-bit 模式下大多数指令 default operand size 是 32 位的,因此,可以有 3 种 effective operand size
表中:--- 表示无需 prefix,REX.W = 1 表示:调整到 64 位(REX.W = 0 它是使用 default operand size)
* 标注处的 default operand size = 64 只有少数的指令 default operand size 是 64 位,大部分指令的 default 是 32 位的。
在 64 位的 default operand size 下,effective operand size 只有 2 种:
因此:只能使用 66H prefix 改写为 16 位的操作数,不能改写为 32 位操作数。
原因前面已经提过,很简单:当 16 位代码下需要访问 32 位数据或者 32 位代码需要访问 16 位数据时。
| 
 bits 16 ; 16 位代码 mov bx, cx                 ; 不需要进行 operand size override  | 
    
上面代码所示,在 16 位模式下,上面的不需要 override,而下面一条指令则需要进行 override
由于在 16 位下,如果操作数的大小缺省是 16 位的,这条指令要访问 32 位的寄存器,那么需要使用 66H prefix 进行 operand size override
89 d8 ---> 66 89 d8 (使用 66H prefix 将 16 位 registers 改为 32 位 registers)
由于在 32 位下,如果操作数的大小缺省是 32 位的,这条指令要访问 16 位寄存器,同样需要使用 66H prefix 进行调
89 d8 ---> 66 89 d8 (使用 66H prefix 将 32 位 registers 改为 16 位 registers)
表 4.5.1 (在实模式,保护模式,v8086 模式以及 compatibility 模式下适用)
指令  | 
    default operand size  | 
    生成的指令编码  | 
  
mov eax, ebx   | 
    16  | 
    66 89 d8   | 
  
32  | 
    89 d8  | 
  |
mov ax, bx   | 
    16  | 
    89 d8  | 
  
32  | 
    66 89 d8   | 
  
上表是这 2 条指令分别在不同的 default operand size 下的指令编码情况
有些人或许会感到疑惑,为什么例 1   与例 2 编译器生成的结果是一样的。这就是 assembler 在不同的 编译上下文环境 译为相同的指令编码。
  
这条指令有两种译法:
使用 66H prefix 进行 operand size override
第 1 条 encode 中,a1 是 opcode,44332211 是 offset 值(我认为不属于 dispalcement,因为不是 ModRM 寻址提供的), 66 改变了缺省的操作数大小,将 32 位调整为 16 位。
第 2 条 encode 的 opcode 是 8b, ModRm 是 05, 而 44332211 是 dispalcement(需要 ModRM 寻址)。
同样一样指令,但目的操作数大小不同,并且 assembler 编译上下文环境不同。
它两种译法为:
与例 3 所不同的是:这条指令增加了 67H prefix 来进行 address size override,使得可以保住 32 位的地址值
注意:在 16 位的 default operand size 下,缺省的 address size 也是 16 位的。但是,在上面的编码里,我们认为编译器没有实行截断地址值!
当编译器对 32 位的地址值进行截断时,可以有下表的指令编码示例。
表 4.5.2
指令  | 
    default operand size  | 
    生成的指令编码  | 
    备注  | 
  
mov ax, [11223344h]   | 
    16  | 
    a1 44 33   | 
    地址值被截断为 16 位  | 
  
32  | 
    66 a1 44 33 22 11  | 
    使用 66H 改写为 16 位操作数  | 
  |
mov eax, [11223344h]   | 
    16  | 
    66 a1 44 33  | 
    使用 66H,以及地址被截断为16位  | 
  
32  | 
    a1 44 33 22 11  | 
    正常  | 
  
这里必须有一点要认识到的:当在 16 模式下, 地址 [11223344h] 多数编译器会它截断只取低 16 位地址
那么:mov ax, [11223344h] 会被编译为 a1 44 33 (它不需 67H prefix 进行 address size override)
在一个汇编语言源文件里,需要给编译器一些编译指示:指示目标代码将生成是多少的?目标平台是什么?文件格式是什么?等等...
这个就编译上下文环境。
例如:
操作系统的引导初始化代码部分是 16 位的,现在绝大多数 OS 是 32 位的,因此,在当前系统下写引导代码,则需要求编译器编译为 16 位实模式代码。因此,你不得不写 16 位代码,编译器根据情况将 32 位操作和地址调整至 16 操作数和地址。但在大部分情况下,不需要作调整,直接生成 16 位代码即可。
以 nasm 编译器为例,下面给出一些代码片断:
| 
 ; ********************************************************* %include "include\arch\x64.inc" ; mouseOS 0.02 project 
    org BOOT_SEG                   ; for int 19 ; A20 gate enable   
 ; How do ?         cli 
         db 0x66                         ; adjust to 32-bit operand size   ; third: enable proected mode 
 jmp dword code32_sel:code32_entry   ; Now: entry 32bit protected mode, but paging is disable code32_entry:  mov di, 0  | 
        
上面代码片断显示:在一个源文件中,使用 bits 16 指示 nasm 生成 16 位的代码,并且使用 bits 32 指示 nasm 生成 32 位代码。 还可以使用 bits 64 来生成 64 位代码。
上面代码片断中,使用 bits 伪指令来指示 nasm 生成何种代码。
注意:
| 
       nasm 的职责是根据给它下达的 bits 命令进行相应的编译,但不管生成的代码需要放在哪里运行!  | 
    
如下例子:
| 
 bits 16     mov eax, ebx  | 
    
代码中使用 bits 16 指示生成 16 位机器编码。至于把它放在哪里(16位还是32位下)运行是程序员的职责。
| 
       00000000  6689D8            mov eax,ebx  | 
    
在第 2 条指令里,使用了 DWORD 指示字强调地址为 32 位。因此,编译器生成了 66H prefix 和 67H prefix 进行 override
processor 处于什么模式下,这是系统程序员需要考虑的问题,从而通过代码体现出来,编译器根据代码生成相应的代码。
| 
       也就是说:  | 
    
下面的表格所列:当前的 default operand size 是多少时,处理器将 69 8d 这条机器编码解析为什么指令:
机器指令  | 
      CS.D = 0 时  | 
      CS.D = 1 时  | 
	  CS.L = 1 并且 CS.D = 0 时  | 
    
69 8d   | 
      mov ax, bx   | 
      mov eax, ebx   | 
      mov eax, ebx   | 	  
    
当需要改变地址大小的时候,也需要使用 67H prefix 来进行调整。同样是在所支持的 effective address-size 范围内。
是指:  | 
  
指令中可以使用 67H 进行 address size override,同样 67H 不是 opcode,处理器在的解码时认为它是 prefix。
怎样进行 Override 以及 Override 什么? 都是有固定的规则的,这和 default address size 以及 effective address size 有紧密的关系
处理器模式  | 
    default address size   | 
    effective address size   | 
    prefix  | 
  
实模式,保护模式,virtual-8086 模式,compatibility 模式  | 
    16  | 
    16  | 
    ---  | 
  
32  | 
    67H  | 
  ||
32  | 
    16  | 
    67H  | 
  |
32  | 
    ---  | 
  ||
64-bit 模式  | 
    64  | 
    32  | 
    67H  | 
  
64  | 
    ---  | 
  
与 Operand size override 规则一样,在 effective address size 范围里调整为另一个 address size 需要使用 67H prefix
在 64 位模式下 default address size 是 64 位,不能调整到 16 位地址。
以 16 位模式为例,如果需要访问 64K 以上的地址,则需要使用 67H 将 16 位寻址模式改写为 32 位的寻址模式。
由于在 16 位的 default operands size 和 default address size 下,但该指令使用 32 位 operand size 以及 32 位 address size
也就是说既要改写 default operand size 也要改写 default address size。所以,应加上 66H prefix 改写 operand-size,再加上 67H prefix 改写 address-size。
最终的   encode 为:  66 67 c7 84 c8 44 33 22 11 78 56 34   12
该指令的编码为: a1 44 33 22 11
------------------------------------
当我们进行手工改写时,加上 67H prefix 变为: 67 a1 44 33 22 11
那么此时,用 67H prefix 调整为 16 位地址,那么在汇编语句将变为: mov eax, dword ptr [3344]
结果是: 加了 67H prefix 之后,它的地址将被截断为 16 位。即地址:0x3344,多出 22 11 两个字节属下条指令边界了。
很显然,这条汇编语句源操作数的地址是 16 位的。
由于 default address size 为 32 位,因此,需使用 67H prefix 将这个 32 位寻址模式调整 16 位寻址模式。
最终的 encodes 是: 67 8b 40 0c 
40H 这个 ModRM 字节对应的 16 位寻址是: [bx + si + 0x0c]
----------------------------------
假如,将 67 8b 40 0c 这个机器码放在 16 位环境下执行,指令则变为: mov eax, dword ptr [eax + 0x0c]
这是由于使用了 67H 将 16 位的 default address size 改写 32 位的 address size 所产生的结果!
上面的 3 个例子显示了 16 位地址和 32 位地址的区别,主要来自 16 位的内存操作数寻址只支持 BX 与 BP 寄存器作为基址寄存器,SI 和 DI 寄存器作为变址寄存器,比 32 位的内存寻址少得多。
表7.5.1 assembler 在 32 位下和 16 位下的区别(assembler 编译上下文环境)
 序号  | 
    指令  | 
    在 bits 32 下编译的结果  | 
    在 bits 16 下编译的结果  | 
  
| 1 | mov dword [eax + ecx * 8 + 0x11223344], 0x12345678 | c7 84 c8 44 33 22 11 78 56 34 12 | 66 67 c7 84 c8 44 33 22 11 78 56 34 12 | 
| 2* | mov eax, dword [0x11223344] | a1 44 33 22 11 | 66 a1 44 33 | 
| 3 | mov eax, dword [bx + si + 0x0c] | 67 8b 40 0c | 66 8b 40 0c | 
表1中显示的是在不同的编译上下文环境,同一条指令产生的不同编码(例如,在 nasm 编译器使用 bits 16 和 bits 32 指示字)
注意:
在第 2 条时,地址 [0x11223344] 在 16 位代码的编译环境中,不同的编译器可能会有不同的处理结果:
★ 大多数 assembler(编译器)会将 [0x11223344] 截断为 [0x3344]。
★ 但是,一个功能强大的,全面的 assembler 应该将 [0x11223344] 还原为 [0x11223344],产生的编码应是: 66 67 a1 44 33 22 11
有关 16 位寻址模式和 32 位寻址模式,详细请参看 AMD 与 Intel 手册
当插入 67H prefix(address-size override)时,它根据处理器当前的 default operand/adderss size,内存寻址上产生相应的转变:
★ 当处理器运行在 16 位的 default address size 下,指示:将要使用 32 位地址。因此,内存寻址要用 32 位寻址模式。
★ 当处理器运行在 32 位的 default address size 下,指示:将要使用 16 位地址。因此,内存寻址要用 16 位寻址模式。
表2:处理器在 16 与 32 位的 default address size 下解析区别 (处理器执行上下文环境)
 序号  | 
    指令编码 encods(机器指令)  | 
    default operand/address size = 32  | 
    default operand/address size = 16  | 
  
| 1* | c7 84 c8 44 33 22 11 78 56 34 12 | mov dword ptr [eax + ecx * 8 + 0x11223344], 0x12345678 | mov word ptr [si + 0x44c8], 0x2233 | 
| 2* | 66 67 c7 84 c8 44 33 22 11 78 56 34 12 | mov word ptr [si + 0x44c8], 0x2233 | mov dword ptr [eax + ecx * 8 + 0x11223344], 0x12345678 | 
| 3 | 67 8b 40 0c | mov eax, dword ptr [bx + si + 0x0c] | mov ax, word ptr [eax + 0x0c] | 
| 4 | 8b 40 0c | mov eax, dword ptr [eax + 0x0c] | mov ax, word ptr [bx + si + 0x0c] | 
表2中显示在不同的执行上下文环境,同一条机器指令编码产生的不同行为。
注意:
★ 第1条中,机器码:c7 84 c8 44 33 22 11 78 56 34 12 当处理器在 16 位 default operand/address size 下,只解析前面的 c7 84 c8 44 33 22
剩下的 11 78 56 34 12 将被视为下一条指令。
★ 第2条中,机器码:66 67 c7 84 c8 44 33 22 11 78 56 34 12 当处理器在 32 位 default operand/address size 下,只解析前面的 66 67 c7 84 c8 44 33 22
剩下的 11 78 56 34 12 将被视为下一条指令。
在汇编代码层面上,assembler 根据当前编译环境,将汇编语句生成相应的 encodes 决定是否使用 67H prefix
在机器代码层面上,processor 根据当前执行环境来决定如何解析机器指令
与 operand-size / address-size 一样,当需要调整缺省的 segment 时,需要使用相应的 segment override prefix
与 default opernads-size、default address-size 一样,segment registers 同样有 default segment register
default segment register 与内存操作数中的 base register 相关。
例如:mov eax, dword ptr [eax] 指令中的 [eax] 操作数 eax 就是 base register
缺省寄存器规则:
| 
       foo:  | 
    
[ebp-0xc]:这个内存操作数缺省是基于 SS 段的
[eax]:这个内存操作数缺省是基于 DS 段的。
因此,对于上面的片段,[ebp - 0x0c] 是在 SS segment,即 stack 内。
[eax] 这个内存地址按照程序的意图是访问 stack 内的数据,所以,这里我将它调整为 stack segment
lea eax, [ebp - 0x0c]为什么一般程序都不会这么写呢? 那是因为,现代的操作系统都是采用平坦的内存模式,即:CS=SS=DS=ES,所以对 [eax] 这个操作数不需调整其结果是正确的。
产生的编码是: 36 8b 00
其中,36 也就是 SS segment-override prefix,将 DS 段调整为 SS 段。
当需要进行调整段寄存器时,就使用以上的 segment-override prefix。
在 64 位模式下,segmentation 管理已经被最大程度上的弱化,因此,当代码中进行 segment override 时,已经显得不重要了,可以有两个分段管理被保留下来:
它们被保留下来,令到程序中可以有额外的段式管理手段,因此,在代码中依然可以使用 fs 与 gs 进行 segment override 操作
这些 prefix 对 Opcode 进行补充,增强指令的功能,优化指令执行。起重复执行指令的功能
看下面这段 c 代码:
| 
 char *move_char(char *d, char *s, unsigned count)     while (count--)      return p;  | 
    
这是典型的、经典的字符串复制c代码,对应以下类似的汇编代码:
最初版本:
| 
 move_char: move_loop:  | 
    
上面的代码性能低下,是很死板的实现,优化的空间巨大。
x86 为串提供了相应的串操作指令(ins,outs,lods,stos,scas,cmps),对这些串指令提供 prefix 来增强优化这些指令。
可以看到 F3H prefix 有两重意义:rep 和 repz,但是使用的范围是不同的:
prefix 含义   | 
    使用范围  | 
    结束条件  | 
  
rep  | 
    movs,lods,stos,ins,outs   | 
    ecx = 0  | 
  
repz/repe  | 
    scas,cmps  | 
    ecx = 0 或 ZF = 0(比较结果不为零)   | 
  
它们的使用范围和结束条件都不同。
rep 重复执行指令一定的次数,这个次数在 ecx 中提供。
用伪代码描述为:
| 
       if (ecx != 0) {  | 
    
首先判断 ecx 是否为 0,不为 0 则执行指令。
使用 rep 优化版本:
| 
       mov_char:  | 
    
使用串指令 movsb 配合 rep prefix 进行复制,rep movsb 的编码为:
F3 prefix 另一层意义是 repe/repz,用于改变标志位的串操作:scas, cmps 指令
意思是:当比较结果相等(ZF=1)并且循环次数(ecx)不为 0 时进行重复操作。(重复的条件是:ZF = 1 & ecx <> 0)
即:它的结束条件是:ecx = 0 或者 ZF = 0, 意思是:不相等时或者次数到了,就不重复执行指令
它的 c 伪码形式如下:
if (ecx != 0 && ZF = 1) {  | 
  
常见运用一些跳过字符的逻辑上,如下面 C 代码,用于截除串前面空格:
| 
 char *trim(char *s)      return s;  | 
    
rep 与 repe/repz 是相同的 prefix,作用于不同的串指操作意义也不同:
当作用于不修改标志位的串指令时,它的意义是 rep,作用于修改标志位的串指令时,它的意义是 repz/repe
F2H prefix 是表达 repne/repnz 意思是: 结果不相等(不为零)时循环。(重复条件是 ZF == 0 并且 ecx <> 0)
结束条件是:ecx = 0 或者 ZF = 1 即:结果相等时退出循环。
同样也是用于改变标志位的串操作 scas 和 cmps
它的 c 伪码形式如下:
if (ecx != 0 && ZF = 0) {  | 
  
常见一些查找字符的逻辑上,如下面 C 代码:
| 
 char *get_char(char *s, char c)     ret  | 
    
  
  10 附加功能(LOCK prefix)
  
  
对于写内存的一些指令增加了锁地址总线的功能,这些写内存的指令如常见的   sub,add 等指令,通过 Lock prefix 来实现这功能,使用 Lock prefix 将会使 procesor 产生 LOCK#   信号锁地址总线
注意:
   Lock prefix 仅使用在一些对内存进行 read-modify-write   操作的指令上,如:add, sub, and 等指令。 否则,将会产生 #UD (无效操作码)   异常
如下指令所示:
| lock add dword ptr [eax], 1 | 
它的指令编码是:
   F0: Lock prefix   锁地址总线。
版权所有 mik 2008 - 2014