在 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