x86 與 x86-64 指令編碼

最後更新: 2022-04-22 00:33:12 +0800 +0800
tags: Assembly | X86 | X86-64
原文以英文撰寫。您正在閱讀AI翻譯版本。

x86 使用可變長度指令。一條指令可包含舊式前綴、opcode、ModR/M 位元組、 scale-index-base(SIB)位元組、位移量及立即值。只有 opcode 必定存在:

legacyprefixesopcodeModR/MSIBdisplacementimmediate

長模式可在舊式前綴與 opcode 之間加入 REX 前綴。VEX、XOP 與 EVEX 編碼會為較新的 指令家族取代此配置的部分內容。無論採用哪種形式,架構上的 x86 指令都不得超過 15 個位元組。

這些欄位並不是組合語言 token 的直接序列化。opcode 可編碼運算元的一部分, ModR/M 欄位可擴充 opcode,而位址運算式會選擇處理器的一種定址形式,並非要求 執行一連串加法與乘法。

操作與運算元之間沒有固定邊界 #

組合語言語法會呈現指令助記碼,後接零個或多個運算元:

nasm
xchg eax, ecx
inc  dword [eax]
mov  edx, [ecx + 0x32251212]

二進位編碼不會為助記碼保留一個欄位,再為每個運算元保留個別欄位。x86 使用數種 緊湊的編碼策略。

ModR/M Opcode 擴充 #

許多指令會將三位元 ModR/M reg 欄位用作額外的 opcode 位元。Intel 與 AMD 語法 將這些形式描述為 /0 至 /7。例如,FF /0 是 INC r/m32,而 FF /1 是 DEC r/m32。

在 32 位元模式中,這些指令編碼為:

text
ff 05 00 00 00 00    inc dword [0]
ff 0d 00 00 00 00    dec dword [0]

兩者都使用 opcode 位元組 FF,但 ModR/M 位元組不同:

text
05 = 00 000 101    mod=00, opcode extension=/0, r/m=101
0d = 00 001 101    mod=00, opcode extension=/1, r/m=101

在 32 位元位址大小下,mod=00、r/m=101 表示僅含位移量的位址。四個零位元組 就是該絕對 disp32。在這些編碼中,reg 欄位無法指定暫存器,因為它用來選擇 操作。

嵌入 Opcode 的暫存器 #

部分 opcode 家族會將暫存器編號嵌入最後一個 opcode 位元組的低三位元。指令表依 暫存器寬度將此形式描述為 +rb、+rw、+rd 或 +rq。

例如,在 32 位元模式中,單位元組 opcode 90 至 97 會將 EAX 與八個 32 位元 一般用途暫存器之一交換:

組合語言Opcode
xchg eax, eax90
xchg eax, ecx91
xchg eax, edx92
xchg eax, ebx93
xchg eax, esp94
xchg eax, ebp95
xchg eax, esi96
xchg eax, edi97

指令表將此家族寫成 90+rd。相較之下,xchg ebx, ecx 使用 opcode 87 加上一個 ModR/M 位元組:

text
87 d9    xchg ebx, ecx

其他 +r 家族包括 PUSH、POP、帶立即值的 MOV,以及 BSWAP。暫存器形式的 單位元組 INC 與 DEC 只會在 64 位元模式以外使用位元組 40 至 4F;長模式 將這些位元組值指定給 REX 前綴。

組合語言位址運算式選擇編碼欄位 #

[eax + ebx*4 + 0x32] 這類運算式不會編碼成算術指令。它要求組譯器選擇 ModR/M、SIB 與位移量欄位,讓位址產生硬體計算此運算式。

例如:

text
ff 44 98 32    inc dword [eax + ebx*4 + 0x32]

各位元組分解如下:

  • FF /0 選擇 INC r/m32。
  • ModR/M 44 為 01 000 100:記憶體定址、/0,後接 SIB。
  • SIB 98 為 10 011 000:scale 4、index EBX、base EAX。
  • 32 是 8 位元位移量。

因此,相同的書面助記碼會依運算元採用不同長度與欄位配置。

舊式前綴 #

