legacy prefix 主要有以下作用:
调整内存操作数的属性 增强指令的功能 提供额外的作用
(1) operand size override prefix:66H --- 改变操作数大小
(2) address size override preifx:67H --- 改变操作数地址模式
(3) segment override prefix:改变 memory 操作数段选择子,包括:
2E --- CS register 3E --- DS register 26 --- ES register 64 --- FS register 65 --- GS register 36 --- SS register(4) rep/repz prefix:F3H --- 串指字重复执行
(5) repnz prefix:F2H --- 串指字重复执行
(6) lock prefix: F0H --- LOCK
★ 改变指令操作数的 default operand size(可以使用 64 位 operands),同样是起 operand size override 的作用
★ 访问 x64 体系所有 16 个 registers:rax ~ r15,xmm0 ~ xmm15
要彻底了解 prefix,必须要结合 3 个很重要的上下文环境:
指令本身的 default operand-size 和 default address-size 以及 effective operand size 和 effective address size assembler 编译器上文环境 当前 processor 的执行上下文环境。x86/x64 指令编码会根据上面提到的 3 个上下文环境而对操作数的位置、大小以及地址进行调整改变。
这里操作数是内存操作数。出现调整的情形,这是因为:
effective operands-size 可以为:16 位、32 位以及 64 位 operands 的地址位置因段选择子的不同而不同(cs、ds、es、ss、fs 以及 gs)。 effective address-size 可以为:16 位、32 位以及 64 位也就是说:指令编码因指令操作数的 operand size, address size 以及 segment 的不同而不同
在 x86/x64 平台的指令系统里有两个很重要的概念:
★ 缺省(default)概念
包括:缺省操作数大小以及缺省地址大小(default operand-size 与 default address-size)。
在 32 位下:default operands-size 和 default address-size 都是 32 位。 在 16 位下:default operands-size 和 default address-size 都是 16 位。 在 64 位下,情况有些特殊: default address-size 是 64 位的 default operands-size 在大多情况下是 32 位的,而一些情况下是 64 位的在 64 位下 default operands-size 是 32 位,是基于 x86 平滑扩展为 x64 的设计理念。
★ 有效(effective)概念
包括:有效的操作数大小以及有效地址大小(effective operand-size 与 effective address-size)
在 32 位下:effective operand size 与 effective address-size 可以为:16 位和 32 位 在 16 位下:effective operand size 与 effective address-size 可以为:16 位和 32 位。 在 64 位下:effective operand size 可以为:16 位、32 位和 64 位。 在 64 位下:effective address-size 可以为:32 位与 64 位。 模式 effective operand size effective address size 备注 16 位 16, 32 16, 32 32 位 16, 32 16, 32 64 位 16, 32, 64 32, 64在 64 位模式下,情况又有些特殊:在 64 位下支持 16 位,32 位以及 64 位的操作数大小。但是不支持 16 位的地址大小。
关于“default 与 effective”的更详细的论述,请见:default(缺省)与 effective(有效)一文
是指: 在可接受的 effective operand size(有效操作数大小)范围内改变其 operands size(操作数的大小),使它的 operand size 不再是 default operands size
基于这种需求,在指令编码中使用 66H prefix 来实现 operand size override
66H 字节这个 prefix 用来更改 operands size,当然只能在指令所支持的 effective operand-size 范围内进行调整。
66H 在 Opcode 表中就是一个 prefix,它不是 Opcode
怎样进行 Override 以及 Override 什么? 都是有固定的规则的,这和 default operand size 以及 effective operand size 有紧密的关系
表 4.3.1
模式 default operand size effective operand size prefix REX prefix 描述 16 模式 16 16 --- --- 16 位模式下的 2 种 default operand size 的情形 32 66H 32 16 66H 32 --- 32 模式 16 16 --- --- 32 位模式下的 2 种 default operand size 的情形 32 66H 32 16 66H 32 --- 64 模式 32 16 66H --- 64 位模式下的 2 种 default operand size 情形 32 --- --- 64 --- REX.W = 1 * 64 16 66H --- 64 ---每一种模式下都分为 2 种 default operand size 情形,除了 64 位模式下 default operand size 是 32 时,有 3 种 effective operand size 外,其它都是 2 种 effective operand size
表中:--- 表示无需 prefix,REX.W = 1 表示:调整到 64 位(REX.W = 0 它是使用 default operand size)
* 标注处的 default operand size = 64 只有少数的指令 default operand size 是 64 位,大部分指令的 default 是 32 位的。
在 16 位模式下 当 default operand size 是 16 位时:需要调整为 32 位,需要加 66H prefix 当 default operand size 是 32 位时:需要调整为 16 位,需要加 66H prefix 在 32 位模式下 当 default operand size 是 16 位时:需要调整为 32 位,需要加 66H prefix 当 default operand size 是 32 位时:需要调整为 16 位,需要加 66H prefix 在 64 位模式下 当 default operand size 是 32 位时:需要调整为 16 位时,需要加 66H prefix。需要调整到 64 位时,需要加 REX prefix 当 default operand size 是 64 位时: 不能调整到 32 位,调整到 16 位时,需要 66H prefix在 64 位的 default operand size 下,effective 只有 2 种:16 位和 64 位。因此:只能使用 66H prefix 调整到 16 位,不能调整到 32 位
原因很简单:16 位代码下需要访问 32 位数据或者 32 位代码需要访问 16 位数据。
由于在 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 (在 16 位和 32 位模式下适用)
指令 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 在不同的 编译上下文环境 译为相同的指令编码。
在 Microsoft 的语法里,在内存操作数前一般要加指示字 word ptr,指明操作数的大小:mov ax, word ptr [11223344h] 实际上,在这条指令里,这个指示字不是必须的,加指示字只是比较直观。但有些情况是必须要加的,如:mov dword ptr [11223344h], 1
这条指令有两种译法:
66 a1 44 33 22 11 66 8b 05 44 33 22 11使用 66H prefix 进行 operand size override
第 1 条 encode 中,a1 是 opcode,44332211 是 immediate(而不是 dispalcement,因为不是 ModRM 寻址提供的), 66 改变了缺省的操作数大小,将 32 位调整为 16 位。
第 2 条 encode 的 opcode 是 8b, ModRm 是 05, 而 44332211 是 dispalcement 而不是 immediate(需要 ModRM 寻址)。
同样一样指令,但目的操作数大小不同,并且 assembler 编译上下文环境不同。
它两种译法为:
66 67 a1 44 33 22 11 66 67 8b 05 44 33 22 11与例 3 所不同的是:这条指令增加了 67H prefix 来进行 address size override
表 4.5.2
指令 default operand size 指令编码 mov ax, [11223344h] 16 66 67 a1 44 33 22 11 32 66 a1 44 33 22 11 mov eax, [11223344h] 16 66 67 a1 44 33 22 11 32 a1 44 33 22 11这里必须有一点要认识到的:当在 16 模式下, 地址 [11223344h] 多数编译器会它截断只取低 16 位地址
那么:mov ax, [11223344h] 会被编译为 66 a1 44 33 (它不需 67H prefix 进行 address size override)
在一个汇编语言源文件里,需要给编译器一些编译指示:指示目标代码将生成是多少的?目标平台是什么?文件格式是什么?等等...
这个就编译上下文环境。
例如:操作系统的引导初始化代码部分是 16 位的,现在绝大多数 OS 是 32 位的,因此,在当前系统下写引导代码,则需要求编译器编译为 16 位实模式代码。因此,你不得不写 16 位代码,编译器根据情况将 32 位操作和地址调整至 16 操作数和地址。但在大部分情况下,不需要作调整,直接生成 16 位代码即可。
以 nasm 编译器为例,下面给出一些代码片断:
; *********************************************************; * unreal_mode.asm for test unreal mode on x86 *; * * ; * Copyright (c) 2009-2010 *; * All rights reserved. *; * mik(deng zhi) *; * visit web site : www.mouseos.com *; * bug send email : mik@mouseos.com *; * *; * *; * version 0.01 by mik * ; *********************************************************
%include "include/arch/x64.inc" ; mouseOS 0.02 project
BOOT_SEG equ 0x7c00
bits 16
org BOOT_SEG ; for int 19 start: cli
; A20 gate enable FAST_A20_ENABLE sti mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, BOOT_SEG
; real mode ---> protected mode next: ; next setup is enter proected mode; the proected mode is temporary
; How do ?; frist: disable all IRQs and NMI
cli NMI_DISABLE ; NMI disable
; second: load temp GDT into GDTR
db 0x66 ; adjust to 32-bit operand size lgdt [GDT_LIMIT] ; load temp GDT into gdtr
; third: enable proected mode mov eax, cr0 bts eax, 0 ; CR0.PE = 1 bts eax, 1 ; CR0.MP = 1 mov cr0, eax ; enable protected mode
; fourth: far jmp proected mode code
jmp dword code32_sel:code32_entry
bits 32
; Now: entry 32bit protected mode, but paging is disable; So: memory address is physical address
code32_entry: mov ax, data16_sel mov ds, ax mov es, ax mov ss, ax mov esp, 0x7ff0
mov di, 0 mov si, protected_msg call printmsg
上面代码片断显示:在一个源文件中,使用 bits 16 指示 nasm 生成 16 位的代码,并且使用 bits 32 指示 nasm 生成 32 位代码。 还可以使用 bits 64 来生成 64 位代码。
上面代码片断中,使用 bits 伪指令来指示 nasm 生成何种代码。
这样的结果是:
实际上是:要 nasm 假设指令将运行在 16 位,32 位还是 64 位模式?
如下例子:
bits 16
mov eax, ebx mov eax, [dword 0x11223344]
代码中使用 bits 16 指示生成 16 位代码,它实际上是要 nasm 假设下面的指令将要运行在 16 位模式下。
00000000 6689D8 mov eax,ebx00000003 6766A144332211 mov eax,[dword 0x11223344]
这和表 4.5.1 和 表 4.5.2 所列的编码是一致的。 使用到了 66H prefix 和 67H prefix 进行 override
对于 16 位和 32 位模式来说,这个指令的位模在式相当于指出 default opernad size 是多少。但是对 64 位模式来说,并不代表 nasm 将假设指令的 default operand size 是 64 位。
processor 处于什么模式下,这是系统程序员需要考虑的问题,从而通过代码体现出来,编译器根据代码生成相应的代码。
也就是说:从 processor 角度来看,它以什么模式来对机器指令进行解码
机器指令 processor 在 16 位模式下 processor 在 32 位模式下 processor 在 64 位模式下 69 8d mov ax, bx mov eax, ebx mov eax, ebx
上表显示,同一条机器指令,当 processor 运行在不同的模式下,指令的解码是不同的。但是只不同在于 operand size
当需要改变地址大小的时候,也需要使用 67H prefix 来进行调整。同样是在所支持的 effective address-size 范围内。
是指: 在可接受的 effective address size(有效地址大小)范围内改变其 address size(地址大小),使它的 address size 不再是 default address size
指令中可以 67H 进行 address size override,同样 67H 不是 opcode,processor 的解码逻辑遇到它会把它当作 prefix
怎样进行 Override 以及 Override 什么? 都是有固定的规则的,这和 default address size 以及 effective address size 有紧密的关系
表 7.2.1
processor 模式 default address size effective address size prefix 16 位模式 16 16 --- 32 67H 32 16 67H 32 --- 32 位模式 16 16 --- 32 67H 32 16 67H 32 --- 64 位模式 64 32 67H 64 ---与 Operand size override 规则一样,在 effective address size 范围里调整为另一个 address size 需要使用 67H prefix
在 64 位模式下 default address size 是 64 位,不能调整到 16 位地址。
以 16 位模式为例,如果需要访问 64K 以上的地址,需要什么 32 位的寻址模式。
那么需要使用 67H prefix 将 16 位寻址模式调整到 32 位寻址模式。
由于在 16 位的 default operands size 和 default address size 下,但该指令使用 32 位 operand size 以及 32 位 address size
也就是说既要调整 operand size 也要调整 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 位的。
在当前的 32 位编译环境下,应使用 67H prefix 将这个 16 位地址调整 32 位地址。
最终的 encodes 是: 67 8b 40 0c
40H 这个 ModRM 寻址的地址方式是: [bx + si + 0x0c] (16 位),当使用 67H prefix 调整为 32 位地址时是:[eax + 0x0c]
若这条指令的 encode 放在 16 位下执行,汇编形式变为: mov eax, dword ptr [eax + 0x0c](16 位下,67H prefix 调整的结果)
上面的 3 个例子显示了 16 位地址和 32 位地址的区别,主要来自 16 位的内存操作数寻址只支持 BX 与 BP 寄存器作为基址寄存器,SI 和 DI 寄存器作为变址寄存器,比 32 位的内存寻址少得多。
表7.5.1 assembler 在 32 位下和 16 位下的区别(assembler 编译上下文环境)
序号 指令 在 32 位下编译的结果 在 16 位下编译的结果 1mov dword ptr [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 ptr [0x11223344] a1 44 33 22 11 66 a1 44 333mov eax, dword ptr [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)指示 processor 对内存操作数寻址模式上的转变:
★ 当运行在 16 位代码时,将要使用 32 位地址(default address-size 是 16 位)。因此,内存寻址要用 32 位寻址模式。
★ 当运行在 32 位代码时,将要使用 16 位地址(default address-size 是 32 位)。因此,内存寻址要用 16 位寻址模式。
表2:processor 在 16 位下与 32 位下解析区别 (processor 执行上下文环境)
序号 指令编码 encods(机器指令) processor 运行在 32 位下时解析为 processor 运行在 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], 0x12345678367 8b 40 0c mov eax, dword ptr [bx + si + 0x0c] mov ax, word ptr [eax + 0x0c] 48b 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 当 processor 在 16 位下,只解析前面的 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 当 processor 在 32 位下,只解析前面的 66 67 c7 84 c8 44 33 22
剩下的 11 78 56 34 12 将被视为下一条指令。
在汇编代码层面上,assembler 根据当前编译环境,将汇编语句生成相应的 encodes 决定是否使用 67H prefix
在机器代码层面上,processor 根据当前执行环境来决定如何解析机器指令
对于大多数内存操数据来说,缺省以 DS 为段基址的。常见的是:DS 段基址,SS 段基址。
与 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
缺省寄存器规则:
基址寄存器为 ebp 的,缺省的段寄存器(default segment register)是:SS 基址寄存器为 esp 的,缺省的段寄存器(default segment register)是:SS 在串处理指令中(如:movsb,scanb,cmpb 等),源串缺省段寄存器为 DS, 目标串缺省段寄存器为 ES 无基址寄存器的操作数中,缺省段寄存寄均为 DS (如:mov eax, [0x11223344],源操作数就是无基址寄存器) 所有代码段的缺省段寄存器都是 CS 除上面几种情况下,所有内存操作数的缺省段寄存器都是 DS
foo: push ebp mov ebp, esp lea eax, [ebp - 0x0c] mov eax, dword ptr [eax] … mov esp,ebp pop ebp ret
[ebp-0xc]:这个内存操作数缺省是基于 SS 段的
[eax]:这个内存操作数缺省是基于 DS 段的。
因此,对于上面的片段,[ebp - 0x0c] 是在 SS segment,即 stack 内。
[eax] 这个内存地址按照程序的意图是访问 stack 内的数据,所以,这里我将它调整为 stack segment
lea eax, [ebp - 0x0c]mov eax, dword ptr ss:[eax](调整为访问 stack)
为什么一般程序都不会这么写呢? 那是因为,现代的操作系统都是采用平坦的内存模式,即:CS=SS=DS=ES,所以对 [eax] 这个操作数不需调整其结果是正确的。
产生的编码是: 36 8b 00
其中,36 也就是 SS segment-override prefix,将 DS 段调整为 SS 段。
当需要进行调整段寄存器时,就使用以上的 segment-override prefix。
这些 prefix 对 Opcode 进行补充,增强指令的功能,优化指令执行。起重复执行指令的功能
F3: rep/repz prefix F2: repnz prefix
看下面这段 c 代码:
char *move_char(char *d, char *s, unsigned count){ char *p = d;
while (count--) *d++ = *s++;
return p;}
这是典型的、经典的字符串复制c代码,对应以下类似的汇编代码:
最初版本:
move_char: push ebp mov ebp, esp sub esp, 0x0c mov eax, [ebp+8] mov edi, eax mov esi, [ebp+0x0c] mov ecx, dword ptr [ebp+0x10] move_loop: mov bl, byte ptr [esi] mov byte ptr [edi], bl inc esi inc edi dec ecx jnz move_loop mov esp, ebp pop ebp ret
----------------------------------------------------------------上面的代码性能低下,是很死板的实现,优化的空间巨大。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) { repeat do ecx = ecx - 1}
首先判断 ecx 是否为 0,不为 0 则执行指令。
使用 rep 优化版本:
mov_char: ... ... mov edi, [ebp + 0x08] mov esi, [ebp + 0x0c] mov ecx, [ebp + 0x10] rep movsb ... ... ret
使用串指令 movsb 配合 rep prefix 进行复制,rep movsb 的编码为:
f3 a4F3 prefix 另一层意义是 repe/repz,用于改变标志位的串操作:scas, cmps 指令
意思是:当比较结果相等(ZF=1)并且循环次数(ecx)不为 0 时进行重复操作。(重复的条件是:ZF = 1 & ecx <> 0)
即:它的结束条件是:ecx = 0 或者 ZF = 0, 意思是:不相等时或者次数到了,就不重复执行指令
它的 c 伪码形式如下:
if (ecx != 0 && ZF = 1) { repeat do ecx = ecx - 1}
常见运用一些跳过字符的逻辑上,如下面 C 代码,用于截除串前面空格:
char *trim(char *s) { while (*s && *s == ' ') s++;
return s;}
rep 与 repe/repz 是相同的 prefix,作用于不同的串指操作意义也不同:
f3 a4 --- 这时它是 rep f3 ae --- 这时它是 repz/repe当作用于不修改标志位的串指令时,它的意义是 rep,作用于修改标志位的串指令时,它的意义是 repz/repe
F2H prefix 是表达 repne/repnz 意思是: 结果不相等(不为零)时循环。(重复条件是 ZF == 0 并且 ecx <> 0)
结束条件是:ecx = 0 或者 ZF = 1 即:结果相等时退出循环。
同样也是用于改变标志位的串操作 scas 和 cmps
它的 c 伪码形式如下:
if (ecx != 0 && ZF = 0) { repeat do ecx = ecx - 1}
常见一些查找字符的逻辑上,如下面 C 代码:
char *get_char(char *s, char c){ while (*s && *s != c) s++;
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 81 00 01 00 00 00F0: Lock prefix 锁地址总线。