《X86编码-指令格式与解析一》

1. x86指令的一般格式

image-20210306153510237

​ 上述是一条X86的一般格式,其中除了opcode(操作码)必定出现之外, 其余组成部分可能不出现, 对于某些组成部分, 其长度并不是固定的。其中Opcode 对指令提供操作码,ModRM 最主要作用是对指令的 operands 提供寻址,另一个作用是对 Opcode 进行补充,而 SIB 则是对 ModRM 进行补充寻址。给定一条具体指令的二进制形式, 其组成部分的划分是有办法确定的, 不会产生歧义(即把一串比特串看成指令的时候, 不会出现两种不同的解释). 例如对于以下指令:

1
100017:    66 c7 84 99 00 e0 ff ff 01 00      movw   $0x1,-0x2000(%ecx,%ebx,4)

其组成部分的划分如下:

image-20210306153740655

2. Prefix

​ 在一条指令的第 1 个字节的空间里:00 ~ FF,Prefix 与 Opcode 共同占用这个空间。由于 x86/x64 是 CISC 架构,指令不定长。解码器解码的唯一途径就是按指令编码的序列进行解码, 如:遇到 66h,它就是 prefix,遇到 89h,它就是 Opcode。

Prefix能与Opcode公用空间的原因是:

​ Prefix 是可选的。在编码序列里,只有 Opcode 是不可缺少的,其它都是可选。这就决定了指令编码中的第 1 个字节对解码工作的重要性。

Prefix前缀的由来:

​ i386是从8086发展过来的. 8086是一个16位的时代, 很多指令的16位版本在当时就已经实现好了. 要踏进32位的新时代, 兼容就成了需要仔细考量的一个重要因素. ​ 一种最直接的方法是让32位的指令使用新的操作码, 但这样1字节的操作码很快就会用光. 假设8086已经实现了200条16位版本的指令形式, 为了加入这些指令形式的32位版本, 这种做法需要使用另外200个新的操作码, 使得大部分指令形式的操作码需要使用两个字节来表示, 这样直接导致了32位的程序代码会变长. 现在你可能会觉得每条指令的长度增加一个字节也没什么大不了, 但在i386诞生的那个遥远的时代(你可以在i386手册的封面看到那个时代), 内存是一种十分珍贵的资源, 因此这种使用新操作码的方法并不是一种明智的选择. ​ Intel想到的解决办法就是引入操作数宽度前缀, 来达到操作码复用的效果. 当处理器工作在16位模式(实模式)下的时候, 默认执行16位版本的指令; 当处理器工作在32位模式(保护模式)下的时候, 默认执行32位版本的指令. 当某些需要的时候, 才通过操作数宽度前缀来指示操作数的宽度. 这种方法最大的好处就是不需要引入额外的操作码, 从而也不会明显地使得程序代码变长. 虽然在NEMU里面可以使用很简单的方法来模拟这个功能, 但在真实的芯片设计过程中, CPU的译码部件需要增加很多逻辑才能实现.

​ 除了 1 个字节的 Opcode 外,还有 2 个字节的 Opcode 以及 3 个字节的 Opcode,第 2 个 Opcode 码是由 0F 字节进行引导,这个 0F 被称为 escape prefix。即:2 个字节的 Opcode 码,其第 1 个 Opcode 必定是 0F 字节。

2.1 escape prefix

x86/x64 平台上的 3 个字节的 Opcode 码是通过 escape prefix + opcode 形式。

这些 escape prefix 可以理解为:引导性的 prefix,这些 prefix 可以说是 opcode 的一部分。

这些 escape prefix(引导 prefix)是:

  • 0F
  • 0F 3A
  • 3F 38

3 个字节的 Opcode 用于 SIMD 指令上(SSE1 ~ SSE4 系列,AVX 指令以及 XOP 指令)

2.2 SIMD prefix

在大多数 SIMD 指令上,SIMD prefixescape prefix 联合起来。