舊式前綴分為四個語意群組。一條指令最多可使用每個群組的一個有效前綴。編碼器 不應輸出同一群組的重複前綴:不同處理器對冗餘或衝突前綴的行為並不一致。

群組位元組用途
1F0, F2, F3LOCK、重複,或強制 opcode 選擇
22E, 36, 3E, 26, 64, 65區段覆寫;歷史上的分支提示
366運算元大小覆寫或強制 opcode 選擇
467位址大小覆寫

雖然舊式解碼不要求所有群組依數字順序出現,組譯器仍會採用慣用的群組順序。 REX 前綴則不同:存在時,它必須位於舊式前綴之後,並緊接在 opcode 或 opcode map 跳脫碼之前。

鎖定與重複前綴 #

F0 是 LOCK 前綴。它只適用於目的地為記憶體的特定讀取—修改—寫入指令。有效 的鎖定操作對其他邏輯處理器而言具有原子性。現代處理器通常透過快取一致性及保有 受影響快取列的獨占所有權來取得此性質,不會為每條 LOCK 指令宣告外部匯流排鎖。 跨界存取與不可快取的記憶體可能需要更強的處理。對不支援的指令或暫存器目的地 使用 LOCK 會引發 #UD。

對字串操作而言,F3 表示 REP 或 REPE/REPZ,F2 則表示 REPNE/REPNZ。 計數值來自 CX、ECX 或 RCX,由位址大小選擇。確切停止條件取決於指令。

對數種 SSE 及後續指令家族而言,F2、F3 或 66 是強制 opcode 選擇前綴, 而非重複或大小操作。相同的 opcode map 位元組可依該前綴識別不同指令。VEX 與 EVEX 會在其 pp 欄位中編碼對應的選擇。

區段覆寫與分支提示 #

區段覆寫位元組如下:

位元組區段
2ECS
36SS
3EDS
26ES
64FS
65GS

在 16 與 32 位元模式中,覆寫會變更記憶體運算元使用的區段。在 64 位元模式中, CS、DS、ES 與 SS 的基底視為零,因此通常只有 FS 與 GS 覆寫會影響 位址形成。

部分較舊的處理器會將條件分支上的 2E 與 3E 解讀為靜態不採用及採用提示。 現行軟體不應依賴這些提示。

運算元大小覆寫 #

對適用的指令,66 會選擇非預設的運算元大小。在 32 位元程式碼區段中,它通常 會在 32 與 16 位元運算元之間切換:

text
ff 05 00 00 00 00       inc dword [0]
66 ff 05 00 00 00 00    inc word  [0]

此前綴會變更 INC 載入及儲存的整數寬度,不會變更此範例中的 32 位元位址位移量。 運算元大小與位址大小彼此獨立。

在 64 位元模式中,預設整數運算元大小通常為 32 位元。66 選擇 16 位元,而 REX.W 對支援這些大小的指令選擇 64 位元。個別 opcode 定義可能制定不同規則。

位址大小覆寫 #

67 會選擇非預設的位址大小。在 32 位元程式碼區段中,它可選擇 16 位元定址。 在 64 位元模式中,它會選擇 32 位元而非 64 位元定址;長模式不提供 16 位元位址 大小編碼。

位址大小決定 ModR/M 與 SIB 使用哪些暫存器及定址表,也會選擇字串指令的計數 暫存器。它本身不會變更載入、儲存或計算之值的寬度。

Opcode Map #

舊式編碼使用數個 opcode map:

  • 單位元組 map 包含 0F 跳脫碼以外的 opcode。
  • 0F xx 選擇雙位元組 map。
  • 0F 38 xx 與 0F 3A xx 選擇額外的三位元組 map。

opcode 可包含方向、寬度、條件碼或暫存器的欄位。是否後接 ModR/M、位移量及立即值 欄位,由指令手冊決定,而非只由 opcode 位元組數量決定。

VEX 與 EVEX 前綴會編碼 opcode map 選擇器,而不直接寫出舊式 0F、0F 38 或 0F 3A 位元組。它們也會編碼強制前綴狀態、向量長度、額外暫存器位元及來源 運算元。因此,其表面上的前綴位元組屬於獨立編碼形式,而不是一連串彼此獨立的 舊式修飾符。

