标题: 【x64 指令系统】之指令编码内幕 -- (1 楼:目录)
作者: mik
时间: 2008-12-5 23:23
标题: 【x64 指令系统】之指令编码内幕 -- (1 楼:目录)
目 录-------------------------------------------------------------------------------
1、序言 -------------- 1 楼
2、指令格式 -------------- 2 楼
3、深入了解 prefix -------------- 3、4 楼
4、64 位计算 ------------- 5 楼
5、指令编码核心之 Opcode ------------- 6 楼
6、x87 指令、3DNow 指令、SSEx 指令 -------------- 7 楼
7、强悍的 AMD SSE5 指令 --------------- 8 楼
8、指令编码核心之 ModRM 寻址 --------------- 9 楼
9、指令编码核心之 SIB 寻址 ---------------- 10 楼
10、Displacement 与 Immediate ---------------- 11楼
11、解析指令 (完结) ---------------- 12 楼
--------------------------------------------------------------------------------
1、序言
在讲解 x64 指令编码之前,先给 2 个例子看看,相当于学习 C 语言经典的第一节课。
main() { printf(“hello,world!”); }
|
1、汇编代码译为机器码例子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 值
-----------------------------------------------------------------
对于多数编译器,立即数 0x12345678 会被截断,只取低 16 位值。要么就是编译器拒绝支持。
至于为什么会译为这个机器编码,在以后的章节里再学习
2、将机器码译为汇编码例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 指令编码的来龙去脉。
.
[
本帖最后由 mik 于 2008-12-14 22:46 编辑 ]
作者: mik
时间: 2008-12-5 23:59
2、指令格式
在序言里的例子里:
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 (共12个字节)。
go ahead~
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 值
2、指令长度图中显示,指令长度最长是 15 个字节,在什么时候达到饱和的 15 个字节呢?
答案是像这条指令:
lock mov dword ptr es:[eax+ecx*8+0x11223344], 0x12345678当在 16 位下,这条指令将达到饱和的 15 个字节长度。
注意,仅在 16 位下,这条指令的编码是:
26 66 67 F0 C7 84 C8 44 33 22 11 78 56 34 12 (正好 15 个字节)
2.1、这个编码的具体含义
26 66 67 F0: 这 4 个字节是 prefix,这 4 个字节达到了饱和的 prefix 状态。
26 是 ES segment register
66 是 operand-size override
67 是 address-size override
F0 是 Lock prefix
C7:Opcode
84:ModRM
C8:SIB
44 33 22 11:displacement
78 56 34 12:immediate
有没有超过 15 个字节的指令编码,答案是:没有! 那么在 64 位下呢? 答案同样是没有!
勘误: 以前,由于作为演示如何达到 15 个字节长度饱和状态,而忽视了 lock 用在 mov 指令上是无效的。 经网友指出 dxcnjupt 实验指出,谢谢。
更正: mov 指令是属于 load - store 类指令。lock 用在 mov 指令上会引发 #UD 异常。 lock 应用于 read-modify-write 类指令上。意即:指令执行会产生中间结果,运算后再 write 内存。 |
那么,将上面的例子改为:lock add dword ptr es:[eax+ecx*8+0x11223344], 0x12345678
对应编码为:
26 66 67 F0 81 84 C8 44 33 22 11 78 56 34 12 (共 15 个字节)既然这样,顺便提一提:
2.1、 为什么指令长度最长是 15 字节? (1)4 个字节的 prefix 已经达到饱和度了:1 个字节用来调整 segment,1 个字节用来调整 Operand-Size,1 个字节用来调整 Address-Size,还有 1 个字节用来 lock 总线。已经无法再增加 prefix 了。
(2)若是采用 2 个字节的 Opcode 码,则寻址模式上不会有 mem32, imm32 这种操作法。所以还是采用 1 个字节的 Opcode,而得到 imm32 4 个字节的立即数。
(3)ModRM + SIB:2 个字节。
(4)4 个字节的 displacement 值。
(5)4 个字节的 immediate 值。
这样每个组成部分都呈饱和状态,加起来总共 15 个字节,而只有采用 mem32, imme32 这种寻址模式可能会达到饱和状态。
在 64 位下,若采用 mem, imme 的寻址模式,这和 32 位是一致的,所以不会超越 15 个字节,[
本帖最后由 mik 于 2008-12-7 02:24 编辑 ]
作者: mik
时间: 2008-12-7 01:10
3、深入了解 Prefix
在 GPI(General-Purpose Instruction)指令里,Legacy Prefix 在整个编码序列里起了对内存操作数进行修饰补充作用,在这里我称呼它为 x86 prefix,这样比较直观。
x86 prefix 主要起了三个作用:调整、加强、附加。REX prefix 只是起将操作扩展 64 位的作用。
要彻底了解 x86 prefix,必须清楚了解 3 个很重要的上下文环境:缺省 operand-size 和缺省 addess-size 环境、编译器上下文环境以及当前执行上下文环境。
1、调整改变操作数 x86 指令编码会根据上面提到的 3 个上下文环境而对操作数的位置、大小以及地址进行调整改变。这里操作数特指是内存操作数。出现调整的情形,这是因为:
(1)指令的操作数大小可以为:8位、16位、32 位以及 64 位
(2)操作数的位置因段选择子而不同。
(3)操作数的地址大小可以为:16 位、32 位以及 64 位
1.1、调整操作数的大小(66H prefix ------ Default Operand-Size Override) 66h 这个 prefix 作用是改变操作数的大小,那么:
1.1.1 为什么需要改变操作数大小? 原因是:16 位下代码需要访问 32 位数据或者 32 位代码需要访问 16 位数据。
看看这两个例子:
例 1 :在 16 位代码下,指令 mov eax, ebx
由于在 16 位下,操作数的大小缺省是 16 位的,如上指令要访问 32 位的寄存器,那么需要进行调整。
变为:
66 89 d8例 2:在 32 位代码下,指令 mov ax, bx
由于在 32 位下,操作数的大小缺省是 32 位的,如上指令要访问 16 位寄存器,那么需要进行调整。
变为:
66 89 d8---------------------------------------------------------
这里有些人会觉得奇怪,为什么例1 与 例2 编译器生成的结果是一样的。这就是在不同的环境里,processor 解码单元会译为不同的操作结果。
1.1.2、 根据什么来改变? 根据什么来改变。这就是根据上面提到过的 3 个很重要的上下文环境:
● 缺省操作数大小
● 编译器编译环境
● 当前执行环境
这 3 个环境是有机结合起来的,是个整体。
1.1.3、缺省操作数大小(Default Operand-Size) 对于实模式环境下,操作数的 Default Operand-Size 是16位,在 32 位保护模式下,操作数的 Default Operand-Size 是 32位,在 64 位 Long 模式下 Default Operand-Size 也是 32 位大小。
当在保护模式下,读取 16 位值时,则须作出调整,同样在实模式下,读取 32 位值时,测须作出调整。
实际上: Default Operand-Size 在实模式下是 16 位这没错! 但是,在 32 位保护模式下,Default Operand-Size 并非一定就是 32 位! 保护模式下,Default Operand-Size 依赖于当前 code segment-descriptor 的 D 位,也就是 CS.D(code segment register's D)。当 CS.D = 1 时,Default Operand-Size 是 32 位的,CS.D = 0 时,Default Operand-Size 是 16 位的。 这也就是,为什么在 64 位 long 模式下,Default Operand-Size 还是 32 位而不是 64 位的根本原因,CS 中只有 0 与 1 来进行 16 位与 32 位的选择,而没有 64 位的选择!那为什么不设为 CS.D = 1 时是 64 位呢? 原因很简单:为了平滑无缝地运行 32 位代码。这是 x86_64 设计的根本原因! 为什么在实模式下,Default Operand-Size 不能进行选择呢? 因为,实模式下根本没有进行设置的途径。原因是没有 segment descriptor 进行设定。 更深入一点: 其实,在实模式下,还是有手段进行设置 Default Operand-Size 为 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.1.4、编译器编译上下文环境 所谓“编译器上下文”,是指编译器编译目标平台上下文环境。说明白点就是:编译器为什么机器编译代码,是编译为 16 位代码,还是编译为 32 位代码或者是编译为 64 位代码?
例如操作系统的引导初始化代码部分是 16 位的,现在绝大多数 OS 是 32 位的,因此,在当前系统下写引导代码,则需要求编译器编译为 16 位实模式代码。
因此,你不得不写 16 位代码,编译器根据情况将 32 位操作和地址调整至 16 操作数和地址。但在大部分情况下,不需要作调整,直接生成 16 位代码即可。
这其实也和编程人员相关的。
1.1.5、当前执行环境 Processor 处理什么模式下,这是程序员需要考虑的问题,从而通过代码体现出来,编译器根据代码生成相应的代码。
一个很典型的例子就是:当 16 位初始化代码完以及保护模式系统数据结构初始化完成后开启保护模式,然后需要从 16 位代码跳转至 32 位代码。
由于在一个汇编程序里同时存在 16 位和 32 位代码,所以,程序员在汇编级代码里应指出 16 位与 32 位分界线。编译器正确同样生成 16 位和 32 位代码。这里当然是通过 Operand-Size 的调整和 Address-Size 的调整,即:66H 和 67H prefix。
此时,程序的脑海里,应存在这样一个概念,在运行 16 位代码时,processor 当前处于实模式状态,跳转至 32 位保护模式代码时,processor 当前处于 32 位保护模式状态。 |
这就是 processor 当前执行上下文环境。
1.2.1、操作数变为什么? 或者说可以调整什么? 这是需要探讨的另一个重要话题。
前面已经提到:为什么需要改变? 后根据什么来改变? 现在是:可以改变为什么?
在 16 位实模式下:操作数的大小可以是 16 位或 32 位,也就是说:可以从 16 位变为 32 位。 在 32 位保护模式下:同样,操作数大小可以是 32 或 16 位,也就是说,可以从 32 变为 16 位。 ---- 仅此而已。
但是,在 64 位 long 模式下,操作数大小有 3 个选择:64 位、32 位以及 16 位。 可以从 32 位变为 64 位,也可以从 32 位变为 16 位! |
以上所述,这是关于 Operand-Size 的有效 Size 的话题。
2、调整地址大小(67H prefix ----- Address-Size Override) Address-Size 和 Operand-Size 一样,也有缺省地址大小(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位。 这是必定的。 |
和 Operand-Size 一样,Address 也有“有效 Address-Size 模式”的语义
16 位下:可以调整为 32 位地址。 32 位下:可以调整为 16 位地址。 64 位下:可以调整为 32 位地址,但不能调整为 16 位地址。
更深入一点: 64 位下,虽然能调整为 32 位址,但实际上还是 64 位地址,32 位地址会被扩展为 64 位地址,所以:调整为 32 位址,实际上是限制在前 4G 范围内活动而已。 |
以下,也举几个例子来说明
例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 eax, [3344] 它的地址将被截断为 16 位。 即,地址:0x3344,多出 22 11 两个字节属下条指令边界了,同时,目标操作数被改变为 ax
除非,
这样编码 66 67 a1 44 33 那么,结果是 mov ax, [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,这才是正确的逻辑
为什么一般程序都不会这么写呢? 那是因为,现代的操作系统都是采用平坦的内存模式,即:CS=SS=DS=ES,所以对 [eax] 这个操作数不需调整其结果是正确的。
那么,我们真要对 [eax] 内存操作数进行调整为:mov dword ptr ss:[eax], 0
这样的话,会产生下面的encode:
36 c7 00 00 00 00 00 其中,36 也就是 prefix,是 SS segment-override 的 prefix,将 DS 段调整为 SS 段
好啦,每个段寄存都有它对应的 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],此时是不需要调整缺省段寄存器
作者: mik
时间: 2008-12-7 01:53
(接上)
2、增强指令功能 一些 prefix 对 Opcode 进行补充,增强指令的功能,优化指令执行,看下面这段 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, 0xc mov eax, [ebp+8] mov edi, eax mov esi, [ebp+0xc] mov ecx, dword ptr [ebp+0x10]
move_loop: mov bl, byte ptr [esi] mov byte ptr [edi], bl inc esi inc edi dec ecx test ecx,ecx jnz move_loop
mov esp, ebp pop ebp ret |
上面的代码性能低下,是很死板的实现,优化的空间巨大。
x86 为串提供了相应的串操作指令(ins,outs,lods,stos,scas,cmps),对这些串指令提供 prefix 来增强优化这些指令:
● F3: rep prefix 或 repe prefix
● F2: repne prefix2.1、 rep prefix 重复进行串操作,仅应用于 ins,outs,movs,lods,stos 这些不改变标志位的串指令,结束条件是 ECX 为 0。
使用串操作及 rep prefix 上面的汇编代码可简单如下:
move_char:
… …
mov eax, [ebp+8]
mov edi, eax
mov esi, [ebp+0xc]
mov ecx, [ebp+0x10]
rep movsb
… …
rep movsb 的操作原理和上面的 C 代码一致,下面是伪码:
while (ecx != 0) { c = ds:[esi]; es:[edi] = c; esi = esi + 1 edi = edi + 1 ecx = ecx – 1; } |
2.2、 repe prefix F3 的另个 prefix 意思是 repe/repz,用于改变标志位的串操作:scas,cmps 意思是当相等(ZF=1)并且循环次数(ecx)不为 0 时进行重复操作。
结束条件是:ecx 为 0 或者 ZF 标志位为 0, 或者说是循环条件是(ecx 不为 0,并且 ZF 标志位为 1)
|
常见运用一些跳过字符的逻辑上,如下面 C 代码,用于截除串前面空格
char *trim(char *s) { while (*s && *s == ‘ ‘) s++;
return s; }
|
而用伪码表示为:
While (ecx != 0 and ZF = 1) { Ecx = ecx – 1; cmpsb } |
rep 与 repe/repz 是相同的 prefix,作用不用体现在对串指操作上:
movsb 的 opcode 是 A4,scasb 的opcode 是 AE,对于下面的 encode:
F3 A4:这时 F3 prefix 是 REP, movsb 不改变标志位
F3 AE:这时 F3 prefix 是 REPZ, scasb 改变标志位2.3、 repne/repnz prefix F2:这个 prefix 是 repne/repnz,意思是:
循环次数(ecx)不为 0 并且 ZF=0 时重复操作。结束条件是:ecx=0 或者 ZF=1 |
同样也是用于改变标志位的串操作 scas 和 cmps。
常见一些查找字符的逻辑上,如下面 C 代码:
char *get_c(char *s, char c) { while (*s && *s != c) s++;
return s; }
|
而用伪码表示为:
While (ecx !=0 and ZF != 1) { Ecx = ecx – 1; cmpsb }
|
3、 附加功能(LOCK) 对于写内存的一些指令增加了锁地址总线的功能,这些写内存的指令如常见的 sub,add 等指令,通过 Lock prefix 来实现这功能,使用 Lock prefix 将会使 processor 产生 LOCK# 信号锁地址总线
注意: Lock prefix 仅使用在一些对内存进行 read-modify-write 操作的指令上,如:add, sub, and 等指令。 否则,将会产生 #UD (无效操作码) 异常 |
F0: Lock prefix 锁地址总线。
作者: mik
时间: 2008-12-8 01:39
4、64 位计算
这里讲的只是 x64 提供的 64 位计算方案,而非关于 64 位方面的编程知识。关于 64 位编程方面以后有机会再讲解。
AMD 在 x86 体系的 32 位计算扩展为 64 位计算,这是通过什么来实现的? 它是怎样设计的? 具体细节是什么?
4.1、 x64 的硬件编程资源 了解现在 processor 提供编程资源是很重要的,下面介绍 x86 和 x64 提供的用户编程资源。
4.1.1、 x86 原来的 32 位编程资源● 8 个 32 位通用寄存器(GPRs):EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI
这些寄存器还可分解为 8 个 8 位寄存器:AL、CL、DL、BL、AH、CH、DH、BH
和 8 个 16 位寄存器:AX、CX、DX、BX、SP、BP、SI、DI
● 6 个段寄存器:ES、CS、SS、DS、FS、GS
● 32 位的 EFLAGS 标志位寄存器
● 32 位的指令指针寄存器 EIP
● 8 个 64 位 MMX 寄存器(mmx0~ mmx7, 同 ST0 ~ ST7)
● 8 个 128 位 XMM 寄存器(xmm0 ~ xmm7)
● 32 位的寻址空间(Virtual Address Space):00000000 ~ FFFFFFFF
4.1.2、 x64 的 64 位编程资源 x64 通过扩展和新增了编程资源:
● 32 位通用寄存器被扩展至 64 位,除了原有的 8 个寄存器,又新增 8 个寄存器,共 16 个通用寄存器:RAX、RCX、RDX、RBX、RSP、RBP、RSI、RDI、R8、R9、R10、R11、R12、R13、R14、R15
● 保留了原有的 6 个段寄存器,但是作用被限制
● 32 位的标志寄存器被扩展为 64 位的标志寄存器 RELAGS
● 8 个 64 位 MMX 寄存器不变 (mmx0 ~ mmx7)
● 新增 8 个 XMM 寄存器,共 16 个 XMM 寄存器(xmm0 ~ xmm15)
● 64 位的寻址空间(Virtaul Address Space):00000000_00000000 ~ FFFFFFFF_FFFFFFFF
x64 虽然新增了 8 个通用寄存器,对于一般 RISC 实现的 32 个通用寄存器来说,还是少了点。AMD 在 64 位寻址空间实则只实现了 48 位 virtual address 寻址空间,高 16 位被保留起来。
|
4.2、 寄存器编码(或者说 ID 值)● 16 个 64 位通用寄存器是: 0000 ~ 1111, 其值是:0 ~ 15
x86 原有的 8 个 32 位通用寄存器是:000 ~ 111 其值是:0 ~ 7
● 6 个段寄存器的编码是:000 ~ 101 其值是:0 ~ 5
● 8 个MMX 寄存器编码是: 000 ~ 111 其值是:0 ~ 7
● 16 个 XMM 寄存器编码是: 0000 ~ 1111 其值是:0 ~ 15
寄存器编码是寄存器对应的二进制编码,按顺序来定义,看下面的表格:
RAX/ES/MMX0/XMM0 -> 0000
RCX/CS/MMX1/XMM1 -> 0001
RDX/SS/MMX2/XMM2 -> 0010
RBX/DS/MMX3/XMM3 -> 0011
RSP/FS/MMX4/XMM4 -> 0100
RBP/GS/MMX5/XMM5 -> 0101
RSI/MMX6/XMM6 -> 0110
RDI/MMX7/XMM7 -> 0111
R8/XMM8 -> 1000
R9/XMM9 -> 1001
R10/XMM10 -> 1010
R11/XMM11 -> 1011
R12/XMM12 -> 1100
R13/XMM13 -> 1101
R14/XMM14 -> 1110
R15/XMM15 -> 1111
x64 的 16 个通用寄存器的编码是 0000 ~ 1111 是在 x86 原有的通用寄存器 3 位编码上通过 REX prefix 对相应的寄存器进行扩充 1 位,从而变成了 4 位编码,共能表达 16 个值。
|
4.3、 开启 64 位计算的基石(REX prefix) AMD64 体系的物理环境:操作数的 Default Operand-Size 是 32 位,而 Address-Size 是固定为 64 位的。因此,在怎么设计 64 位的计算方案时,有 3 个问题要解决的:
● 问题1:当要访问是 64 位的寄存器时,那么必须要有一种机制去开启或者说确认访问的寄存器是 64 位的。
● 问题2:当要寻址内存操作数时,那么也必须要去开启 64 位地址。
● 问题3:如何去访问新增加的几个寄存器呢? 那么也必须要有方法去访问增加的寄存器?
那么在 64 位 Long 模式下,为什么不将操作数的 Default Operand-Size 设计为 64 位呢? 原因前面已提到:由于体系限制,x86 体系初计的初衷是为了实现平滑无缝地运行原有的 32 位代码,达到完美的兼容性。
x86 体系当初设计时就没思考到会被扩展到 64 位。所以在 Segment-Descriptor(段描述符)里就没有可以扩展为 64 位的标志位。CS.D 位只有置 1 时是 32 位,清 0 时为 16 位,这两种情况。
AMD在保持兼容的大提前下,只好令谋计策。AMD的解决方案是:增加一个 64 位模式下特有 Prefix,以起到开启 64 位计算功能以及访问新增寄存器的能力。 这就是 REX prefix。
4.3.1、 REX prefix 的具体格式及含义 REX prefix 的取值范围是:40 ~ 4F(0100 0000 ~ 0100 1111),这样一来原有相应的 Opcode 被占用了,这些原来的 Opcode 在 64 位 Long 模式下是无效的。变成了 REX prefix。
来看下原来 opcode 取值范围的 40 ~ 4F 的是什么指令:Opcode 为 40 ~ 47 在 x86 下是 inc eax ~ inc edi 指令,48 ~ 4F 在 x86 下是 dec eax ~ dec edi 指令。
REX prefix字节的组成部分如下:0 1 0 0 0 0 0 0
- - - -
W R X B
● bit0:REX.B
● bit1:REX.X
● bit2:REX.R
● bit3:REX.W
● bit4 ~ bit7:此域固定为 0100,也就是高半字节为 4。
--------------------------------------------------------------------------------
REX.W 用来打开 64 位访问能力,REX.W = 1 时,操作数是 64 位, REX.W = 0 时,操作数是 Default Operand-Size
REX.R 用来扩展 ModRM 寻址中的 ModRM.reg 使用 ModRM.reg 为 4 位编码。
REX.X 用来扩展 SIB 寻址中的 SIB.index,使得 SIB.index 域为 4 位编码。
REX.B 用来扩展 SIB 寻址中的 SIB.base, 使用 SIB.base 域为 4 位编码及对直接嵌在 Opcode 中的 reg 进行扩展。
4.3.2、 解决之道(1)设计 REX.W 来解决访问 64 位操作数的能力。在 REX.W = 1 就开启了 64 位计算能力,包括 64 位操作数和 64 位寻址。
(2)设计 REX.R 来解决访问新增的 8 个寄存器的能力。 ModRM.reg = 000 而 REX.R = 1,组合的寄存器 ID 为 1000,这是寄存器 r8。
(3)设计 REX.X 及 REX.B 来解决 64 位寻址的问题。
下面使用几个例子来说明解决之道:例1:指令
mov eax, 1 这条指令的 Default Operand-Size 是 32 位,在 32 位下它的机器编码是:
b8 01 00 00 00(其5个字节)
64 位下使用 64 位寄存器,它的语法元素变成:
mov rax, 1 此时,它的机器编码是
48 b8 01 00 00 00 00 00 00 00 (共10个字节)
注意这里的 48 就是 REX prefix字节,即:0100 1000 它的各个域值是:REX.W = 1,定义操作数是 64 位的,REX.R = 0、REX.X = 0、 REX.B = 0 这条指令不需要 ModRM 和 SIB 字节进行寻址,所以 RXB 域都为 0。
这里有个值得思考的地方,若 REX.W 域为 0 时,这条指令的操作数是 32 位的,机器编码:40 b8 01 00 00 00(其 6 个字节)是与 b8 01 00 00 00 结果一样的,都是 mov eax, 1 40 即:0100 0000 REX.WRXB 都为 0,没有什么实际意义。
|
例2:指令:
mov rax, r14 这是一条常见 64 位指令,它是需要 ModRM 来进行寻址的。源寄存器是 r14,目标寄存器是 rax 它的机器编码是:
4c 89 f0(共3个字节)
在这个编码里 4c 是 REX prefix,89 是 opcode,F0 是 ModRM。
4c (0100 1100),其中 REX.W = 1,REX.R = 1,XB 都为 0。
ModRM 的值是 F0(11-110-000),Mod=11,Reg=110, R/M = 000,在这里先不讲 ModRM 的含义,在后面的章节再详述。在这条指令里,Reg 表示源操作数 r14 的 ID 值。
r14 是新增加寄存器,所以需要 REX.R 进行扩展,得出最终寄存器的 ID 值:1110,这是 r14 寄存器的 ID 值,从而得出正确的编码。 REX.R 扩展了 ModRM.reg 从而变成 4 位 ID 值。
例3:回到序言里的例子:
mov word ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678 作为例子,我将它改为 64 位指令,如下:
mov qword ptr [rax + rcx * 8 + 0x11223344], 0x12345678 操作数大小变为 64 位,而 base 寄存器和 index 寄存器都改为 64 位,disp(offset)和 imme(值不变),为啥不变?在以后的章节会有详述。
好,现在来看看指令怎么译:
(1) REX.W: 要置为 1 以使用 64 位大小。
(2) REX.B: 由于 base 不是新增的寄存器,所以置为 0
(3) REX.X: 由于 index 也不是新增的寄存器,所以置为 0
(4) REX.R: 源操作数和目标作数不是寄存器,所以置为 0
所以,REX prefix 就等于 48(0100 1000)
故,整条指令编码是:
48 c7 84 c8 44 33 22 11 78 56 34 12(共12个字节)
例4:我将上面的例子再改一改,变为:
mov qword ptr [r8 + r9 * 8 + 0x11223344], 0x12345678那么,看看这指令怎么译:
(1)REX.W:置 1,使用 64 位大小
(2)REX.B:base 寄存器是 r8,是新增寄存器,所以置为 1
(3)REX.X:index 寄存器是 r9,是新增寄存器,所以置为 1
(4)REX.R:操作数中没有寄存器,所在置为 0
所以,REX prefix就等于(0100 1011)4b
故,整条指令编码是:
4b c7 84 c8 44 33 22 11 78 56 34 12(共12个字节)
例5:看看这条指令
mov r8, 1(1)REX.W:置1
(2)REX.B:访问 Opcode 中的寄存器 ID 值,它是新增寄存器,所为置 1
(3)REX.X:置 0
(4)REX.R:置 0
所以,REX是 49(0100 1001)
故整条指令编码是:
49 b8 01 00 00 00 00 00 00 004.3.3、 解开 REX prefix 迷惑(1)关于顺序:REX 一定是在 x86 prefix 之后,而在 Opcode 之前。
(2)关于冲突:当 x86 prefix 和 REX prefix 同时出现,而又出现冲突时,REX 的优先权要优于 x86 prefix,
举个例子:指令
mov r8, 1 若解码器遇到以下编码怎么办?
66 49 b8 01 00 00 00 00 00 00 00 既有 66 又有 49,那么结果 66 会被忽略,也就等于:
49 b8 01 00 00 00 00 00 00 00。
而对于
66 b8 01 00 00 00 00 00 00 00 这个编码来说:会被解析为:
mov ax, 1 去掉了 49 这个 REX prefix 后操作数被调整为 16 位。
(3) 关于原来 Opcode 码,由于 40 ~ 4F 被作为 REX prefix,那么原指令 inc reg/dec reg,只能使用 FF/0 和 FF/1 这两个 Opcode 了。
(4)缺省操作数大小(Default Operand-Size)
64 位绝大部分缺省操作数是 32 位的,但有一部分是 64 位的,依赖于 rsp 的寻址和短跳转(near jmp/near call)是 64 位的。
如下指令:push r8
REX 值是 41(0100 0001),即 REX.W 为 0,使用 default opearnd-size,它的编码是
41 ff f0
有两大类指令,它的 Default Operand-Size 是固定 64 位。 它不依赖于 CS.D,且不能被 66h prefix 改变 Size。
第一类是:转移指令,包括 call、jmp(任何形式)以及 loop(任何形式)。这类指令的操作数是依赖于 rip (Instruction Pointer 寄存器),64 位下 rip 是 64 位且不能改变。 如:call [rax] 这条指令,它的编码是:FF 10。 它是无需给出 REX prefix 的。 第二类是:栈操作指令,包括 push、pop 以及 enter、leave 等。 同样,它不依赖于 SS.B 且不能被 66h prefix 改变 Size。这类指令依赖于 rsp(stack pointer 寄存器)。 64 位下 rsp 是固定 64 位且不能改变的。 如:push [rax] 这条指令的编码是:FF 30,同样它无需给出 REX prefix。
更深入一点: 对于第一类指令:call [rax],它深入的含义是: rip = [rax]、 goto rip。 rip 的获取是由 [rax] 给出。rip 的 Size 是固定为 64 位的,rip 的值不可能会被改变 32 位。所以,依赖于 rip 的这类转移指令是 rip 的长度,也就是 64 位。
对于第二类指令:push [rax],它深入的含义是:rsp = rsp - 8、[rsp] = [rax]。 同样在 64 位,rsp 的值是不能改变的。
但是,rsp 与 rip 含义不同。 在 32 位下,esp 所指向的栈结构 的值是可以被改变的。那是因为:stack pointer 的值要依赖于 SS.B(SS segment 的 B 位),SS.B = 1 时,esp = esp +- 4 ,stack pointer 为 32 位。 SS.B = 0 时,esp = esp +- 2,stack pointer 为 16 位。 但是在 64 位 Long 模式下,SS segment 是无效的。也就是说:SS.B 是无效的。此时:rsp 的值固定为 64 位。
在 64 位下,call [eax] 这类指令,编译器要么就提示错误。要么就忽略错误,自动调正。 |
最后,一句话来对 x64 的 64 位扩展作出评价:我认为它的设计架构是完美的,也很成功的。对 AMD 的设计能力是很赞同。我不会认为比 Intel 差,甚至要好。因为在对 Opcode 的设计上,Intel 就不那么完美,这是后一节的话题了。
对 REX prefix 话题的探讨到此结束。 对 x64 编码探讨才刚刚开始,后面继续探讨 Opcode 相关话题。
作者: mik
时间: 2008-12-8 14:42
5、 指令编码核心之 Opcode
x86 指令编码的核心是:Opcode、ModRM 以及 SIB,Opcode 提供指令的操作码,ModRM 及 SIB 提供操作数的寻址模式。指令编码设计模式是:Opcode 的设计要考虑兼顾 ModRM。ModRM 要服务于 Opcode,SIB 是对 ModRM 的补充辅助。
5.1、 初窥 Opcode 一条指令对应一个唯一的 Opcode 码。 在 1 个字节的空间里:00 ~ FF,Prefix 与 Opcode 共同占用这个空间。由于 x86 是 CISC 架构,指令不定长。解码器解码的唯一途径就是按指令编码的序列进行解码,关键是第 1 字节是什么? 遇到 66h,它就是 prefix,遇到 89h,它就是 Opcode。
记住: Prefix 与 Opcode 共享空间的原因是:Prefix 是可选的。在编码序列里,只有 Opcode 是不可缺少的,其它都是可选。这就决定了指令编码中的第 1 个字节对解码工作的重要性。 |
除了 1 个字节的 Opcode 外,还有 2 个字节的 Opcode 以及 3 个字节的 Opcode,第 2 个 Opcode 码是由 0F 字节进行引导。即:2 个字节的 Opcode 码,其第 1 个 Opcode 必定是 0F 字节。
在 AMD 的 SSE5 指令集推出之前,x86 平台没有真正意义上的 3 个字节的 Opcode 码。可以理解为:伪 3 个字节 Opcode。3 个字节的 Opcode 是通过 Prefix 修饰而来。用于修饰 Opcode 的 prefix 是:66h、F2h 以及 F3h。 使用 66h (Default Operand-Size Override),F2h(REPNZ)及 F3(REP/REPZ)来修饰 Opcode 而达到 3 个字节 Opcode 码,是其 Opcode 码有这 3 个 prefix 功能的隐含语义。 |
5.2、 学会看 Opcode 表 怎么去看指令的 opcode,获得指令 opcode 编码,还是很有学问的。
5.2.1、 从指令参考页里看opcode 码 可能许多人喜欢从指令参考页里查看 Opcode,下面的图是 AMD 文档中对于指令参考页的描述:
从指令参考页里可以得出以下信息:
(1)指令助记符 mnemonic
(2)指令的 Operand 属性
(3)指令的 Opcode 码
(4)指令的描述。
这确实可以得到想要的 Opcode 码,还是 Operand 数以其属性。下面摘录了 mov 指令一部分的参考页:
从这里看出,这个 Opcode 8B 有几种操作数形式,reg <- reg、reg <- mem,Operand-size 可以是 16/32/64。
不过这并不是了解 Opcode 码的好地方,指令参考页主要是对指令的操作进行相应的描述。对掌握 Opcode 码不是那么直观和透彻。下面要看全局的 Opcode 表格。
5.2.2、 怎么看 Opcode 表 学会看 Opcode 表才能清晰地进行分析,Opcode 表是一个全面的透彻的总结表,又可以说十分细致。Intel 和 AMD 的文档中均提供了 Opcode 表,Opcode 表有 One-byte Opcode 表、Two-byte Opcode 表和 X87 Opcode 表等。
5.2.2.1、 Opcode表上的基本元素 Opcode 表上描述的范围是 00 ~ FF,即 1 个字节共 256 个值,每 1 个值描述不同的属性,包括:
● 绝大部分代表 1 个 Opcode 码。
● 26、2E、36、3E、64、65、66、67、F0、F2、F3 则是 prefix。
● 0F 指示 2 个字节的 Opcode,引导性 Opcode。
● 还有一部分是 Group 属性指令,由 ModRM 中的 reg 来决定。这部分还包括了 x87 float 指令的 Opcode 码。
每个 Opcode 码还附有相应的 Operands 属性,Operands 属性是用来描述 Operands 的,包括 Operands 个数、寻址类型及 Size。
注意: 所谓 Group(组)属性指令是指:Intel 将一些 Opcode 码抽出来,不具体实际的操作。具体的功能是由 ModRM 的 reg 来决定。ModRM.reg 就起决定性作用,它反过影来 Opcode 码,这主要原因是原因:这种 Opcode 的操作数无法与 ModRM 得到良好的配合。从而决定了 Opcode 受制于 ModRM.reg。 很典型的 FFh,这就是一个 Group 属性的 Opcode 码。 FFh 是一组指令的代表,FFh 要由 ModRM.reg 才能决定它的指令功能。当 ModRM.reg = 010 时,FFh 是 CALL 指令的 Opcode 码。 当 ModRM.reg = 000 时,它是 INC 指令的 Opcode 码。
更透彻一点: Opcode 的组是按照 Operands 的属性进行分组的。 说白了就是:这些 Opcode 提供的 Operands 寻址是不含寄存器。还就是说: 内存寻址和立即数寻址或是 disp 值。
|
看看 mov 指令 8B Opcode 表是怎样的:
上图圆圈所示是 Opcode 8B,它对应的是 mov 的 mnemonic,表格中的 Gv, Ev 是描述这个 Opcode 码所对应的指令的 Operand 属性。表示:
(1)两个 Operands 分别是:目标操作数 Gv,源操作数 Ev
(2)Gv 表示:G 是寄存器操作数,v 是表示操作数大小依赖于当前代码的 Default Operand-Size,也就是CS.D。可以是 16 位,32位以及 64 位。
(3)Ev 表示:E 是寄存器或者内存操作数,具体要依赖于 ModRM.r/m,操作数大小和 Gv 一致。
4 个字符便可以很直观的表示出:操作数的个数以及寻址方式,更重要的信息是这个 Opcode 的操作数需要 ModRM 进行寻址。
要看懂 Opcode 表必须学会分析和理解 Operand 属性字符,Intel 和 AMD 的 Opcode 表前面都有对 Operands 属性字符很仔细清晰的定义和说明。
记住以下两点:(1)Operands 属性都有两组字符来定义,前面的一组大写字母是 Operand 类型,后面一组小定字母是 Operand Size。
如: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。
这与 v 明显不同:v 是可以调整,z 是固定的。(2)另一个典型的例子是 call Ev,它的 Opcode 是FF,这个 Opcode 是个典型的 Group Opcode,为什么会定义为 Group,下面的将会有阐述。操作数的寻址是典型的 ModRM 寻址,准确地讲是 ModRM.r/m 寻址。E 既可是 GPRs 也可以是 Mem。它同样是 v 属性的 Operand-Size。
下面列举一些常见的 Operands 属性字符,详述请参考 Intel 或 AMD 手册。(1)、Operand类型字符E:GPR 或 Mem,依赖于 ModRM.r/m
G:GPRs 具体 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 及 Operands 详情。
5.3、 透析 Opcode 的编码规则 如上所述:prefix 与 Opcode 共享 00~FF 的空间,由于 Prefix 部分是 可选的,当 CPU 取指单元从 ITLB 加载指令 L1-Icache和 prefetch buffer,预解码单元通过 prefix 自已的 ID 值来解析 prefix。如读入 66h 时是解析为 prefix 而不是 Opcode。同样,读入 0Fh 时被解析为是 2 个 字节的 Opcode 中的第 1 个字节。
Opcode 的 operands 寻址一部分是在 Opcode 码直接中指定,一部分是依赖 ModRM 给定,还有一部分不依赖 ModRM 给定。直接中 Opcode 指定的 operand 寻址的是 GPRs 寻址,如:inc eax 指令(Opcode 是 40h),还有串指令,如 loads 等。
5.3.1、 1 个 Operand 的 Opcode 码编码规则(1)直接嵌入 Opcode 中
一部分 Opcode 的操作数是直接嵌入 Opcode 中的,如:inc eax、push eax、pop eax 等。 这些指令编码是 1 个字节。不依赖于 ModRM 寻址。是常见的指令。
(2)依赖于 ModRM 寻址,Group 属性 Opcode
对于单 Operand 的指令而又依赖于 ModRM 寻址。这种指令必定是 Group 属性的指令。这种 Opcode 操作数的定义是 Ev 字符。ModRM.reg 决定 Opcode 操作码,ModRM.r/m 决定寻址模式。如前面提到的典型 Call Ev 这种指令,操作数既可是寄存器,也可以是内存操作数,由 ModRM.r/m 来决定到底是 registers 还是 memory。
(3)不依赖于 ModRM 寻址,不是 Group 属性 Opcode
这种情况下的 Operand 既不嵌入 Opcode 中,也不依赖于 ModRM 进行寻址,那么它必定是 immediate 或者 displacement 值。它的 Operand 属性字符是 Iv、Ib 或者 Jz。 这种指令很常见,如:push Iv、push Ib、Jmp Jz、Jmp Jb 等。
push 0x12345678 这就是常见的这种指令,还非常常见的短跳转 jmp $+0x0c。
5.3.2、 2 个 Operands 的 Opcode 码编码规则。(1)1 个 Operand 嵌入 Opcode,另一个 Operand 不依赖于 ModRM(非 Group 属性)
这种情况下,一个 Operand 必定是 GPRs,另一个不依赖于 ModRM 的 Operand 必定是 Immediate 或 Displacement。所以它不是 Group 属性的。看看以下两个 Opcode:
指令 mov rax, Iv 它的 Opcode 是 B8,目标操作数是由 Opcode 中指定的 GPRs(rax),源操作数不依赖于 ModRM 的 Immediate。是一个寄存器与立即数的寻址指令。
在这种情况下,这个 Opcode 不依赖于 ModRM 进行寻址。 所以:这个 Immediate 可以不受 4 字节限制,它可以是 64 位的值,即:mov rax, 0x1122334455667788 是完全正确的。
思考另一个问题,在 64 位下: mov qword ptr [rax],0x1122334455667788,这指令是的错误的。原因是它的寻址是依赖于 ModRM 的。此时,它要受限于 Immediate 最大为 4 个字节的限制。
mov rax, qword ptr [0x1122334455667788],这条指令是完全正确的。它不依赖于 ModRM 寻址。此时,它不受于 displacement 最大为 4 个字节的限制。它的 displacement 值是直接嵌入指令编码中。
|
(2)依赖于 ModRM 寻址,非 Group 属性
这种依赖于 ModRM 寻址而又非 Group 属性的 2 个 Operands,绝大部分是:寄存器与内存操作数之间或 2 个寄存器之间。它的 Operands 属性字符是 Gv, Ev 或 Ev, Gv。
典型的如: mov eax, ebx
(3)依赖于 ModRM 寻址,是 Group 属性
在这种 Opcode 编码下,另一个操作数必定是 Immediate 或 Displacement。典型的如:mov ecx, 0x10 ,它的 Operands 属性字符是 Ev, Iv 等。
5.3.3、 3 个 Operands 的 Opcode 编码 在 AMD 的 SSE5 指令集推出之前,是没有第 3 个 Operand 是非寄存器或内存操作数的情形。所以,第 3 个操作必定是 Immediate 值。这种指令很少。
imul eax, ebx, 3 这是其中的一种形式。
关于 AMD SSE5 指令集在后续中有稍为详细的介绍。
要深入掌握 Opcode 表,必要记住 2 点:一是 Opcode 表的基本元素,学会分析 Operands 属性字符,二是理解上面所讲的 Operand 寻址模式。
作者: mik
时间: 2008-12-8 21:04
6、 x87 指令、3DNow 指令、SSEx 指令
对于 GPIs 来说,这些指令的寻址模式会简单些,但是 Opcode 码的数量却在增加,到现在的 SSE4 指令,3 个实 Opcode 加上 1 个 prefix Opcode,达到了 4 个 Opcode 数量。
6.1、 x87 float 指令集 Opcode 范围从 D8 ~ DF 是 x87 float 指令的 Opcode,实际上 x87 指令的 Opcode 是 2 个字节的,D8 ~ DF 是主字节,ModRM 是补充字节,协助主字节。
因此,它是 Group 属性的 Opcode,比起 GPIs 的 Group 属性指令,x87 的 ModRM 进一步增强协助关系。
x87 指令绝大部分是 float 寄存器(st0~st7)寻址的,实际是与 mmx0~mmx7 寄存器共用物理寄存器。
6.1.1、 x87 float 指令格式
Opcode + XXX = x87's Opcode ------ --- | | | |
D8 ~ DF ModRM.reg |
x87 float 指令的编码序列里,没有 prefix 和 immediate 部分。x87 的 Group 属性 Opcode 由上表格式所示:
D8 ~ DF: 共 8 个 Opcode 被安排为 x87 的 Opcode 码。
ModRM.reg : 这部分是附加的 Opcode 码,辅助 Opcode。
● 在 ModRM.mod = 11 模式下,ModRM.r/m 能提供 8 个不同组合的寄存器寻址(st0 ~ st7),它们全是寄存器寻址。
● 在 ModRM.mod != 11 模式下,ModRM.reg 能提供 8 个不同的 Opcode 码。 此时,x87 指令可以提供内存操作数寻址。
● D8 ~ DF 共 8 个 Opcode 码,理论上可提供 8 * 8 = 64 条 x87 指令。 在 ModRM.mod = 11 和 ModRM.mod != 11 之间提供两组 Opcode 值,所以,x87 的数量理论上可达到 64 ~ 128 条。实际上,x87 float 指令约为 70 条左右。
6.1.2、 看看两条常见的 x87 float 指令编码。(1)fstp st(1) ; 将 st(0) 值复制到 st(1) 到,并置 stack 顶为 st(1)
它是寄存器寻址,ModRM.mod = 11 提供寄存器寻址模式,ModRM.reg = 011 提供 fstp opcode,ModRM.r/m = 001 提供寄存器 ID 值。ModRM 的值是:11-011-001 = D9
所以,这条指令的编码是:
dd d9(2)fstp dword ptr [eax] ; 将 st(0) 值复制到 [eax],并置 stack 顶为 st(1)
它是内存寻址,ModRM.mod = 00 提供无 disp 内存寻址,Mod.reg = 011 提供 fstp mem32 指令的 opcode,Mod.r/m = 000 提供 [eax] 内存寻址。 ModRM 值是:00-011-000
所以,这条指令的编码是:
d9 186.2、 AMD 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 |
举1个例子来示范:
指令
pfcmpge mmx1, qword ptr [eax] 这条指令的 Operand 助记符是:Pq,Qq 实际上与 Gv, Ev 情况一样,只不过这里寄存器由 GPRs 变为 mmx 寄存器,操作数却固定为 64 位。
它的 Opcode 码是:90 也就是 imme 为 90,由 ModRM 的 reg 提供 P 的寻址,r/m 提供 Q 的寻址。故 mod = 00,reg = 001, r/m = 000
所以,这条指令的编码是:
0F 0F 08 906.3、 SSEx 指令 一部分指令是 3 个 Opcode,一部分是 2 个 Opcode,而 3 个 Opcode 则是有一个 prefix 来充当。这是 Intel 设计的编码架构,若是 AMD 设计的编码则极大可能不同,AMD 设计的风格是使用后面的 Imme 来充当第 3 个 Opcode,而 Inte l则是使用 prefix,这里可以看出 AMD 与 Intel 风格的不同。
Intel 的 SSE4 指令集达到了 4 个 Opcode 的规模。真 3 个 Opcode 和 1 个 伪 Opcode(prefix)。
这些指令使用 66h、F2 以及 F3 prefix 作为第 1 个 Opcode,0F 作为第 2 个 Opcode。
SSEx 系列指令发展到 SSE4 数量庞大,编码混乱。具体指令参考 Intel 文档
举 1 个例子:
movntdq xmmword ptr [rax], xmm0 这是一条复制 128 位的指令,从 xmm0 复制到 [rax] 内存上,它采用 66 0F 前导,第 3 个 Opcode 是E7,ModRM 提供寻址,mod = 00,reg = 000,r/m = 000,ModRM 的值是:00
所以,这条指令的编码是
66 0F E7 00
作者: mik
时间: 2008-12-9 00:31
7、 强悍的 AMD SSE5 指令集
AMD 设计 3Dnow 与 SSE5 指令集的目的很明显,想摆 Intel 的制约,在 x86 平台上有属于自已的东西,在 x86 平台上能站稳脚步。特别是 SSE5 指令集的推出,目的想让 Intel 向自己靠。
SSE5 指令集的数量不少,有八九十个之多。实事上AMD推出的 x86_64 的 64 位平滑扩展技术就成功的制约了 Intel,Intel 不得不跟随 AMD 脚步。SSE5 能不能再创辉煌,引导 Intel 向他靠还得拭目以待。
7.1、 AMD SSE5 指令集编码的特点:(1)SSE5 指令 Operands 可以增加到 4 个。这 4 个操作数可以全是寄存器操作数,这是通过增加 DREX 字节来实现的。
(2)SSE5 指令编码有 3 个 Opcode,其中 2 个是引导 Opcode 和 1 个主导 Opcode。
(3)SSE5 指令编码中有 3 个字节来定位寻址操作数。在原有的 ModRM 和 SIB 的基础上,增加了 Drex 字节来寻址。
(4)无需 prefix 以及 REX prefix 进行修饰。
(5)目标操作数固定为寄存器
对比 Intel 的 SSE4 指令集,个人会 AMD 的设计更为优越。SSE5 有 4 个操作数,SSE4 是 2 个操作数。SSE5 取消掉 prefix 的修饰,SSE4 的 Opcode 达到了 4 个之多,虽然 1 个是伪 Opcode(prefix)。
纵观 SSEx 系列指令集,Opcode 布局混乱、指令繁多。没有一个编码统一的机制。若有新的指令集产生,恐怕还得修改编码序列。
编码不统一是由于 CISC 的缘故,从编码的角度来讲,使得任一个 x86 平台生产者都可以肆意添加新的指令集。
关于编码不统一的话题: 对当今的处理器来说,是 RISC 指令集,还是 CISC 指令集? 这已经不重要了。对 CISC 架构的 x86 处理器来说,这些 x86 指令集已经不是原生执行的指令集了。 x86 指令在处理器内部会被解码为微指令行式执行,这些微指令执行单一功能。 AMD 可以在 x86 处理器上添加新的指令集。只要在 GPIs 指令集架构(ISA)层面上保证兼容就行了。 对于新增的指令集,这是只给了解码器更多的解析工作而已。 |
7.2、 SSE5 指令结构 上图是 SSE5 指令编码序列
● 0F 24 和 0F 25 是引导 Opcode,这两个 Opcode 在原来是无效。
● Opcode3 是主导 Opcode,定性 SSE5 的操作
● ModRM 与 SIB 意义和原来一致
● DREX 意即:Dest+REX,DREX.dest 是定义目标操作数,REX 的含义和 REX prefix 一致。
● Displacement 含义和以前一致。
● Immediate 含义有些改动,在 SSE5 指令里只有 1 个字节大小。
7.3、 Opcode3 的结构位 含义
------------------------------------------------------------------
7 ~ 3 Opcode 码
2 Oc1,它与 DREX 的 Oc0 组合起来控制操作数
1 ~ 0 OPS,它定义操纵数据的大小
Opcode: 定义 SSE5 指令的 Opcode 码
OC1 : Opcode3.oc1 与 DREX.oc0 组合起来是 00 ~ 11
OPS : 指令执行时对操纵的数据的大小。
7.4、 DREX 的结构位 含义
------------------------------------------------------------------
7 ~ 4 Dest 域,即目标操作数的 ID 值
3 Oc0,与 Opcode3 的 Oc1 组合起来控制操作数
2 即 REX.R
1 即 REX.X
0 即 REX.B
DREX.dest 是目标操作数的 ID,这个操作数必定是 xmm 寄存器。
DREX.RXB 的含义与 REX prefix 的含义是一致的,用来扩展 xmm8 ~ xmm15 寄存器
7.5、 控制操作数 Opcode3.Oc1+DREX.Oc0 组合共 2 位值控制操作数分配,从 00 ~ 11
对于 4 个操作数的指令来说,例:
fmaddps dest, src1,src2,src3 其控制的操作数分配如下:
值 含义
---------------------------------------------------------------------------------------
00 dest = DREX.dest,src1 = DREX.dest,src2 = ModRM.reg
src3 = ModRM.r/m
01 dest = DREX.dest, src1 = DREX.dest, src2 = ModRM.r/m
src3 = ModRM.reg
10 dest = DREX.dest,src1 = ModRM.reg,src2 = ModRM.r/m
src3 = DREX.dest
11 dest = DREX.dest,src1 = ModRM.r/m,src2 = ModRM.reg
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
作者: mik
时间: 2008-12-9 02:33
8、 指令编码核心之:ModRM 寻址
前面多次提到:
Opcode 对指令提供操作码,ModRM 则对指令提供操作数寻址,SIB 对 ModRM 进行补充寻址。
有两种情况下是无需用 ModRM 提供寻址的:
第一是:操作数是寄存器,它直接嵌入 Opcode 中。
第二是:操作数是立即数,它直接嵌入指令编码中。
|
本节讲解指令核心另一个重点:ModRM 寻址。这个 ModRM 寻址非常重要。是理解 x64 平台上指令 Operands 的关键。
8.1、 ModRM 的含义 ModRM 字节的组成部分为:mod-reg-r/m 三个部分,mod 为 2 位,reg 与 r/m 是 3,组成 2-3-3 的比例,在这整篇文档中的写法是:ModRM.mod、ModRM.reg 以及 ModRM.r/m。
mod 是提供寻址的模式,这个模式以 displacement 值作区别的。reg 是提供寄存器寻址,reg 表示寄存器 ID 值。r/m 提供对内存的寻址中的寄存器 ID 值或寄存器寻址中的 ID 值。
8.1.1、 mod 寻址模式。 2 位组成 4 种寻址模式,总的来说,只有两种寻址模式,就是:内存寻址模式和寄存器寻址模式。mod = 11 时指出寄存器寻址模式,mod = 00 ~ 10 时指出内存寻址模式:
值 含义
-------------------------------------------------------------------------------------
00 [register] 间接寻址,无 displacement 值。
01 [register + disp8],有 8 位 displacemnet 偏移值。
10 [register + disp32],有 32 位 displacement 偏移值。
11 registers 寻址
8.1.2、 reg 寻址寄存器 3 位组成 8 个寄存器 ID 值,从 000 ~ 111,对应于 RAX、RCX、RDX、RBX、RSP、RBP、RSI 以及 RDI。这个 ID 值可以被REX prefix 扩充为 4 位,范围从 0000 ~ 1111 可表示 16 个寄存器。
ModRM.reg 的另一含义是对 Opcode 的补充,对分为一组 Opcode 的进行选择(Group 属性)。 |
ModRM.reg 是提供 registers 寻址。
8.1.3、r/m 寻址 register / memory r/m 意即:registers/memory,提供对 registers 或 memory 的寻址。
000 ~ 111 也是用来表示寄存器 ID 值。当寻址 registers 时是寄存器 ID 值。当寻址 memory 时是寄存器间接寻址中的寄存器 ID 值。当 mod != 11 时,r/m 表示 [rax] ~ [rdi]。mod = 11 时,r/m 表示 rax ~ rdi
REX prefix 用来扩充寄存器ID值。
8.2、 探讨 2 个设计上的问题以及解决之道8.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 指针,设计的语义上是用来指示 stack top,而不是寻址。
经过衡量和考虑,把原来属于 [rsp] 的领域腾空,用于 [SIB] 来实现引导 SIB 字节。 Mod.r/m = 100,这个领域被 [SIB] 替代了。 事实上在 16 位机器原本是没有 SIB 字节的,base+index*scale+disp 这种寻址是后来才增加的。16 位的 ModRM 上是没有 SIB 引导域。 |
8.2.2、 另一种情况是 [disp32] 如果内存寻址中没有 base 和 index,只有 disp 的话,如:
mov ebx, [0x11223344]。对于这种直接寻址方式,在设计上 ModRM 还必须为它提供这种寻址模式。
Intel 又作出修改,选择了原来属于 [rbp] 模式的领域提供给 [disp32]。
选择 [rbp] 让给 [disp32],是因为 rbp 原本意图就是设计为 stack frame 基址指针。[rbp] 寻址一般都要加上一个偏移量,也就是基于stack frame 指针的偏移量。 所以,[rbp] 寻址显得不合逻辑。 |
[rbp + disp] 这种寻址模式是符合逻辑语义。在 ModRM.mod = 01 或 ModRM.mod = 10 中给出了这种寻址模式。那么,最终 ModRM.mod = 00 时,[rbp] 让位给了 [disp32]。
在最终的 ModRM.mod = 00 上, r/m 寻址设计上,如下表格:
r/m 内存寻址
--------------------------------
000 [rax]
001 [rcx]
010 [rdx]
011 [rbx]
100 [SIB] ---- 原本对应 [rsp]
101 [disp] ---- 原本对应 [rbp]
110 [rsi]
111 [rdi]
而在 ModRM.mod = 01 及 ModRM = 10 上,ModRm.r/m = 101,设计为 [rbp + disp8] 及 [rbp + disp32],提供了对 [rbp+偏移量] 的支持,这样也符合原本设计的语义。
8.3、16 位寻址下的 ModRM 在 16 位寻址下是不支持 base+index*scale 这种寻址模式的,这样在编码序列里就无需提供 SIB 字节了。
16 位的 ModRM 寻址支持的是基址和变址寄存器间接寻址和 基址+变址寻址模式。基址寄存器 2 个:就是 bx 和 bp。变址寄存器也是 2 个:就是 si 和 di。 |
基于上述设计,基址+变址寻址的组合只有 4 个,也就是:[bx+si]、[bx+di]、[bp+si] 以及 [bp+di]。它们对应 ModRm.r/m 域就是 000 ~ 011。
那么这 4 个寄存器的间接寻址就是:[si]、[di]、[bp] 以及 [bx],对应于 Mod.r/m 的 100 ~ 111。
同样如上述,bp 是 stack frame 指针,一般使用需加 disp(offset),所以 [bp] 让位给 [disp] 解决直接寻址的问题。
所以,16 位的 ModRM.mod = 00 下,ModRM 最终设计方案是:
r/m 内存寻址
-------------------------------------------------
000 [bx+si]
001 [bx+di]
010 [bp+si]
011 [bp+di]
100 [si]
101 [di]
110 [disp] ------ [bp] 让位给 [disp16]
111 [bx]
ModRM.mod = 01、ModRM.mod = 10 和 11 的情形下如前如述。
8.4、 64 位寻址下的 ModRM 在 64 位下,ModRM 的含义与 32 位一致,改进的只是在原来基础上增加了 8 个 GPRs,通过 REX prefix 进行对新增的寄存器进行访问。
寄存器的 ID 取值为:0000 ~ 1111。由 REX.R 以及 REX.B 位进行扩展访问。
8.5、 结合Opcode来看寻址模式及Opcode的定位 Opcode 定义指令的执行码,用于执行什么操作,对于操作数寻址上,Opcode 结合 ModRM 来定义操作数,这是一个经过反复琢磨推敲的过程,而最终又影响到 Opcode 的定位。
下面讲讲怎么影响到 Opcode 最终定位。
8.5.1、 一个操作数的 Opcode 定位 操作数要么就是 registers,要么就是 memory,要么就是 Immediate 值。如果指令只有一个操作数。
(1)如它是 register 的话,Opcode 是无需 ModRM 配合确定寻址方式的。
记住 1: ModRM 是用于寻址 2 个操作数,这两个操作是必定是寄存器之间,以及寄存器与内存操作数之间的寻址方式。
|
在只有 1 个寄存器操作数的情况下,ModRM 无用武之处。所以,在这种情冲下,寄存器操作数绝大部分是嵌在 Opcode 里面,它由 Opcode 的寄存器域指出。如常见的 inc ecx、dec ecx、push eax 等。
记住 2: 除了上述嵌入 Opcode 情况外,如果 ModRM 用于寻址 1 个操作数,这条指令的 Opcode 必定是:Group 属性。
|
若是 Group 属性,则 ModRM 有用武之地了。
(2)如它是 Immediate 的话,它绝对是无 ModRM。直接将 Immediate 值嵌入指令编码里。
(3)如它是 memory 的话,它绝对是 Group 属性,需要 ModRM.reg 来配合定位。那为什么不能是直接 offset 呢,直接 offset 寻址留给最常用的,最有用的 Opcode,以免 Opcode 占位,浪费资源。
8.5.2、 两个操作数的 Opcode 定位 两个操作数大部分都需 ModRM 配合定位寻址。ModRM 提供的 2 个操作数寻址大有用武之地。
(1)2 个操作数中,其中 1 个是寄存器,另1个不是立即数的这种情况最直接简单,由 ModRM 的 reg 及 r/m 提供寻址。
若其中 1 个是 GPRs 另 1 个是 immediate 或 displacement 的情形下更简单,GPRs 则直接由 Opcode 提供寻址。
(2)2 个操作数中,没有寄存器的情形下,也就是要么是 memory,要么是 Immediate,它必然是个 Group 属性,reg 域提供 Opcode 的定位,r/m 提供内存寻址。Immediate 直接嵌入指令编码中。
8.5.3、 三个操作数的 Opcode 定位 三个操作数中有一个必定是 Immediate,在 AMD 的 SSE5 指令集推出之前,x86 平台是无法提供第 3 个非 Immediate 操作数的定位。
直至 AMD 的 SSE5 通过增加另一个描述操作数的字节来寻址第 3 个操作数。
如上所述,Opcode 的设计要考虑到与 ModRM 的配合。主要表现在什么时候是 Group 属性。
前面已提过: Opcode 的分组是按 Operands 属性进行的。即:这一组 Opcode 拥有相同类型的 Operands。 这个类型的 Operands 主要是单个内存操作数或单个寄存器操作数而非嵌入 Opcode 中。 |
作者: mik
时间: 2008-12-9 11:43
9、指令编码核心之 SIB 寻址
在 ModRM 无法提供更多内存寻址方式时,使用 SIB 进行协助寻址。对内存操作数寻址提供补充定义。SIB 提供的补充内存寻址支持 base + index * scale + displacement 寻址,如:[rax + rcx * 8 + 0x11223344]。
此时,SIB 字节是:scale = 11,index = 001,base = 000,组合起来是 c8
9.1、 SIB 的含义及结构 SIB 意即:Scale – Index – Base,用来定义 base+index*scale+disp 这种寻址模式。同样按 2-3-3 比例组合。
Scale 索引因子的含义:
值 含义
----------------------------------------------------------------------
00 无 scale, 或者:index * 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,可寻址 16 个寄存器。
9.2、 对 ModRM 寻址的补充 前面提到 ModRM 中,ModRM.r/m = 100 时,[esp] 让位给 [SIB] 提供引导 SIB 字节。这种情况下,指令可使用 base+index 的寻址方式。
ModRM.r/m 含义 __ SIB.index 含义 ------------------ / --------------------- ... ... / 000 [rax + base] 100 [SIB] ---> SIB.scale = 00 / 001 [rcx + base] ... ... = 01 \ 010 [rdx + base] = 10 \ 011 [rbx + base] = 11 \ 100 [base] --- 101 [rbp + base] 110 [rsi + base] 111 [rdi + base]
|
SIB 提供了从 [base+index] 到 [base+index*8] 寻址范围,那么这里延续上一节提到的 ModRM 设计上的问题:[rsp] 这种寻址方式将怎么解决呢? 在 ModRM 中抛弃了 [rsp] 的寻址模式,在 SIB 中将得到补救。
在 index = 100 时,按规则应该是 [rsp + base] 这种寻址模式吧?
由于 rsp 的特殊性,rsp 寄存器只能做 base 寄存器,以 rsp 为基址。而不能作为 index 寄存器。所以:[rsp+base] 这种寻址模式中 rsp 被去除,那么将剩余 [base]。
|
故当 index = 100 时,[rsp+base]具体形式将取决于 base。9.2、 对 [rsp] 寻址的补救方式
由于 ModRM 中没提供 [rsp] 寻址,而又确实需要 [rsp] 寻址的话,在 SIB 中通过 index = 100 时提供,[rsp+base] 中去除掉 rsp,留下 [base] 寻址,所以在 base = 100 时就提供了对 [rsp] 的寻址。
|
9.3、 重复编码 在 index = 100,去除 rsp 后,变为 [base] 寻址,那么此时又提供了 [base] 的寻址方式,这样就与 ModRM.mod = 00 时 ModRM.r/m 提供了完全相同的寻址方式。
所不同的是一种由 ModRM.mod = 00 直接给出,另一种通过 ModRM./rm = 100 然后转接到 SIB,再由 SIB.index = 100 时 SIB.base = 100 提供。
Intel 设计的时候已不能顾及这么多了,反正重码也没什么多大的害处,只是麻烦了 processor 的解码单元而已,但这样就显不得够严谨,话说回来,本来 x86 平台指令集就不是十分严谨。?
9.4、 对于 [rbp] 寻址模式的裁减 那么当 index = 100,也就是 [base] 寻址,而 base = 101(rbp)时,按规则此时应该为 [rbp] 寻址是吧?
Intel 又变变花样,应是 [rbp] 时而又不是 [rbp],原因前面已经提过,rbp 的语义应该是 stack frame pointer,以 rbp 为基址加上 offset (displacement)进行寻址,这才符合语义。
前面提到:当 ModRM.r/m = 100,[rsp] 让位给 [SIB],而链接到 SIB 寻址。
而 SIB.index = 100 ([base] 寻址)且 SIB.base = 101(rbp),Intel 对它作出了裁减,去掉 [rbp] 寻址模式。
Intel 为了保证 [rbp + disp] 寻址,两处地方对 [rbp] 作出了裁减: (1) ModRM.mod = 00 & ModRM.r/m = 101,原来对应 [rbp] 变成了 [displacement32] 这种直接寻址模式。
(2) ModRM.r/m = 100(引发[SIB]),SIB.index = 100(变为 [base]),SIB.base = 101(对应 [rbp]),作出了裁减:ModRM.mod = 00 时,SIB = xx-100-101 下,变成了 [displacement32] 寻址。ModRM.mod = 01,SIB(XX-100-101)变成了 [rbp + disp8]。ModRM.mod =10,SIB(XX-100-101),变成了 [rbp + disp32] 寻址。
|
这样,使用 ModRM 寻址和使用 ModRM + SIB 寻址下,保证了 [rbp+disp8] 及 [rbp+disp32] 这两种寻址,去掉了 [rbp] 这种寻址模式。
那么,在汇编语法中 [rbp] 这种寻址,只好变通一点:采用 [rbp + 0x00] 或 [rbp + 0x00000000] 这种形式了,编译器会自动产生这种编码模式。9.5、 解开 SIB 寻址的迷惑1、重码问题 x86 指令的内存寻址有三个地方的重码现象:
(1)[disp32] 寻址方式的重码现象
● ModRM.mod = 00,ModRM.r/m = 101 提供了 [disp32] 寻址。
● ModRM.mod = 00,ModRM.r/m = 100,SIB.index = 100,SIB.base = 101 提供了 [disp32] 重码寻址。
-----------------------------------------------------------------
即
ModRM = 00-XXX-101 与
ModRM = 00-XXX-100 + SIB = XX-100-101 下重码
(2)[base] 寻址方式的重码现象
● ModRM.mod = 00 提供了 [base] 寻址,也就是 [rax] 之类。
● ModRM.mod != 11 & ModRM.r/m = 100 ,SIB.index = 100 提供了 [base] 寻址,导致重码。
-----------------------------------------------------------------------------------
即:
ModRM = 00-XXX-XXX 与
ModRM = 00-XXX-100 + SIB = XX-100-XXX(不含101)下重码
(3)[ebp+disp8] 与 [ebp+disp32] 的重码现象
● ModRM.mod = 01 和 10,ModRM.r/m = 101,提供了相应的 [ebp+disp8] 和 [ebp+disp32] 寻址。
● ModRM.mod 01 和 10,ModRM.r/m = 100。
SIB.index = 100,SIB.base = 101,提供了相应的 [ebp+disp8] 和 [ebp+disp32]
---------------------------------------------------------------------------------------------------------------
即:
ModRM = 01-XXX-101 与
ModRM = 01-XXX-100 + SIB = XX-100-101 都提供了 [rbp+disp8]
ModRM = 10-XXX-101 与
ModRM = 10-XXX-100 + SIB = XX-100-101 都提供了 [rbp+disp32]
2、不支持 rsp 作为 index 寄存器 因 rsp 设计为 stack top 指针,而不支持 [rsp+base] 这种寻址方式。 rsp 只能作为 base 寻址,即:[rsp]。 故 [rsp+base] 被去除 rsp,只剩下 [base],而引发重码问题。
3、 SIB 同样不支持 [rbp] 寻址方式 须作为 stack frame pointer 加偏移量的寻址方式。这符合设计语义。程序中使用 [rbp] 则要变为:[rbp+disp8] 或者 [rbp + disp32] 这种方式。
SIB 和 ModRM 回绕在一起,保证不提供 [rbp] 寻址。
.
[
本帖最后由 mik 于 2008-12-9 11:54 编辑 ]
作者: mik
时间: 2008-12-9 18:01
10、 Displacement 与 Immediate
Displacement 与 Immediate 是嵌入指令编码中的。在编码组合序列里 Displacement 先于 Immediate。
Displacement 与 immediate 最大 4 个字节。可是在 64 位 Long 模式下,一些应用场合下 displacement 与 immediate 可以是 8 个字节。
Immediate 是符号数;大部分情况下 displacement 是符号数。可是在直接内存寻址下 displacement 不是符号数。
10.1、 displacement Displacement 意即基于基址的偏移量,意同 offset。常见于基址+disp 的形式。
10.1.1、 displacement 的符号数 在两种寻址模式下,displacement 是符号值,有符号数的语义。
(1)基址+displacement 的寻址模式下 这种寻址是使用 ModRM 进行,相对于基址的偏移量。这时 displacement 是符号数,当 displacement 为 8 位时,会符号扩展至 32 位。
典型的指令如:lea eax, [ebp - 0x0c]
(2)相对转移寻址 这种寻址是基于 rip(instruction pointer)的相对寻址,这时 displacement 是个基于 rip 的符号数偏移量。如:call Jz 和 Jmp Jz 等。同样,displacement 为 8 位时,会符号扩展至 32 位来计算结果。
典型的指令如:jmp $+0xc(短跳转)
相反,在直接内存寻址下,displacement 不是符号值,它不含符号数的语义。能提供直接内存寻址有两种方式:
(1)基于 ModRM 寻址中的 [disp32] 寻址 在 ModRM = 00-XXX-101 能提供直接内存寻址。或者:ModRM = 00-XXX-100 + SIB = 00-100-101 也能提供直接内存寻址。
典型的编码是:
c7 04 25 44 33 22 11 01 00 00 00 它的指令是:
mov dword ptr [0x11223344], 1(2)寻址模式直接嵌入 Opcode 中的情况 另一种是在 Opcode 中就提供直接内存寻址模式。这种 Opcode 为数少,典型的编码是:
a1 44 33 22 11 它的指令形式是:
mov eax, [0x11223344]10.1.2、 64 位的 displacement 值
64 位 Long 模式下,只有一种情况 displacement 是 64 位。前面已经提到:采用嵌在 Opcode 中的直接内存寻址,displacement 是可以为 64 的,仅止而已。 |
mov rax, Ov 这条指令中它的内存寻址属性字符是 Ov,它是不依赖于 ModRM 的直接内存寻址。
典型的编码是:
48 a1 88 77 66 55 44 33 22 11 指令形式是:
mov rax, qword ptr [0x1122334455667788] 它仅能对 rax 寄存器寻址提供 64 位的 displacement 值。
10.2、immediate Immediate 相对简单多了,采用 immediate 寻址的 Opcode 要么就是嵌入 Operand,要么就是 Group 属性的 Opcode。
Immediate 是个符号数,32 位下 imme8 会符号扩展到 32 位。64 位下会符号扩展到64位。
10.2.1、采用 Immediate 的寻址模式(1)、 采用 Immediate 单 Operand 的指令寻址模式 只有 1 个操作数下,Immediate 寻址模式在 Opcode 嵌入寻址模式,典型的如:push Iv 或 push Ib 指令,仅此而已。
(2)、两个以上的 Operands 的 Immediate 寻址模式
一种是与 GPRs 之间的寻址,如指令:mov eax,0x10 这条指令的目标操作数是 rax,源操作数是 Iv。其中 rax 的寻址已嵌入到Opcode中。
它的编码是:
b8 10 00 00 00 还有就是与 mem 之间的寻址,如指令:mov dword ptr [eax], 1 这种指令是 Group 属性的指令。
10.2.2、 64 位 Immediate 值
64 位下,只有与 GPRs 之间的传送,才可能是 64 位,仅止而已。
|
但 push Iv 和 push Ib 这类 push 指令它的缺省操作数是 64 位的。虽然指令编码不能有 64 位 immediate 值,但它会符号扩展至 64 位,其结果是 64 位的。
记住: 1、64 位下 displacement / immediate 与 GPRs 之间的寻址才可能是 64 位,仅止而已。 2、displacement 含有偏移的语义时,它是符号数。它是直接内存寻址时,它是非符号数;immediate 则是符号数。 |
x86 & x64 平台的指令编码已经讲解完毕。
作者: mik
时间: 2008-12-9 22:44
11、 解析指令
现在,回头看看序言里的两个例子,应该怎样解析呢?
1、 mov word ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678分析:
(1) 这条指令有两个操作数,目标操作数是 mem 寻址。源操作数是 imme 寻址。
(2) 目标操数的大小是 word,源操作数的大小是 0x12345678,这是一个 32 位值。这样出现了操作数不匹配,大部分编译器会将 imme 截断为 16 位,即结果为:imme = 0x5678。
最后,两个操作数属性是: mem16, imme16
(3) 这条指令是 mov,那么就有: mov mem16, imme16, 是典型的:mov Ev, Iv 具 Group 属性
所以:Opcode = c7
(4) 分析 mem16 中, eax 是 base 寄存器,ecx 是 index 寄存器,8 是 scale,还有一个 32 位 displacement 值。
那么其 SIB.scale = 11 (8)、SIB.index = 001(ecx)、SIB.base = 000(eax),disp = 0x11223344
SIB = c8
(5)现在来看看 ModRM 字节怎么来分解。
SIB 需要 ModRM 引导出来,ModRM = 10-XXX-100,ModRM.mod = 10 是选择 [SIB+disp32] 寻址
关键是 ModRM.reg 的值是多少?
(6) mov Ev, Iv 是 Group 属性,ModRM.reg = 000 时,分配给 mov Ev, Iv 这个 Opcode 码
最终 ModRM = 10-000-100 = 84
--------------------------------------------------------------------------------------------------
现在,基本的编码已经出来了:
c7 84 c8 44 33 22 11 78 56 由于 x86 平台是 little-endian 机器,所以 disp 与 imme 要按 little-endian 排列。
但是,这个 encode 还不是最终的,因为操作数需要调整大小和段,因此还要加上 26h 和 66h,
所以最终的 encode 是:
26 66 c7 84 c8 44 33 22 11 78 562、 解析 encode:FF 15 D4 81 DF 00(1) 提取第 1 个字节 FF ,这是个 Group 属性的 Opcode。
(2) 提取第 2 个字节 15,这必定是个 ModRM 寻址。
它是结构是:00-010-101 = 15。 mod = 00、reg = 010、r/m = 101
(3) 分析 ModRM 字节:
mod = 00:将提供 [base] 寻址
reg = 010:将对补充 Opcode 码,在 FF 里选择第 3 列,它是 call Ev 指令
r/m = 101:提供 [disp32] 直接内存寻址模式。
(4) 由以上第 3 点分析得出:ModRM 字节后是个 32 位的 displacement 值。
也就是:D4 81 DF 00 是个 displacement,且是个直接内存寻址。
所以内存操作数是:[0x00DF81D4]
同样,在这里要注意 little-endian 排列。
(5) 最终这个 encode 的指令形式是:call [0x00DF81D4]
那么,在这里需要注指令格式的问题:在 32 位下 call 指令的操作数必然是 32 位。eip = [0x00DF81D4]。所以对有些编译器来说,可以不用加 Operand-Size 修饰符。而 Microsoft 的编译器则需要加上
call dword ptr [0x00DF81D4],对于 Microsoft 的编译器来说,这样还不够。还需要明确指定段。
对 Microsoft 的编译器来说,最终形式是
call dword ptr ds:[0x00DF81D4] 对 nasm 来说:call [0x00DF81D4] 已经可以了,对 nasm 来说 [0x00DF81D4] 是缺省 DS 段
到此,这篇 【x64 指令编码内幕】 已接近尾声。
有机会再探讨更高级的应用话题,包括:assembler(汇编器)、disassembler(反汇编器) 以及 decoder(解码器)。