1. 程式人生 > >X86指令編碼內幕 --- 指令 Opcode 碼

X86指令編碼內幕 --- 指令 Opcode 碼

指令 Opcode 碼

x86 指令編碼的核心是:Opcode、ModRM 以及 SIB。Opcode 提供指令的操作碼,ModRM 及 SIB 提供運算元的定址模式。

指令編碼設計模式是:Opcode 的設計要考慮兼顧 ModRM。ModRM 要服務於 Opcode,SIB 是對 ModRM 的補充輔助。 

1.初窺 Opcode

在 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。即:2 個位元組的 Opcode 碼,其第 1 個 Opcode 必定是 0F 位元組。

1.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 指令)

1.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

例子:movntdq xmmword ptr [rax], xmm0

這條指令是 SSE2 指令,它 encodes 是:66 0f e7 00

它的 Opcode 是 66 0f e7 (3 bytes opcode),這裡 66 是 SIMD prefix,0f 是 escape prefix

2 學會看 Opcode 表

怎麼去看指令的 opcode,獲得指令 opcode 編碼,還是很有學問的。

2.1 從指令參考頁裡看 opcode 碼

下面的圖是 AMD 文件中對於指令參考頁的描述:

從指令參考頁裡可以得出以下資訊:

(1)指令助記符 mnemonic
(2)指令的 Operand 屬性
(3)指令的 Opcode 碼
(4)指令的描述。

這確實可以得到想要的 Opcode 碼,還有 Operand 數以其屬性。下面摘錄了 mov 指令一部分的參考頁:

從這裡看出,這個 Opcode 8B 有幾種運算元形式,reg <- regreg <- mem,Operand-size 可以是 16/32/64。

不過這並不是瞭解 Opcode 碼的好地方,指令參考頁主要是對指令的操作進行相應的描述。對掌握 Opcode 碼不是那麼直觀和透徹。下面要看全域性的 Opcode 表格。

2.2 怎麼看 Opcode 表

學會看 Opcode 表才能清晰地進行分析,Opcode 表是一個全面的透徹的總結表,又可以說十分細緻。

Intel 和 AMD 的文件中均提供了 Opcode 表,Opcode 表有 One-byte Opcode 表、Two-byte Opcode 表和 X87 Opcode 表等。

2.2.1 Opcode 表上的基本元素

下圖是一部分 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 的屬性進行分組的。

看看 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 屬性字元很仔細清晰的定義和說明。

表格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)

每個指令的 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。

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 等。

3.1、 1 個 Operand 的 Opcode 碼編碼規則


(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 的屬性

  mov rax, qword ptr [0x1122334455667788],這條指令是完全正確的,它的指令是 MOV rAX, Ov, 具有 v 屬性的 size

  它不依賴於 ModRM 定址。此時,它的 memory 是絕對地址,它將以 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 等。



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 定址。