ModR/M 位元組 #

ModR/M 分為 mod、reg 與 r/m:

7mod65r4eg32r1/m0

opcode 定義 reg 的意義。它可以指定運算元暫存器,或提供 /0 至 /7 的 opcode 擴充。mod 與 r/m 欄位則選擇暫存器或記憶體定址形式。

mod在 32 與 64 位元位址大小下的一般意義
00記憶體,通常不含位移量
01記憶體,含符號延伸的 disp8
10記憶體,含 disp32
11暫存器運算元;不進行記憶體定址

特殊的 r/m 與 SIB 值會修改 mod=00 規則。

暫存器形式 #

當 mod=11 時,r/m 會指定暫存器。若 reg 是運算元欄位,它會指定另一個 暫存器。若沒有擴充位元,三位元代碼選擇如下:

代碼32 位元16 位元低 8 位元舊式高 8 位元
000EAXAXAL-
001ECXCXCL-
010EDXDXDL-
011EBXBXBL-
100ESPSP使用 REX 時為 SPL不使用 REX 時為 AH
101EBPBP使用 REX 時為 BPL不使用 REX 時為 CH
110ESISI使用 REX 時為 SIL不使用 REX 時為 DH
111EDIDI使用 REX 時為 DIL不使用 REX 時為 BH

運算元大小與指令類別決定代碼識別的是一般用途、MMX、向量、控制、除錯或其他 暫存器類別。REX、VEX 與 EVEX 擴充位元可加入編號更高的暫存器。

記憶體形式 #

當 mod 不等於 11 時,r/m 會選擇記憶體定址形式。在 32 位元位址大小下:

  • 多數 r/m 值會選擇基底暫存器。
  • r/m=100 選擇後接的 SIB 位元組。
  • mod=00, r/m=101 選擇只由 disp32 組成、沒有基底的位址。
  • mod=01 加入符號延伸的 disp8。
  • mod=10 加入 disp32。

在 64 位元位址大小下,mod=00, r/m=101 改為選擇 RIP + sign-extended disp32。RIP 相對定址可對附近的程式碼與資料進行位置獨立 參照。絕對位址可能需要其他編碼策略,包括無基底的 SIB 形式,或先將位址載入 暫存器。

解碼器會分階段選擇有效位址形式:

Mmbnooaodds-R,eb/raM/oosmtrehesfrpoerrcmr/i/maml=100sS,IiB,bmodselectsdi0s,pl8a,ceomren3t2

16 位元位址大小使用由 BX、BP、SI 與 DI 組成的獨立表格,且不含 SIB 位元組。在該位址大小中,由 mod=10 選擇的位移量為 16 位元。

SIB 位元組 #

在 32 與 64 位元位址大小形式中,記憶體 ModR/M 的 r/m=100 會選擇一個 SIB 位元組:

7scal6e5in4dex32ba1se0

有效位址為:

text
base + index * scale + displacement

scale 欄位的編碼如下:

位元Scale
001
012
104
118

若沒有擴充位元,index=100 表示沒有 index。在長模式中,擴充位元可選擇額外的 暫存器。對 mod=00 而言,base=101 表示沒有 base,且必須有 disp32;當 mod=01 或 10 時,相同的 base 位元會選擇 EBP 或 RBP,並使用 mod 所指定的位移量。

因此,即使不需要縮放 index,使用 ESP 或 RSP 作為 base 仍需要 SIB 位元組。 組譯器會隱藏這項表示細節。

位移量與立即值 #

位移量參與有效位址計算。它是否存在及其編碼寬度取決於定址形式:

  • disp8 會符號延伸至作用中的位址大小。
  • disp16 出現在指定的 16 位元定址形式中。
  • disp32 用於 32 位元形式,以及長模式中的基底相對或 RIP 相對形式。