这些 SIMD prefix 包括:

  • 66
  • F2
  • F3

1.3 escape prefix 与 SIMD prefix 总结

说明
escape prefix 0f 引导 opcode
0f 38    
0f 3A    
SIMD prefix 66 SIMD 指令修饰性 prefix
F3    
F2    

3. Opcode

Opcode描述的范围是 00 ~ FF,即 1 个字节: 256 个值,其中每 1个值描述不同的属性,包括:

  • 1 个 byte 的 opcode 码

  • prefix

  • 0F(escape prefix)

  • Group 属性的 opcode 码

    每个 Opcode 码还附有相应的 Operands 属性,Operands 属性是用来描述 Operands 的,包括 Operands 个数、寻址类型及 Size。

​ 所谓Group的属性 Opcode 是指:Intel 将一些 operands 寻址相同 Opcode 码抽出来组成一组。具体的功能是由 ModRM 的 reg 来决定。ModRM.reg 就起决定性作用,它反过影响 Opcode 码。ModRM.reg 此时相当是一个 index 值。用来选择 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 码具体对应的内容可在i386手册【1】 上查阅。下面以部分表的内容为例介绍 mov 指令 8B 如何利用 Opcode 表是怎样的确定具体的指令:

image-20210310162747064

​ 由上面图中的 opcode 表中所示 Opcode 8B,对应的是指令 mov,表格中的 Gv, Ev 是描述这个 Opcode 码所对应的指令的 Operand 属性。

Gv, Ev 表示:

(1)两个 Operands 分别是:目标操作数 Gv,源操作数 Ev 或说:frist operand 是 Gv, second operand 是 Ev

(2)Gv 表示:G 是寄存器操作数,v 是表示操作数大小依赖于指令的 Effective Operand-Size,可以是 16 位,32 位以及 64 位。

(3)Ev 表示:E 是寄存器或者内存操作数,具体要依赖于 ModRM.r/m,操作数大小和 G 一致。

4 个字符便可以很直观的表示出:操作数的个数以及寻址方式,更重要的信息是这个 Opcode 的操作数需要 ModRM 进行寻址。

3.1 Operand 描述表

​ 由上面的mv指令的例子可知,想确定mov所对应的 Gv, Ev 的内容,还需要分析和理解 Operand 属性字符。这部分内容也可在【1】上查阅。每个指令的 operand 属性都由operand type 和operand size两者描述, 其中前面的大写字母代表的是 Operand type,后面小写字母代表的是 Operand Size。以Gv为例 :

  1. G 是 Operand type,表示 General-Purpose Register(GPR)通用寄存器,也就是 rax ~ r15 共 16 个。这是有别与 Segment Register、XMM 寄存器等。
  2. v 是 Operand size,依赖于当前的 Effective Operand-Size,这个 operand size 可以使用 66H prefix 和 REX prefix 进行 operand size override

下面给出这两部分内容的参考表格:

表格1:operand type 表

