x86 使用可變長度指令。一條指令可包含舊式前綴、opcode、ModR/M 位元組、 scale-index-base(SIB)位元組、位移量及立即值。只有 opcode 必定存在:
長模式可在舊式前綴與 opcode 之間加入 REX 前綴。VEX、XOP 與 EVEX 編碼會為較新的 指令家族取代此配置的部分內容。無論採用哪種形式,架構上的 x86 指令都不得超過 15 個位元組。
這些欄位並不是組合語言 token 的直接序列化。opcode 可編碼運算元的一部分, ModR/M 欄位可擴充 opcode,而位址運算式會選擇處理器的一種定址形式,並非要求 執行一連串加法與乘法。
操作與運算元之間沒有固定邊界 #
組合語言語法會呈現指令助記碼,後接零個或多個運算元:
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 位元模式中,這些指令編碼為:
ff 05 00 00 00 00 inc dword [0]
ff 0d 00 00 00 00 dec dword [0]兩者都使用 opcode 位元組 FF,但 ModR/M 位元組不同:
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, eax | 90 |
xchg eax, ecx | 91 |
xchg eax, edx | 92 |
xchg eax, ebx | 93 |
xchg eax, esp | 94 |
xchg eax, ebp | 95 |
xchg eax, esi | 96 |
xchg eax, edi | 97 |
指令表將此家族寫成 90+rd。相較之下,xchg ebx, ecx 使用 opcode 87 加上一個
ModR/M 位元組:
87 d9 xchg ebx, ecx其他 +r 家族包括 PUSH、POP、帶立即值的 MOV,以及 BSWAP。暫存器形式的
單位元組 INC 與 DEC 只會在 64 位元模式以外使用位元組 40 至 4F;長模式
將這些位元組值指定給 REX 前綴。
組合語言位址運算式選擇編碼欄位 #
[eax + ebx*4 + 0x32] 這類運算式不會編碼成算術指令。它要求組譯器選擇
ModR/M、SIB 與位移量欄位,讓位址產生硬體計算此運算式。
例如:
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、indexEBX、baseEAX。 32是 8 位元位移量。
因此,相同的書面助記碼會依運算元採用不同長度與欄位配置。
舊式前綴 #
舊式前綴分為四個語意群組。一條指令最多可使用每個群組的一個有效前綴。編碼器 不應輸出同一群組的重複前綴:不同處理器對冗餘或衝突前綴的行為並不一致。
| 群組 | 位元組 | 用途 |
|---|---|---|
| 1 | F0, F2, F3 | LOCK、重複,或強制 opcode 選擇 |
| 2 | 2E, 36, 3E, 26, 64, 65 | 區段覆寫;歷史上的分支提示 |
| 3 | 66 | 運算元大小覆寫或強制 opcode 選擇 |
| 4 | 67 | 位址大小覆寫 |
雖然舊式解碼不要求所有群組依數字順序出現,組譯器仍會採用慣用的群組順序。 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 欄位中編碼對應的選擇。
區段覆寫與分支提示 #
區段覆寫位元組如下:
| 位元組 | 區段 |
|---|---|
2E | CS |
36 | SS |
3E | DS |
26 | ES |
64 | FS |
65 | GS |
在 16 與 32 位元模式中,覆寫會變更記憶體運算元使用的區段。在 64 位元模式中,
CS、DS、ES 與 SS 的基底視為零,因此通常只有 FS 與 GS 覆寫會影響
位址形成。
部分較舊的處理器會將條件分支上的 2E 與 3E 解讀為靜態不採用及採用提示。
現行軟體不應依賴這些提示。
運算元大小覆寫 #
對適用的指令,66 會選擇非預設的運算元大小。在 32 位元程式碼區段中,它通常
會在 32 與 16 位元運算元之間切換:
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:
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 位元 |
|---|---|---|---|---|
000 | EAX | AX | AL | - |
001 | ECX | CX | CL | - |
010 | EDX | DX | DL | - |
011 | EBX | BX | BL | - |
100 | ESP | SP | 使用 REX 時為 SPL | 不使用 REX 時為 AH |
101 | EBP | BP | 使用 REX 時為 BPL | 不使用 REX 時為 CH |
110 | ESI | SI | 使用 REX 時為 SIL | 不使用 REX 時為 DH |
111 | EDI | DI | 使用 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 形式,或先將位址載入
暫存器。
解碼器會分階段選擇有效位址形式:
16 位元位址大小使用由 BX、BP、SI 與 DI 組成的獨立表格,且不含 SIB
位元組。在該位址大小中,由 mod=10 選擇的位移量為 16 位元。
SIB 位元組 #
在 32 與 64 位元位址大小形式中,記憶體 ModR/M 的 r/m=100 會選擇一個 SIB
位元組:
有效位址為:
base + index * scale + displacementscale 欄位的編碼如下:
| 位元 | Scale |
|---|---|
00 | 1 |
01 | 2 |
10 | 4 |
11 | 8 |
若沒有擴充位元,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 的值:
| 位元 | 功能 |
|---|---|
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 特有的規則。
例如:
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、indexRCX及 baseRBX。 20是位移量。
VEX 與 EVEX #
REX 擴充舊式一般用途編碼,但向量指令家族需要更多狀態。VEX 會編碼 opcode map、 強制前綴選擇、向量長度、暫存器擴充及額外的來源暫存器欄位。常見 VEX 形式使用 兩個或三個位元組。
EVEX 會加入更多暫存器位元,以及遮罩、歸零、廣播、捨入與更大向量長度的控制項。
部分 EVEX 記憶體運算元使用壓縮位移量縮放,因此編碼的 disp8 會先乘以由 tuple
決定的因子,再進行位址計算。
VEX 與 EVEX 會重複使用在較舊模式中具有其他意義的位元組值。模式與前綴有效性 規則讓處理器得以區分這些形式。應將它們解碼為完整的前綴結構,而非逐一對每個 位元組套用舊式前綴規則。
實用解碼程序 #
依相依順序解碼指令:
- 確立執行模式、程式碼區段預設值及已啟用的架構擴充。
- 取用有效的舊式前綴。
- 在適用時識別 REX、VEX、XOP 或 EVEX 形式。
- 選擇 opcode map 與 opcode。
- 查閱 opcode 定義,判斷是否需要 ModR/M。
- 若 ModR/M 選擇記憶體,且在支援 SIB 的位址大小中
r/m=100,則解碼 SIB。 - 根據位址大小、
mod與特殊欄位值判斷位移量寬度。 - 根據 opcode 與有效運算元大小判斷立即值寬度。
- 套用暫存器擴充欄位及指令特有的語意規則。
沒有任何位元組可自我描述。例如,66 可以是運算元大小覆寫、強制 opcode
選擇器,或間接編碼於 VEX/EVEX 狀態。因此,可靠的解碼需要處理器手冊中的指令表。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)