4. 指令 Opcode 码
x86/x64 通用指令编码的核心是:Opcode,ModRM 以及 SIB
因此:指令编码设计模式是 Opcode 的设计要考虑兼顾 ModRM,ModRM 要服务于 Opcode,SIB 是对 ModRM 的补充辅助。
关于 opcode 的组成结构,请详见《x86/x64 指令编码构造》一文:http://www.mouseos.com/x64/encode.html
在 1 个字节的空间里:00 - FF,Prefix 与 Opcode 共同占用这个空间。
由于 x86/x64 是 CISC 架构,指令不定长。解码器解码的唯一途径就是按指令编码的序列进行解码,关键是第 1 字节是什么? 如:遇到 66h,它就是 prefix,遇到 89h,它就是 Opcode。
Prefix 与 Opcode 共享空间的原因是:Prefix 是可选的。在编码序列里,只有 Opcode 是不可缺少的,其它都是可选。这就决定了指令编码中的第 1 个字节对解码工作的重要性。 |
除了 1 个字节的 Opcode 外,还有 2 个字节的 Opcode 以及 3 个字节的 Opcode,第 2 个 Opcode 码是由 0F 字节进行引导,这个 0F 被称为 escape prefix(escape opcode)即:2 个字节的 Opcode 码,其第 1 个 Opcode 必定是 0F 字节。
x86/x64 平台上的 3 个字节的 Opcode 码是通过 escape opcode + opcode 形式。
这些 escape opcode 可以理解为:opcode 的 prefix,这些 prefix 可以说是 opcode 的一部分。
这些 escape opcode 是:
3 个字节的 Opcode 用于 SIMD 指令上(SSE 系列指令),然而随着 AMD/Intel 在未来有可能加入更多的指令子集,意味着可能会产生更多的 escape opcode(已经被集成在 VEX prefix 或 XOP preix 中)
在多数 SIMD 指令上,SIMD prefix 与 escape opcode 联合起来最终决定指令的 opcode
这些 SIMD prefix 包括:
在这种情况下,Opcode 码将达到 4 个 bytes,下面是一个 4 bytes Opcode 码的例子:
它是 SSE4.1 的一条 BLENDPS 指令,SIMD prefix 与 escape opcode 结合为 4 个 bytes Opcode 码
--- |
值 |
说明 |
escape opcode |
0f |
引导 opcode |
0f 38 |
||
0f 3A |
||
SIMD prefix |
66 |
SIMD 指令修饰性 prefix |
F3 |
||
F2 |
movntdq xmmword ptr [rax], xmm0 |
这是一条 SSE2 指令,它 encodes 是:
它的 Opcode 是 66 0f e7 (3 bytes opcode),这里 66 是 SIMD prefix,0f 是 escape prefix
怎么去看指令的 opcode,获得指令 opcode 编码,还是很有学问的
下面的图是 AMD 文档中对于指令参考页的描述:
从指令参考页里可以得出以下信息:
这确实可以得到想要的 Opcode 码,还有 Operand 数以其属性。下面摘录了 mov 指令一部分的参考页:
从这里看出,这个 Opcode 8B 有几种操作数形式:
operand size 可以为:
不过这并不是了解 Opcode 码的好地方,指令参考页主要是对指令的操作进行相应的描述。对掌握 Opcode 码不是那么直观和透彻。下面要看全局的 Opcode 表格。
Intel 和 AMD 的文档中均提供了 Opcode 表,Opcode 表有 One-byte Opcode 表、Two-byte Opcode 表和 X87 Opcode 表等。
下图是一部分 Opcode 表
Opcode 表上描述的范围是 00 ~ FF,即 1 个字节共 256 个值,每 1 个值描述不同的属性,包括:
每个 Opcode 码还附有相应的 Operands 属性,Operands 属性是用来描述 Operands 的,包括 Operands 个数、寻址类型及 Size
注意: 这主要原因是原因:这种 Opcode 的操作数无法与 ModRM 得到良好的配合。从而决定了 Opcode 受制于 ModRM.reg。 实际上: |
看看 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 进行寻址。
2.2.2 operand 描述表
要看懂 Opcode 表必须学会分析和理解 Operand 属性字符,Intel 和 AMD 的 Opcode 表前面都有对 Operands 属性字符很仔细清晰的定义和说明。
类型 |
描述 |
寻址方式 |
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 中无需给出 |
类型 |
描述 |
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) | |
每个指令的 operand 属性都由上面的表1和表2两者描述, Operands 属性都有两组字符来定义,前面的一组大写字母是 Operand 类型,后面一组小定字母是 Operand Size。
如:Gv 表示:
★ G 是 Operand 类型,表示 General-Purpose Register(GPR)通用寄存器,也就是 rax ~ r15 共 16 个。这是有别与 Segment Register、XMM 寄存器等。
★ v 是 Operand 大小,依赖于当前的 Effective Operand-Size,这个 operand size 可以使用 66H prefix 和 REX prefix 进行 operand size override
举 2 个例子:
(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 寻址。
E 既可是 GPRs 也可以是 Mem。 它同样是 v 属性的 Operand-Size。
关于 default 与 effective 阐述, 详见: default(缺省) 与 effective(有效)
(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.2、 2 个 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 值)
思考另一个问题: 在 64 位下:mov qword ptr [rax],0x1122334455667788,这指令是的错误的。 原因:这条指令是 MOV Ev, Iz,它是 z 属性的 operand size, 因此,它不可能有 64 位的 immediate 值。 本质上: 是它的寻址是使用了 ModRM 的。此时,它要受限于 Immediate 最大为 4 个字节的限制。所以不会有 Iv 的属性 |
(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 等。
3.3、 3 个 Operands 的 Opcode 编码
在 AMD 的 SSE5 指令集推出之前,是没有第 3 个 Operand 是非寄存器或内存操作数的情形。所以,第 3 个操作必定是 Immediate 值。
这种指令很少。imul eax, ebx, 3 这是其中的一种形式。
4、 Opcode 混乱的本质
造成 x86/x64 平台的 Opcode 混乱,起因于众多寻址模式,而 operand 分为:0 operand、1 operand、2 operands、3 operands 以及 4 operands
Group 属性的 Opcode
由于这部分 Opcode 需要 ModRM.reg 进行补充,那么 ModRM.r/m 就可以提供 1 个 operand 的寻址了。
当 ModRM.mod = 11,它就提供 registers 寻址,当 ModRM.mod <> 11,它就提供 memory 寻址,
因此,它的 operand 要么是 Ev 要么是 Iv、Iz 或 Ib
当 operand 是 Iv、Iz 或 Ib 时,表示 Opcode 将忽略 ModRM.r/m 寻址。