类型 描述 寻址方式
A operand 是一个 far pointer(selector:offset 形式) 以 immediate 形式直接在 encode 里给出。(offset 在低,seletor 在高)
C operand 是一个 control register(CR0 ~ CR15) 由 modrm.reg 提供寻址。
D operand 是一个 debug register(DR0 ~ DR15) 由 modrm.reg 提供寻址。
E operand 是一个 register 或者 memory 由 modrm.r/m 提供寻址。
F operand 是 rflags 寄存器 直接嵌在 opcode 里。
G operand 是一个通用寄存器 (rax ~ r15) 由 modrm.reg 提供寻址。
I operand 是立即数 immediate encode 中的 immediate 形式。
J operand 是基于 rip 的 offset(偏移量),是 signed(符号数) encode 中的 immediate 形式。
M operand 是 memory 操作数 由 modrm.r/m 提供寻址,其中 modrm.mod ≠ 11(它是 memory)
O operand 是 memory offset,直接提供绝对地址 encode 中的 immediate 形式,无需 modrm 和 SIB 寻址。
P operand 是 MMX 寄存器 由 modrm.reg 提供寻址。
PR operand 是 MMX 寄存器 由 modrm.r/m 提供寻址,其中 modrm.mod = 11
Q operand 是 MMX 寄存器或者 memory 操作数 由 modrm.r/m 提供寻址。
R opernad 是一个通用寄存器(rax ~ r15) 由 modrm.r/m 提供寻址,其中 modrm.mod = 11
S opernad 是 segment 寄存器 由 modrm.reg 提供寻址。
V operand 是 XMM 寄存器 由 modrm.reg 提供寻址。
VR operand 是 XMM 寄存器 由 modrm.r/m 提供寻址,其中 modrm.mod = 11
W opernad 是 XMM 寄存器或者 memory 操作数 由 modrm.r/m 提供寻址。
X operand 是串指令的源串 default operand 寻址 由 ds:rsi 提供寻址,在 encode 中无需给出。
Y operand 是串指令目的串 default operand 寻址 由 es:rdi 提供寻址,在 encode 中无需给出

表格2:operand size 表

类型 描述 operand size  
a 仅用于 bound 指令,operand 是一个 memory,它提供 array 的 limit(下限地址和上限地址) word 或者 doubleword,依赖于 effective operand size(可以进行 operand size override)  
b operand size 固定为 byte,不可进行 operand size override byte (8 位)  
d operand size 固定为 doubleword,不可进行 operand size override doubleword (32 位)  
dq operand size 固定为 double-quadword,不可进行 operand size override double-quadword (128 位)  
p operand 是 far pointer:32 位或 48 位(16:16 或 16:32) 16:16 或 16:32 依赖于 effective operand size(可进行 operand size override)  
pd packed double(128 位双精度浮点压缩数),即:64:64(128 位 packed double) 128 位 packed-double。  
pi MMX - packed integer(64 位压缩整数) 64 位 packed-integer。  
ps packed signed(128 位单精度压缩数),即:32:32:32:32(128 位 packed signed) 128 位 packed-signed。  
q operand size 固定为 quadword,不可进行 operand size override quadword(64 位)  
s 6 bytes 或 10 bytes 的描述符表类型(limit + base) 16:32(16/32 位 opernad siz)或 16:64(64 位 operand size)  
sd scalar double scalar double  
si scalar integer scalar integer  
ss scalar signed scalar signed  
v word,doubleword 或 quadword word,doubleword 或 quadword 取决于 effective operand size,可进行 operand size override,REX prefix  
w opernad size 固定为 word,不可进行 operand size override word(16 位)  
z word = effective operand size 是 16 位时 word 或 doubleword 依赖于 effective operand size
dword = effective operand size 是 32 位或 64 位时    
/n n 代表一个具体数值(0 ~ 7),在 ModRM.reg 中提供 由 ModRM.reg 提供 (000 ~ 111)  

3.2 补充两个例子:

为了更好的理解如何通过Opcode一步步得到一条确定的指令下面补充两个例子。

(1)以典型的 Jmp Jz 为例。

​ 它的 Opcode 是 E9,Operand 属性是 Jz

  1. J 是代表基于 RIP 的相对寻址,也就是说,操作数寻址是偏移量(Offset)加上 RIP 得出。
  2. z 则表示 Operand-Size 是 16 或 32(effective operand size = 16 时是 16,effective operand size = 32/64 时是 32)这与 v 不同,z 属性下不存在 64 位 operand size

(2)另一个典型的例子是 call Ev

​ 它的 Opcode 是 FF,这个 Opcode 是个典型的 Group Opcode,为什么会定义为 Group,下面的将会有阐述。操作数的寻址是典型的 ModRM 寻址,由 ModRM.r/m 寻址。

​ 1. E 既可是 GPRs 也可以是 Mem。 它同样是 v 属性的 Operand-Size。

关于 default 与 effective 阐述, 详见: default(缺省) 与 effective(有效)

