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 <- reg、reg <- 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
注意: 這主要原因是原因:這種 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 屬性字元很仔細清晰的定義和說明。
表格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 的屬性 |
(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 定址。