1. x86指令的一般格式
上述是一条X86的一般格式,其中除了opcode(操作码)必定出现之外, 其余组成部分可能不出现, 对于某些组成部分, 其长度并不是固定的。其中Opcode 对指令提供操作码,ModRM 最主要作用是对指令的 operands 提供寻址,另一个作用是对 Opcode 进行补充,而 SIB 则是对 ModRM 进行补充寻址。给定一条具体指令的二进制形式, 其组成部分的划分是有办法确定的, 不会产生歧义(即把一串比特串看成指令的时候, 不会出现两种不同的解释). 例如对于以下指令:
1 |
|
其组成部分的划分如下:
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 prefix 与 escape 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 表是怎样的确定具体的指令:
由上面图中的 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为例 :
- G 是 Operand type,表示 General-Purpose Register(GPR)通用寄存器,也就是 rax ~ r15 共 16 个。这是有别与 Segment Register、XMM 寄存器等。
- 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
- J 是代表基于 RIP 的相对寻址,也就是说,操作数寻址是偏移量(Offset)加上 RIP 得出。
- 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