3.3 Opcode 的编码规则

​ 如上所述:prefix 与 Opcode 共享 00~FF 的空间,由于 Prefix 部分是 可选的,当 CPU 取指单元从 ITLB 加载指令 并开始解析指令时。如读入的第一个字节是66h时是解析为 prefix 而不是 Opcode。同样,读入 0Fh 时被解析为是 2 个 字节的 Opcode 中的第 1 个字节。

​ 其中Opcode的operands 寻址一部分是在 Opcode 码直接中指定,一部分是依赖 ModRM 给定,还有一部分不依赖 ModRM 给定。直接中 Opcode 指定的 operand 寻址的是 GPRs 寻址,如:inc eax 指令(Opcode 是 40h),还有串指令,如 loads 等。 **(1)直接嵌入 Opcode 中 **

​ 一部分 Opcode 的 operand 是直接嵌入 Opcode 中的,如:inc eax、push eax、pop eax 等。 这些指令编码是 1 个字节。不依赖于 ModRM 寻址。

(2)依赖于 ModRM 寻址,这部分 Opcode 码需要 ModRM.reg 进行补充修饰,是 Group 属性 Opcode

​ 对于单 Operand 的指令而又依赖于 ModRM 寻址。这种指令必定是 Group 属性的指令。 它的 operand 属性是 Ev 字符。ModRM.reg 决定最终的 Opcode 操作码,ModRM.mod 和 ModRM.r/m 决定寻址模式。 ​ 如前面提到的典型 Call Ev 这种指令,operand 可以是 register,也可以是 memory,由 ModRM.mod 来决定到底是 registers 还是 memory,ModRM.r/m 决定具体的 operand。

(3)不依赖于 ModRM 寻址,不是 Group 属性 Opcode

​ 这种情况下的 Operand 既不嵌入 Opcode 中,也不使用 ModRM 进行寻址,那么它必定是 immediate 值。它的 Operand 属性字符是 Iz、Ib 或者 Jz。

这种指令很常见,如:push Iz、push Ib、Jmp Jz、Jmp Jb 等。 push 0x12345678 这就是常见的这种指令,还非常常见的短跳转 jmp $+0x0c。

3.3.1 两个Operands的Opcode 码编码规则

(1)1 个 Operand 嵌入 Opcode,另一个 Operand 不依赖于 ModRM(非 Group 属性)

​ 这种情况下,一个Operand必定是 GPRs,另一个不使用 ModRM 寻址的 Operand 必定是 Immediate。所以它不是 Group 属性的。

看看以下两个 Opcode:

​ 指令 mov rax, Iv 它的 Opcode 是 B8,目标操作数是由 Opcode 中指定的 GPRs(rax),源操作数是不使用 ModRM 寻址的 Immediate。是一个寄存器与立即数的寻址指令。由于 mov rax, Iv 它的 immediate operand size 是 v 属性,因此:这个 immediate 可以使用 REX prefix 进行 override 到 64 位

如:指令 mov rax, 0x1122334455667788

它的 encode 是:b8 88 77 66 55 44 33 22 11 (使用了 64 位的 immediate 值)

(2)依赖于 ModRM 寻址,非 Group 属性

这 种依赖于 ModRM 寻址而又非 Group 属性的 2 个 Operands,绝大部分是:寄存器与内存操作数之间或 2 个寄存器之间。 它的 Operands 属性字符是 Gv, Ev 或 Ev, Gv。

典型的如: mov eax, ebx

(3)依赖于 ModRM 寻址,是 Group 属性

在这种 Opcode 编码下,另一个操作数必定是 Immediate,典型的如:mov ecx, 0x10,它的 Operands 属性字符是 Ev, Iv 等。

Reference

【1】https://nju-projectn.github.io/i386-manual/appa.htm

【2】https://nju-projectn.github.io/ics-pa-gitbook/ics2020/i386-intro.html

【3】https://blog.csdn.net/xfcyhuang/article/details/6230542