立即值是嵌入指令的運算元值。其寬度由 opcode 決定,而非目的地寬度的直接結果。 範例包括:

  • 對 16 或 32 位元運算元使用符號延伸 imm8 的算術運算;
  • 對 64 位元運算元使用符號延伸 imm32 的算術運算;
  • 可攜帶完整 64 位元立即值的 MOV r64, imm64;
  • 計數值為隱含值 1、暫存器 CL 或編碼 imm8 的位移指令。

位移量與立即值以 little-endian 儲存。反組譯器必須先知道 opcode 與模式,才能判斷 一個欄位在哪裡結束、下一個欄位從哪裡開始。

長模式 REX 編碼 #

單位元組 REX 前綴的形式為 0100WRXB,占用 40 至 4F 的值:

706150403W2R1X0B
位元功能
W對支援的指令選擇 64 位元運算元
R擴充 ModR/M reg 欄位
X擴充 SIB index 欄位
B擴充 ModR/M r/m、SIB base 或嵌入 opcode 的暫存器

當指令使用受支援的 64 位元運算元、參照 R8 至 R15,或需要位元組暫存器 SPL、BPL、SIL 或 DIL 時,就需要 REX 前綴。它必須緊接在 opcode 之前。 置於 REX 位元組之後的舊式前綴,會使該位元組無法成為指令的有效 REX 前綴。

任何 REX 前綴都會使該指令無法使用 AH、BH、CH 與 DH。其舊式編碼與 REX 引入的低位元組暫存器代碼重疊。

長模式的預設值並非全都是 64 位元:

  • 位址大小預設為 64 位元,67 選擇 32 位元。
  • 多數整數指令預設使用 32 位元運算元。
  • 66 通常選擇 16 位元運算元。
  • 在有定義時,REX.W 選擇 64 位元運算元。
  • 堆疊、分支與固定寬度指令具有 opcode 特有的規則。

例如:

text
48 8b 44 8b 20    mov rax, [rbx + rcx*4 + 0x20]
  • 48 是 REX.W。
  • 8B 是 MOV r64, r/m64。
  • ModR/M 44 選擇記憶體運算元、目的暫存器 RAX、一個 SIB 位元組及 disp8。
  • SIB 8B 選擇 scale 4、index RCX 及 base RBX。
  • 20 是位移量。

VEX 與 EVEX #

REX 擴充舊式一般用途編碼,但向量指令家族需要更多狀態。VEX 會編碼 opcode map、 強制前綴選擇、向量長度、暫存器擴充及額外的來源暫存器欄位。常見 VEX 形式使用 兩個或三個位元組。

EVEX 會加入更多暫存器位元,以及遮罩、歸零、廣播、捨入與更大向量長度的控制項。 部分 EVEX 記憶體運算元使用壓縮位移量縮放,因此編碼的 disp8 會先乘以由 tuple 決定的因子,再進行位址計算。

VEX 與 EVEX 會重複使用在較舊模式中具有其他意義的位元組值。模式與前綴有效性 規則讓處理器得以區分這些形式。應將它們解碼為完整的前綴結構,而非逐一對每個 位元組套用舊式前綴規則。

實用解碼程序 #

依相依順序解碼指令:

  1. 確立執行模式、程式碼區段預設值及已啟用的架構擴充。
  2. 取用有效的舊式前綴。
  3. 在適用時識別 REX、VEX、XOP 或 EVEX 形式。
  4. 選擇 opcode map 與 opcode。
  5. 查閱 opcode 定義,判斷是否需要 ModR/M。
  6. 若 ModR/M 選擇記憶體,且在支援 SIB 的位址大小中 r/m=100,則解碼 SIB。
  7. 根據位址大小、mod 與特殊欄位值判斷位移量寬度。
  8. 根據 opcode 與有效運算元大小判斷立即值寬度。
  9. 套用暫存器擴充欄位及指令特有的語意規則。

沒有任何位元組可自我描述。例如,66 可以是運算元大小覆寫、強制 opcode 選擇器,或間接編碼於 VEX/EVEX 狀態。因此,可靠的解碼需要處理器手冊中的指令表。

參考資料 #

評論

在 GitHub 上閱讀或評論(發表評論需要登入)