小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

Linux兼容內(nèi)核論壇 ? 查看主題 - 【x64 指令系統(tǒng)】之指令編碼內(nèi)幕

 jijo 2009-05-05

目  錄
-------------------------------------------------------------------------------
1、序言     -------------- 1 樓
2、指令格式     -------------- 2 樓
3、深入了解 prefix       -------------- 3、4 樓
4、64 位計算 ------------- 5 樓
5、指令編碼核心之 Opcode   ------------- 6 樓
6、x87 指令、3DNow 指令、SSEx 指令 -------------- 7 樓
7、強悍的 AMD SSE5 指令 --------------- 8 樓
8、指令編碼核心之 ModRM 尋址 --------------- 9 樓
9、指令編碼核心之 SIB 尋址 ---------------- 10 樓
10、Displacement 與 Immediate ---------------- 11樓
11、解析指令 (完結(jié)) ---------------- 12 樓
--------------------------------------------------------------------------------



1、序言




在講解 x64 指令編碼之前,先給 2 個例子看看,相當于學(xué)習(xí) C 語言經(jīng)典的第一節(jié)課。

main()
{
  printf(“hello,world!”);
}





1、匯編代碼譯為機器碼


例子1:在當前 32 位機器,32 位系統(tǒng)下,有如下匯編指令:

mov word ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678

分析這條匯編碼:
  這是一條 mov 指令,目標操作數(shù)是 mem, 源操作數(shù)是 imme, 

注意:我特地將操作數(shù)的大小定為是word(2個字節(jié)),而不是 dword
源操作數(shù)故意定為 0x12345678,這個 dword 大小的立即數(shù)。


對應(yīng)的機器編碼是:26 66 c7 84 c8 44 33 22 11 78 56

現(xiàn)在,我對這個機器碼略為解釋一下:

26:在指令序列里是:prefix 部分,作用是調(diào)整內(nèi)存操作數(shù)的段選擇子
66:在指令序列里是:prefix 部分,作用是調(diào)整操作數(shù)的缺省大小
C7:在指令序列里是:Opcode 部分,是 mov 指令是操作碼
84:在指令序列里是:ModRM 值,定義操作數(shù)的屬性
C8:在指令序列里:SIB 值定義內(nèi)存操作數(shù)的屬性
44332211:在指令序列里是: displacement 值
7856:在指令序列里是:immediate 值
-----------------------------------------------------------------
對于多數(shù)編譯器,立即數(shù) 0x12345678 會被截斷,只取低 16 位值。要么就是編譯器拒絕支持。



至于為什么會譯為這個機器編碼,在以后的章節(jié)里再學(xué)習(xí)




2、將機器碼譯為匯編碼


例2:隨便找一個機器碼如:FF 15 D4 81 DF 00

粗略分析一下:

FF:這個字節(jié)是個具有 Group 屬性的 Opcode 碼,它進行什么操作需要依賴于 ModRM 字節(jié)的 Reg 域.。換句話來說,F(xiàn)F 并不是完整獨立的 Opcode 碼,它要聯(lián)合 ModRM 才能確定具體的操作。

15:這個是 ModRM 字節(jié),Mod 域為 00 Reg 域為 010 RM 域為 101。 其中 Reg 域被 FF 作為確定具體操作碼的參考。

FF / 010 :最終確定為:Call 指令,
Mod 域以及RM域確定操作數(shù)的屬性,這是一個內(nèi)存操作數(shù)是且是個 offset 值或者說是 displacement 值。


所以,這個機器碼最終被解碼為: call dword ptr [00DF81D4]



這 2 個例子,作為對學(xué)習(xí) x86 指令編碼的一個感性認識。下面逐一剖析 x86 指令編碼的來龍去脈。
2、指令格式



在序言里的例子里:

mov dword ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678

  這里稍作修改:將 word ptr 這個內(nèi)存操作數(shù)指示字改回 dword ptr,這是個具有典型指令編碼意義的指令。

它的encode(機器編碼)是:26 c7 84 c8 44 33 22 11 78 56 34 12 (共12個字節(jié))。

go ahead~




1、編碼序列




如上圖所示:
  這個x86_x64 體系的 General-Pupose Instruction(通用體系指令)的編碼格式,記住這個編碼序列很重要,這是解析指令編碼的基石。

這個編碼序列分為 Legacy Prefix、REX prefix、Opcode、ModRM、SIB、Displacement 以及 Immediate 7個部分。

按功能組別,我將這個指令序列分為 4 個部分:Prefix、Opcode、ModRM/SIB、Disp/Imme



● Prefix(前綴):
  AMD推出 x86 擴展 64 位技術(shù)時,增加了一個用于擴展訪問 64 位數(shù)據(jù)的 REX prefix,而 x86 的 prefix 是 Legacy prefix。
  在 x86 模式下,REX prefix 是無效的。但是在 x64 的 64 位下 Legacy prefix 是有效的。

● Opcode(操作碼):
  大多數(shù)通用指令 Opcode 是單字節(jié),最多是 2 字節(jié),但是對有些 Float 指令和 SSEx midea 指令來說是 3 個字節(jié)的。

● ModRM/SIB:
  ModRM 字節(jié)實際意義為:mod-reg-rm,按 2-3-3 比例劃分字節(jié),SIB 意即:Sacle-Index-Base 也是按 2-3-3 比例劃分字節(jié)。 這兩個字節(jié)用來修飾指令操作數(shù)。

● Disp/Imme:
  Displacement 最大可為 8 個字節(jié) 64 位,當然 8 個字節(jié)的 displacment 只有在 x64 平臺下的某些情況才會有,displacement 可理解為 offset。同樣 immediate 大可為8個字節(jié),同樣在 x64 下的某些情況才會有的。
  需要注意的一點是:displacement 和 immediate 都是符號數(shù)(single),在 32 位下,小于 32 位被符號擴展至 32 位,在 64 位下,小于 64 位會被符號擴展 64 位。


對照上面的 encode 來看:
26 c7 84 c8 44 33 22 11 78 56 34 12

(1) 26 是 prefix,這是 segment-override prefix,指明是 ES 段選擇子
(2) c7 是 Opcode,表明這個指令是 mov reg/mem, imme
(3) 84 是 ModRm,即:10-000-100。
(4) c8 是 SIB,即:11-001-000
(5) 44332211 是 disp,是 32 位 displacement 值
(6) 78563412 是 imme,是 32 位 immediate 值



2、指令長度

圖中顯示,指令長度最長是 15 個字節(jié),在什么時候達到飽和的 15 個字節(jié)呢?

答案是像這條指令:lock mov dword ptr es:[eax+ecx*8+0x11223344], 0x12345678
當在 16 位下,這條指令將達到飽和的 15 個字節(jié)長度。
注意,僅在 16 位下,這條指令的編碼是:26 66 67 F0 C7 84 C8 44 33 22 11 78 56 34 12 (正好 15 個字節(jié))

2.1、這個編碼的具體含義
26 66 67 F0: 這 4 個字節(jié)是 prefix,這 4 個字節(jié)達到了飽和的 prefix 狀態(tài)。
26 是 ES segment register
66 是 operand-size override
67 是 address-size override
F0 是 Lock prefix
C7:Opcode
84:ModRM
C8:SIB
44 33 22 11:displacement
78 56 34 12:immediate

有沒有超過 15 個字節(jié)的指令編碼,答案是:沒有! 那么在 64 位下呢? 答案同樣是沒有!


勘誤:
  以前,由于作為演示如何達到 15 個字節(jié)長度飽和狀態(tài),而忽視了 lock 用在 mov 指令上是無效的。
  經(jīng)網(wǎng)友指出 dxcnjupt 實驗指出,謝謝。

更正:
  mov 指令是屬于 load - store 類指令。lock 用在 mov 指令上會引發(fā) #UD 異常。
  lock 應(yīng)用于 read-modify-write 類指令上。意即:指令執(zhí)行會產(chǎn)生中間結(jié)果,運算后再 write 內(nèi)存。



那么,將上面的例子改為:lock add dword ptr es:[eax+ecx*8+0x11223344], 0x12345678

對應(yīng)編碼為:26 66 67 F0 81 84 C8 44 33 22 11 78 56 34 12 (共 15 個字節(jié))



既然這樣,順便提一提:

2.1、 為什么指令長度最長是 15 字節(jié)?

 ?。?)4 個字節(jié)的 prefix 已經(jīng)達到飽和度了:1 個字節(jié)用來調(diào)整 segment,1 個字節(jié)用來調(diào)整 Operand-Size,1 個字節(jié)用來調(diào)整 Address-Size,還有 1 個字節(jié)用來 lock 總線。已經(jīng)無法再增加 prefix 了。

 ?。?)若是采用 2 個字節(jié)的 Opcode 碼,則尋址模式上不會有 mem32, imm32 這種操作法。所以還是采用 1 個字節(jié)的 Opcode,而得到 imm32 4 個字節(jié)的立即數(shù)。

  (3)ModRM + SIB:2 個字節(jié)。
 ?。?)4 個字節(jié)的 displacement 值。
  (5)4 個字節(jié)的 immediate 值。

這樣每個組成部分都呈飽和狀態(tài),加起來總共 15 個字節(jié),而只有采用 mem32, imme32 這種尋址模式可能會達到飽和狀態(tài)。

  在 64 位下,若采用 mem, imme 的尋址模式,這和 32 位是一致的,所以不會超越 15 個字節(jié),
3、深入了解 Prefix



  在 GPI(General-Purpose Instruction)指令里,Legacy Prefix 在整個編碼序列里起了對內(nèi)存操作數(shù)進行修飾補充作用,在這里我稱呼它為 x86 prefix,這樣比較直觀。
  x86 prefix 主要起了三個作用:調(diào)整、加強、附加。REX prefix 只是起將操作擴展 64 位的作用。
  要徹底了解 x86 prefix,必須清楚了解 3 個很重要的上下文環(huán)境:缺省 operand-size 和缺省 addess-size 環(huán)境、編譯器上下文環(huán)境以及當前執(zhí)行上下文環(huán)境。


1、調(diào)整改變操作數(shù)

  x86 指令編碼會根據(jù)上面提到的 3 個上下文環(huán)境而對操作數(shù)的位置、大小以及地址進行調(diào)整改變。這里操作數(shù)特指是內(nèi)存操作數(shù)。出現(xiàn)調(diào)整的情形,這是因為:
(1)指令的操作數(shù)大小可以為:8位、16位、32 位以及 64 位
(2)操作數(shù)的位置因段選擇子而不同。
(3)操作數(shù)的地址大小可以為:16 位、32 位以及 64 位


1.1、調(diào)整操作數(shù)的大小(66H prefix ------ Default Operand-Size Override)

  66h 這個 prefix 作用是改變操作數(shù)的大小,那么:

1.1.1 為什么需要改變操作數(shù)大???
  原因是:16 位下代碼需要訪問 32 位數(shù)據(jù)或者 32 位代碼需要訪問 16 位數(shù)據(jù)。

看看這兩個例子:
例 1 :在 16 位代碼下,指令 mov eax, ebx
  由于在 16 位下,操作數(shù)的大小缺省是 16 位的,如上指令要訪問 32 位的寄存器,那么需要進行調(diào)整。
變?yōu)椋?6 89 d8

例 2:在 32 位代碼下,指令 mov ax, bx
  由于在 32 位下,操作數(shù)的大小缺省是 32 位的,如上指令要訪問 16 位寄存器,那么需要進行調(diào)整。
變?yōu)椋?6 89 d8
---------------------------------------------------------
  這里有些人會覺得奇怪,為什么例1 與 例2 編譯器生成的結(jié)果是一樣的。這就是在不同的環(huán)境里,processor 解碼單元會譯為不同的操作結(jié)果。



1.1.2、 根據(jù)什么來改變?
  根據(jù)什么來改變。這就是根據(jù)上面提到過的 3 個很重要的上下文環(huán)境:
● 缺省操作數(shù)大小
● 編譯器編譯環(huán)境
● 當前執(zhí)行環(huán)境

這 3 個環(huán)境是有機結(jié)合起來的,是個整體。


1.1.3、缺省操作數(shù)大?。―efault Operand-Size)
   對于實模式環(huán)境下,操作數(shù)的 Default Operand-Size 是16位,在 32 位保護模式下,操作數(shù)的 Default Operand-Size 是 32位,在 64 位 Long 模式下 Default Operand-Size 也是 32 位大小。
  當在保護模式下,讀取 16 位值時,則須作出調(diào)整,同樣在實模式下,讀取 32 位值時,測須作出調(diào)整。

實際上:
  Default Operand-Size 在實模式下是 16 位這沒錯! 但是,在 32 位保護模式下,Default Operand-Size 并非一定就是 32 位!
  保護模式下,Default Operand-Size 依賴于當前 code segment-descriptor 的 D 位,也就是 CS.D(code segment register's D)。當 CS.D = 1 時,Default Operand-Size 是 32 位的,CS.D = 0 時,Default Operand-Size 是 16 位的。
  這也就是,為什么在 64 位 long 模式下,Default Operand-Size 還是 32 位而不是 64 位的根本原因,CS 中只有 0 與 1 來進行 16 位與 32 位的選擇,而沒有 64 位的選擇!那為什么不設(shè)為 CS.D = 1 時是 64 位呢? 原因很簡單:為了平滑無縫地運行 32 位代碼。這是 x86_64 設(shè)計的根本原因!
  為什么在實模式下,Default Operand-Size 不能進行選擇呢? 因為,實模式下根本沒有進行設(shè)置的途徑。原因是沒有 segment descriptor 進行設(shè)定。
 
更深入一點:
  其實,在實模式下,還是有手段進行設(shè)置 Default Operand-Size 為 32 位的?!£P(guān)于這點,以后有機會再講解。



以下舉2個例子加以說明:

例1:在 32 位保護模式下,指令:mov ax, [11223344h]

  在 Microsoft 的語法里,在內(nèi)存操作數(shù)前一般要加指示字 word ptr,指明操作數(shù)的大?。簃ov ax, word ptr [11223344h] 實際上,在這條指令里,這個指示字不是必須的,加指示字只是比較直觀。但有些情況是必須要加的,如:mov dword ptr [11223344], 1
例 1 這條指令里,絕大多數(shù)編譯器會編譯為以下機器編碼encode:
66 a1 44 33 22 11   
  在個 encode 里,66 是prefix,a1 是 opcode,44332211 是 displacement 或者說 mem-offset

66 改變了缺省的操作數(shù)大小,將 32 位調(diào)整為 16 位。


例2:在16位實模下,同樣一條指令:mov eax, [11223344]

  同樣一樣指令,只是目的操作數(shù)大小不同,在16 位實模式下,這條指令將被編譯器編譯為:66 67 a1 44 33 22 11
  在這個 encode 里,66 prefix 將16 位缺省操作數(shù)調(diào)整為 32 位大小,67 這也是 prefix,但它是調(diào)整 Addess-Size preifx 將 16 位地址調(diào)整為 32 位地址。其余的字節(jié)和 例1 的完全一樣。





1.1.4、編譯器編譯上下文環(huán)境
  所謂“編譯器上下文”,是指編譯器編譯目標平臺上下文環(huán)境。說明白點就是:編譯器為什么機器編譯代碼,是編譯為 16 位代碼,還是編譯為 32 位代碼或者是編譯為 64 位代碼?
  例如操作系統(tǒng)的引導(dǎo)初始化代碼部分是 16 位的,現(xiàn)在絕大多數(shù) OS 是 32 位的,因此,在當前系統(tǒng)下寫引導(dǎo)代碼,則需要求編譯器編譯為 16 位實模式代碼。
  因此,你不得不寫 16 位代碼,編譯器根據(jù)情況將 32 位操作和地址調(diào)整至 16 操作數(shù)和地址。但在大部分情況下,不需要作調(diào)整,直接生成 16 位代碼即可。
  這其實也和編程人員相關(guān)的。



1.1.5、當前執(zhí)行環(huán)境
  Processor 處理什么模式下,這是程序員需要考慮的問題,從而通過代碼體現(xiàn)出來,編譯器根據(jù)代碼生成相應(yīng)的代碼。
  一個很典型的例子就是:當 16 位初始化代碼完以及保護模式系統(tǒng)數(shù)據(jù)結(jié)構(gòu)初始化完成后開啟保護模式,然后需要從 16 位代碼跳轉(zhuǎn)至 32 位代碼。
  由于在一個匯編程序里同時存在 16 位和 32 位代碼,所以,程序員在匯編級代碼里應(yīng)指出 16 位與 32 位分界線。編譯器正確同樣生成 16 位和 32 位代碼。這里當然是通過 Operand-Size 的調(diào)整和 Address-Size 的調(diào)整,即:66H 和 67H prefix。
  此時,程序的腦海里,應(yīng)存在這樣一個概念,在運行 16 位代碼時,processor 當前處于實模式狀態(tài),跳轉(zhuǎn)至 32 位保護模式代碼時,processor 當前處于 32 位保護模式狀態(tài)。

  這就是 processor 當前執(zhí)行上下文環(huán)境。



1.2.1、操作數(shù)變?yōu)槭裁矗?或者說可以調(diào)整什么?
  這是需要探討的另一個重要話題。
  前面已經(jīng)提到:為什么需要改變? 后根據(jù)什么來改變? 現(xiàn)在是:可以改變?yōu)槭裁矗?br>
  在 16 位實模式下:操作數(shù)的大小可以是 16 位或 32 位,也就是說:可以從 16 位變?yōu)?32 位。
  在 32 位保護模式下:同樣,操作數(shù)大小可以是 32 或 16 位,也就是說,可以從 32 變?yōu)?16 位。
  ---- 僅此而已。

  但是,在 64 位 long 模式下,操作數(shù)大小有 3 個選擇:64 位、32 位以及 16 位。 可以從 32 位變?yōu)?64 位,也可以從 32 位變?yōu)?16 位!

  以上所述,這是關(guān)于 Operand-Size 的有效 Size 的話題。






2、調(diào)整地址大?。?7H prefix ----- Address-Size Override)

  Address-Size 和 Operand-Size 一樣,也有缺省地址大?。―efault Address-Size),當需要改變地址大小的時候,也需要使用67H prefix 來進行調(diào)整,所不同的是,Default Address-Size 不需要從 Descriptor 里獲取。直接定義:
  在16實模式下 Default Address-Size 為 16 位,32 位保護模式下 Default Address-Size 為 32 位,64 位 Long 模式下 Default Address-Size 為 64位。
  這是必定的。


  和 Operand-Size 一樣,Address 也有“有效 Address-Size 模式”的語義
  16 位下:可以調(diào)整為 32 位地址。
  32 位下:可以調(diào)整為 16 位地址。
  64 位下:可以調(diào)整為 32 位地址,但不能調(diào)整為 16 位地址。

更深入一點:
  64 位下,雖然能調(diào)整為 32 位址,但實際上還是 64 位地址,32 位地址會被擴展為 64 位地址,所以:調(diào)整為 32 位址,實際上是限制在前 4G 范圍內(nèi)活動而已。



以下,也舉幾個例子來說明

例1:16 位實模式下,序言里的指令:mov dword ptr [eax+ecx*8+0x11223344], 0x12345678

  由于在 16 位下,但該指令是 32 位 operand-size 以及 32 位 address-size,也就是說既要調(diào)整 default operand-size 也要調(diào)整 default address-size。所以,應(yīng)加上 66 調(diào)整 operand-size,再加上 67 調(diào)整 address-size,最終的 encode 為:
66 67 c7 84 c8 44 33 22 11 78 56 34 12


例2:在 32 位模式下,指令:mov eax, [11223344]

  對該指令,編譯器不會產(chǎn)生 16 位代碼,所以,我們手工編譯該指令,得出 encode:
67 a1 44 33 22 11
  這條指令是不對的,用 67 調(diào)整為 16 位地址,那么在匯編碼來看,它將是:
mov eax, [3344]
  它的地址將被截斷為 16 位?!〖矗刂罚?x3344,多出 22 11 兩個字節(jié)屬下條指令邊界了,同時,目標操作數(shù)被改變?yōu)?ax
除非,這樣編碼 66 67 a1 44 33 那么,結(jié)果是 mov ax, [3344]






3、調(diào)整段選擇子(段寄存器)

  對于大多數(shù)內(nèi)存操數(shù)據(jù)來說,缺省以 DS 為段基址的。常見的是:DS 段基址,SS 段基址。


來看看下面的代碼片段:

Foo:
push ebp
mov ebp, esp
lea eax, [ebp-0xc] ; int *p = &i;
mov dword ptr [eax], 0 ; *p = 0
….
mov esp,ebp
pop ebp
------------------------------------------
[ebp-0xc]:這個內(nèi)存操作數(shù)缺省是基于 SS 段的
[eax]: 這個內(nèi)存操作數(shù)缺省是基于 DS 段的。



因此,正確的語義應(yīng)該要這樣才對

lea eax, [ebp-0xc]
mov dword ptr ss:[eax], 0 ; 將 DS 改變?yōu)?SS,這才是正確的邏輯



  為什么一般程序都不會這么寫呢? 那是因為,現(xiàn)代的操作系統(tǒng)都是采用平坦的內(nèi)存模式,即:CS=SS=DS=ES,所以對 [eax] 這個操作數(shù)不需調(diào)整其結(jié)果是正確的。

  那么,我們真要對 [eax] 內(nèi)存操作數(shù)進行調(diào)整為:mov dword ptr ss:[eax], 0

  這樣的話,會產(chǎn)生下面的encode:
36 c7 00 00 00 00 00

  其中,36 也就是 prefix,是 SS segment-override 的 prefix,將 DS 段調(diào)整為 SS 段

好啦,每個段寄存都有它對應(yīng)的 prefix,下面列出每個段寄存器的 prefix:
CS: 2E
DS: 3E
ES: 26
FS: 64
GS: 65
SS: 36


  當需要進行調(diào)整段寄存器時,就使用以上的 segment-override prefix。但有些指令的缺省段寄存是 ES,典型的如 movsb 這些串操作指令
movsb 指令的實際意義是: movs byte ptr es:[edi], byte ptr ds:[esi],此時是不需要調(diào)整缺省段寄存器2、增強指令功能

  一些 prefix 對 Opcode 進行補充,增強指令的功能,優(yōu)化指令執(zhí)行,看下面這段 c 代碼:

char *move_char(char *d, char *s, unsigned count)
{
char *p = d;
while (count--)
*d++ = *s++;

return p;
}


這是典型的、經(jīng)典的字符串復(fù)制c代碼,對應(yīng)以下類似的匯編代碼:

move_char:
push ebp
mov ebp, esp
sub esp, 0xc
mov eax, [ebp+8]
mov edi, eax
mov esi, [ebp+0xc]
mov ecx, dword ptr [ebp+0x10]

move_loop:
mov bl, byte ptr [esi]
mov byte ptr [edi], bl
inc esi
inc edi
dec ecx
test ecx,ecx
jnz move_loop

mov esp, ebp
pop ebp
ret



  上面的代碼性能低下,是很死板的實現(xiàn),優(yōu)化的空間巨大。

  x86 為串提供了相應(yīng)的串操作指令(ins,outs,lods,stos,scas,cmps),對這些串指令提供 prefix 來增強優(yōu)化這些指令:


● F3: rep prefix 或 repe prefix
● F2: repne prefix


2.1、 rep prefix

  重復(fù)進行串操作,僅應(yīng)用于 ins,outs,movs,lods,stos 這些不改變標志位的串指令,結(jié)束條件是 ECX 為 0。
使用串操作及 rep prefix 上面的匯編代碼可簡單如下:

move_char:
… …
mov eax, [ebp+8]
mov edi, eax
mov esi, [ebp+0xc]
mov ecx, [ebp+0x10]
rep movsb
… …

rep movsb 的操作原理和上面的 C 代碼一致,下面是偽碼:

while (ecx != 0) {
c = ds:[esi];
es:[edi] = c;
esi = esi + 1
edi = edi + 1
ecx = ecx – 1;
}




2.2、 repe prefix

  F3 的另個 prefix 意思是 repe/repz,用于改變標志位的串操作:scas,cmps 意思是當相等(ZF=1)并且循環(huán)次數(shù)(ecx)不為 0 時進行重復(fù)操作。
  結(jié)束條件是:ecx 為 0 或者 ZF 標志位為 0, 或者說是循環(huán)條件是(ecx 不為 0,并且 ZF 標志位為 1)



常見運用一些跳過字符的邏輯上,如下面 C 代碼,用于截除串前面空格

char *trim(char *s)
{
while (*s && *s == ‘ ‘)
s++;

return s;
}



而用偽碼表示為:

While (ecx != 0 and ZF = 1) {
Ecx = ecx – 1;
cmpsb
}



rep 與 repe/repz 是相同的 prefix,作用不用體現(xiàn)在對串指操作上:

movsb 的 opcode 是 A4,scasb 的opcode 是 AE,對于下面的 encode:

F3 A4:這時 F3 prefix 是 REP, movsb 不改變標志位
F3 AE:這時 F3 prefix 是 REPZ, scasb 改變標志位




2.3、 repne/repnz prefix

  F2:這個 prefix 是 repne/repnz,意思是:循環(huán)次數(shù)(ecx)不為 0 并且 ZF=0 時重復(fù)操作。結(jié)束條件是:ecx=0 或者 ZF=1

  同樣也是用于改變標志位的串操作 scas 和 cmps。


常見一些查找字符的邏輯上,如下面 C 代碼:

char *get_c(char *s, char c)
{
while (*s && *s != c) s++;

return s;
}



而用偽碼表示為:

While (ecx !=0 and ZF != 1) {
Ecx = ecx – 1;
cmpsb
}








3、 附加功能(LOCK)

  對于寫內(nèi)存的一些指令增加了鎖地址總線的功能,這些寫內(nèi)存的指令如常見的 sub,add 等指令,通過 Lock prefix 來實現(xiàn)這功能,使用 Lock prefix 將會使 processor 產(chǎn)生 LOCK# 信號鎖地址總線

注意:
  Lock prefix 僅使用在一些對內(nèi)存進行 read-modify-write 操作的指令上,如:add, sub, and 等指令?!》駝t,將會產(chǎn)生 #UD (無效操作碼) 異常


4、64 位計算



  這里講的只是 x64 提供的 64 位計算方案,而非關(guān)于 64 位方面的編程知識。關(guān)于 64 位編程方面以后有機會再講解。
AMD 在 x86 體系的 32 位計算擴展為 64 位計算,這是通過什么來實現(xiàn)的? 它是怎樣設(shè)計的? 具體細節(jié)是什么?


4.1、 x64 的硬件編程資源
  了解現(xiàn)在 processor 提供編程資源是很重要的,下面介紹 x86 和 x64 提供的用戶編程資源。


4.1.1、 x86 原來的 32 位編程資源

● 8 個 32 位通用寄存器(GPRs):EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI
  這些寄存器還可分解為 8 個 8 位寄存器:AL、CL、DL、BL、AH、CH、DH、BH
  和 8 個 16 位寄存器:AX、CX、DX、BX、SP、BP、SI、DI
● 6 個段寄存器:ES、CS、SS、DS、FS、GS
● 32 位的 EFLAGS 標志位寄存器
● 32 位的指令指針寄存器 EIP
● 8 個 64 位 MMX 寄存器(mmx0~ mmx7, 同 ST0 ~ ST7)
● 8 個 128 位 XMM 寄存器(xmm0 ~ xmm7)
● 32 位的尋址空間(Virtual Address Space):00000000 ~ FFFFFFFF


4.1.2、 x64 的 64 位編程資源
  x64 通過擴展和新增了編程資源:

● 32 位通用寄存器被擴展至 64 位,除了原有的 8 個寄存器,又新增 8 個寄存器,共 16 個通用寄存器:RAX、RCX、RDX、RBX、RSP、RBP、RSI、RDI、R8、R9、R10、R11、R12、R13、R14、R15
● 保留了原有的 6 個段寄存器,但是作用被限制
● 32 位的標志寄存器被擴展為 64 位的標志寄存器 RELAGS
● 8 個 64 位 MMX 寄存器不變 (mmx0 ~ mmx7)
● 新增 8 個 XMM 寄存器,共 16 個 XMM 寄存器(xmm0 ~ xmm15)
● 64 位的尋址空間(Virtaul Address Space):00000000_00000000 ~ FFFFFFFF_FFFFFFFF

  x64 雖然新增了 8 個通用寄存器,對于一般 RISC 實現(xiàn)的 32 個通用寄存器來說,還是少了點。AMD 在 64 位尋址空間實則只實現(xiàn)了 48 位 virtual address 尋址空間,高 16 位被保留起來。





4.2、 寄存器編碼(或者說 ID 值)

● 16 個 64 位通用寄存器是: 0000 ~ 1111, 其值是:0 ~ 15
  x86 原有的 8 個 32 位通用寄存器是:000 ~ 111  其值是:0 ~ 7
● 6 個段寄存器的編碼是:000 ~ 101 其值是:0 ~ 5
● 8 個MMX 寄存器編碼是: 000 ~ 111 其值是:0 ~ 7
● 16 個 XMM 寄存器編碼是: 0000 ~ 1111 其值是:0 ~ 15

寄存器編碼是寄存器對應(yīng)的二進制編碼,按順序來定義,看下面的表格:

RAX/ES/MMX0/XMM0 -> 0000
RCX/CS/MMX1/XMM1 -> 0001
RDX/SS/MMX2/XMM2 -> 0010
RBX/DS/MMX3/XMM3 -> 0011
RSP/FS/MMX4/XMM4 -> 0100
RBP/GS/MMX5/XMM5 -> 0101
RSI/MMX6/XMM6 -> 0110
RDI/MMX7/XMM7 -> 0111
R8/XMM8   -> 1000
R9/XMM9   -> 1001
R10/XMM10 -> 1010
R11/XMM11 -> 1011
R12/XMM12 -> 1100
R13/XMM13 -> 1101
R14/XMM14 -> 1110
R15/XMM15 -> 1111


  x64 的 16 個通用寄存器的編碼是 0000 ~ 1111 是在 x86 原有的通用寄存器 3 位編碼上通過 REX prefix 對相應(yīng)的寄存器進行擴充 1 位,從而變成了 4 位編碼,共能表達 16 個值。





4.3、 開啟 64 位計算的基石(REX prefix)

  AMD64 體系的物理環(huán)境:操作數(shù)的 Default Operand-Size 是 32 位,而 Address-Size 是固定為 64 位的。因此,在怎么設(shè)計 64 位的計算方案時,有 3 個問題要解決的:

● 問題1:當要訪問是 64 位的寄存器時,那么必須要有一種機制去開啟或者說確認訪問的寄存器是 64 位的。
● 問題2:當要尋址內(nèi)存操作數(shù)時,那么也必須要去開啟 64 位地址。
● 問題3:如何去訪問新增加的幾個寄存器呢? 那么也必須要有方法去訪問增加的寄存器?


  那么在 64 位 Long 模式下,為什么不將操作數(shù)的 Default Operand-Size 設(shè)計為 64 位呢? 原因前面已提到:由于體系限制,x86 體系初計的初衷是為了實現(xiàn)平滑無縫地運行原有的 32 位代碼,達到完美的兼容性。
  x86 體系當初設(shè)計時就沒思考到會被擴展到 64 位。所以在 Segment-Descriptor(段描述符)里就沒有可以擴展為 64 位的標志位。CS.D 位只有置 1 時是 32 位,清 0 時為 16 位,這兩種情況。
  AMD在保持兼容的大提前下,只好令謀計策。AMD的解決方案是:增加一個 64 位模式下特有 Prefix,以起到開啟 64 位計算功能以及訪問新增寄存器的能力。 這就是 REX prefix。



4.3.1、 REX prefix 的具體格式及含義

  REX prefix 的取值范圍是:40 ~ 4F(0100 0000 ~ 0100 1111),這樣一來原有相應(yīng)的 Opcode 被占用了,這些原來的 Opcode 在 64 位 Long 模式下是無效的。變成了 REX prefix。
  來看下原來 opcode 取值范圍的 40 ~ 4F 的是什么指令:Opcode 為 40 ~ 47 在 x86 下是 inc eax ~ inc edi 指令,48 ~ 4F 在 x86 下是 dec eax ~ dec edi 指令。


REX prefix字節(jié)的組成部分如下:

0 1 0 0 0 0 0 0
- - - -
W R X B


● bit0:REX.B
● bit1:REX.X
● bit2:REX.R
● bit3:REX.W
● bit4 ~ bit7:此域固定為 0100,也就是高半字節(jié)為 4。

--------------------------------------------------------------------------------
REX.W 用來打開 64 位訪問能力,REX.W = 1 時,操作數(shù)是 64 位, REX.W = 0 時,操作數(shù)是 Default Operand-Size
REX.R 用來擴展 ModRM 尋址中的 ModRM.reg 使用 ModRM.reg 為 4 位編碼。
REX.X 用來擴展 SIB 尋址中的 SIB.index,使得 SIB.index 域為 4 位編碼。
REX.B 用來擴展 SIB 尋址中的 SIB.base, 使用 SIB.base 域為 4 位編碼及對直接嵌在 Opcode 中的 reg 進行擴展。




4.3.2、 解決之道

(1)設(shè)計 REX.W 來解決訪問 64 位操作數(shù)的能力。在 REX.W = 1 就開啟了 64 位計算能力,包括 64 位操作數(shù)和 64 位尋址。
(2)設(shè)計 REX.R 來解決訪問新增的 8 個寄存器的能力。 ModRM.reg = 000 而 REX.R = 1,組合的寄存器 ID 為 1000,這是寄存器 r8。
(3)設(shè)計 REX.X 及 REX.B 來解決 64 位尋址的問題。



下面使用幾個例子來說明解決之道:

例1:指令 mov eax, 1  
  這條指令的 Default Operand-Size 是 32 位,在 32 位下它的機器編碼是:b8 01 00 00 00(其5個字節(jié))

  64 位下使用 64 位寄存器,它的語法元素變成: mov rax, 1
  此時,它的機器編碼是 48 b8 01 00 00 00 00 00 00 00 (共10個字節(jié))

  注意這里的 48 就是 REX prefix字節(jié),即:0100 1000 它的各個域值是:REX.W = 1,定義操作數(shù)是 64 位的,REX.R = 0、REX.X = 0、 REX.B = 0 這條指令不需要 ModRM 和 SIB 字節(jié)進行尋址,所以 RXB 域都為 0。


  這里有個值得思考的地方,若 REX.W 域為 0 時,這條指令的操作數(shù)是 32 位的,機器編碼:40 b8 01 00 00 00(其 6 個字節(jié))是與 b8 01 00 00 00 結(jié)果一樣的,都是 mov eax, 1
  40 即:0100 0000 REX.WRXB 都為 0,沒有什么實際意義。





例2:指令:mov rax, r14

  這是一條常見 64 位指令,它是需要 ModRM 來進行尋址的。源寄存器是 r14,目標寄存器是 rax 它的機器編碼是:
  4c 89 f0(共3個字節(jié))
  在這個編碼里 4c 是 REX prefix,89 是 opcode,F(xiàn)0 是 ModRM。
  4c (0100 1100),其中 REX.W = 1,REX.R = 1,XB 都為 0。
  ModRM 的值是 F0(11-110-000),Mod=11,Reg=110, R/M = 000,在這里先不講 ModRM 的含義,在后面的章節(jié)再詳述。在這條指令里,Reg 表示源操作數(shù) r14 的 ID 值。
  r14 是新增加寄存器,所以需要 REX.R 進行擴展,得出最終寄存器的 ID 值:1110,這是 r14 寄存器的 ID 值,從而得出正確的編碼。 REX.R 擴展了 ModRM.reg 從而變成 4 位 ID 值。




例3:回到序言里的例子:mov word ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678

  作為例子,我將它改為 64 位指令,如下:mov qword ptr [rax + rcx * 8 + 0x11223344], 0x12345678
  操作數(shù)大小變?yōu)?64 位,而 base 寄存器和 index 寄存器都改為 64 位,disp(offset)和 imme(值不變),為啥不變?在以后的章節(jié)會有詳述。


好,現(xiàn)在來看看指令怎么譯:

(1) REX.W: 要置為 1 以使用 64 位大小。
(2) REX.B: 由于 base 不是新增的寄存器,所以置為 0
(3) REX.X: 由于 index 也不是新增的寄存器,所以置為 0
(4) REX.R: 源操作數(shù)和目標作數(shù)不是寄存器,所以置為 0

所以,REX prefix 就等于 48(0100 1000)

故,整條指令編碼是:48 c7 84 c8 44 33 22 11 78 56 34 12(共12個字節(jié))





例4:我將上面的例子再改一改,變?yōu)椋簃ov qword ptr [r8 + r9 * 8 + 0x11223344], 0x12345678

那么,看看這指令怎么譯:

(1)REX.W:置 1,使用 64 位大小
(2)REX.B:base 寄存器是 r8,是新增寄存器,所以置為 1
(3)REX.X:index 寄存器是 r9,是新增寄存器,所以置為 1
(4)REX.R:操作數(shù)中沒有寄存器,所在置為 0

所以,REX prefix就等于(0100 1011)4b

故,整條指令編碼是:4b c7 84 c8 44 33 22 11 78 56 34 12(共12個字節(jié))



例5:看看這條指令 mov r8, 1
(1)REX.W:置1
(2)REX.B:訪問 Opcode 中的寄存器 ID 值,它是新增寄存器,所為置 1
(3)REX.X:置 0
(4)REX.R:置 0

所以,REX是 49(0100 1001)
故整條指令編碼是:49 b8 01 00 00 00 00 00 00 00





4.3.3、 解開 REX prefix 迷惑

(1)關(guān)于順序:REX 一定是在 x86 prefix 之后,而在 Opcode 之前。

(2)關(guān)于沖突:當 x86 prefix 和 REX prefix 同時出現(xiàn),而又出現(xiàn)沖突時,REX 的優(yōu)先權(quán)要優(yōu)于 x86 prefix,

舉個例子:指令 mov r8, 1

  若解碼器遇到以下編碼怎么辦? 
66 49 b8 01 00 00 00 00 00 00 00 既有 66 又有 49,那么結(jié)果 66 會被忽略,也就等于:49 b8 01 00 00 00 00 00 00 00。

  而對于 66 b8 01 00 00 00 00 00 00 00 這個編碼來說:會被解析為:mov ax, 1
  去掉了 49 這個 REX prefix 后操作數(shù)被調(diào)整為 16 位。


(3) 關(guān)于原來 Opcode 碼,由于 40 ~ 4F 被作為 REX prefix,那么原指令 inc reg/dec reg,只能使用 FF/0 和 FF/1 這兩個 Opcode 了。


(4)缺省操作數(shù)大?。―efault Operand-Size)

  64 位絕大部分缺省操作數(shù)是 32 位的,但有一部分是 64 位的,依賴于 rsp 的尋址和短跳轉(zhuǎn)(near jmp/near call)是 64 位的。

如下指令:push r8
  REX 值是 41(0100 0001),即 REX.W 為 0,使用 default opearnd-size,它的編碼是 41 ff f0


  有兩大類指令,它的 Default Operand-Size 是固定 64 位。 它不依賴于 CS.D,且不能被 66h prefix 改變 Size。

  第一類是:轉(zhuǎn)移指令,包括 call、jmp(任何形式)以及 loop(任何形式)。這類指令的操作數(shù)是依賴于 rip (Instruction Pointer 寄存器),64 位下 rip 是 64 位且不能改變。 如:call [rax] 這條指令,它的編碼是:FF 10。 它是無需給出 REX prefix 的。
  
  第二類是:棧操作指令,包括 push、pop 以及 enter、leave 等。 同樣,它不依賴于 SS.B 且不能被 66h prefix 改變 Size。這類指令依賴于 rsp(stack pointer 寄存器)。 64 位下 rsp 是固定 64 位且不能改變的?!∪纾簆ush [rax] 這條指令的編碼是:FF 30,同樣它無需給出 REX prefix。


更深入一點:
  對于第一類指令:call [rax],它深入的含義是: rip = [rax]、 goto rip。 rip 的獲取是由 [rax] 給出。rip 的 Size 是固定為 64 位的,rip 的值不可能會被改變 32 位。所以,依賴于 rip 的這類轉(zhuǎn)移指令是 rip 的長度,也就是 64 位。

  對于第二類指令:push [rax],它深入的含義是:rsp = rsp - 8、[rsp] = [rax]。 同樣在 64 位,rsp 的值是不能改變的。

  但是,rsp 與 rip 含義不同。 在 32 位下,esp 所指向的棧結(jié)構(gòu) 的值是可以被改變的。那是因為:stack pointer 的值要依賴于 SS.B(SS segment 的 B 位),SS.B = 1 時,esp = esp +- 4 ,stack pointer 為 32 位。 SS.B = 0 時,esp = esp +- 2,stack pointer 為 16 位?!?br>  但是在 64 位 Long 模式下,SS segment 是無效的。也就是說:SS.B 是無效的。此時:rsp 的值固定為 64 位。

  在 64 位下,call [eax] 這類指令,編譯器要么就提示錯誤。要么就忽略錯誤,自動調(diào)正。




  最后,一句話來對 x64 的 64 位擴展作出評價:我認為它的設(shè)計架構(gòu)是完美的,也很成功的。對 AMD 的設(shè)計能力是很贊同。我不會認為比 Intel 差,甚至要好。因為在對 Opcode 的設(shè)計上,Intel 就不那么完美,這是后一節(jié)的話題了。

5、 指令編碼核心之 Opcode



  x86 指令編碼的核心是:Opcode、ModRM 以及 SIB,Opcode 提供指令的操作碼,ModRM 及 SIB 提供操作數(shù)的尋址模式。指令編碼設(shè)計模式是:Opcode 的設(shè)計要考慮兼顧 ModRM。ModRM 要服務(wù)于 Opcode,SIB 是對 ModRM 的補充輔助。



5.1、 初窺 Opcode   
  一條指令對應(yīng)一個唯一的 Opcode 碼。 在 1 個字節(jié)的空間里:00 ~ FF,Prefix 與 Opcode 共同占用這個空間。由于 x86 是 CISC 架構(gòu),指令不定長。解碼器解碼的唯一途徑就是按指令編碼的序列進行解碼,關(guān)鍵是第 1 字節(jié)是什么? 遇到 66h,它就是 prefix,遇到 89h,它就是 Opcode。

記?。?br>  Prefix 與 Opcode 共享空間的原因是:Prefix 是可選的。在編碼序列里,只有 Opcode 是不可缺少的,其它都是可選。這就決定了指令編碼中的第 1 個字節(jié)對解碼工作的重要性。


  除了 1 個字節(jié)的 Opcode 外,還有 2 個字節(jié)的 Opcode 以及 3 個字節(jié)的 Opcode,第 2 個 Opcode 碼是由 0F 字節(jié)進行引導(dǎo)。即:2 個字節(jié)的 Opcode 碼,其第 1 個 Opcode 必定是 0F 字節(jié)。

  在 AMD 的 SSE5 指令集推出之前,x86 平臺沒有真正意義上的 3 個字節(jié)的 Opcode 碼??梢岳斫鉃椋簜?3 個字節(jié) Opcode。3 個字節(jié)的 Opcode 是通過 Prefix 修飾而來。用于修飾 Opcode 的 prefix 是:66h、F2h 以及 F3h。
  使用 66h (Default Operand-Size Override),F(xiàn)2h(REPNZ)及 F3(REP/REPZ)來修飾 Opcode 而達到 3 個字節(jié) Opcode 碼,是其 Opcode 碼有這 3 個 prefix 功能的隱含語義。





5.2、 學(xué)會看 Opcode 表

  怎么去看指令的 opcode,獲得指令 opcode 編碼,還是很有學(xué)問的。



5.2.1、 從指令參考頁里看opcode 碼

  可能許多人喜歡從指令參考頁里查看 Opcode,下面的圖是 AMD 文檔中對于指令參考頁的描述:




從指令參考頁里可以得出以下信息:

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


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



  從這里看出,這個 Opcode 8B 有幾種操作數(shù)形式,reg <- reg、reg <- mem,Operand-size 可以是 16/32/64。

  不過這并不是了解 Opcode 碼的好地方,指令參考頁主要是對指令的操作進行相應(yīng)的描述。對掌握 Opcode 碼不是那么直觀和透徹。下面要看全局的 Opcode 表格。




5.2.2、 怎么看 Opcode 表

  學(xué)會看 Opcode 表才能清晰地進行分析,Opcode 表是一個全面的透徹的總結(jié)表,又可以說十分細致。Intel 和 AMD 的文檔中均提供了 Opcode 表,Opcode 表有 One-byte Opcode 表、Two-byte Opcode 表和 X87 Opcode 表等。


5.2.2.1、 Opcode表上的基本元素

  Opcode 表上描述的范圍是 00 ~ FF,即 1 個字節(jié)共 256 個值,每 1 個值描述不同的屬性,包括:

● 絕大部分代表 1 個 Opcode 碼。
● 26、2E、36、3E、64、65、66、67、F0、F2、F3 則是 prefix。
● 0F 指示 2 個字節(jié)的 Opcode,引導(dǎo)性 Opcode。
● 還有一部分是 Group 屬性指令,由 ModRM 中的 reg 來決定。這部分還包括了 x87 float 指令的 Opcode 碼。


  每個 Opcode 碼還附有相應(yīng)的 Operands 屬性,Operands 屬性是用來描述 Operands 的,包括 Operands 個數(shù)、尋址類型及 Size。

注意:
  所謂 Group(組)屬性指令是指:Intel 將一些 Opcode 碼抽出來,不具體實際的操作。具體的功能是由 ModRM 的 reg 來決定。ModRM.reg 就起決定性作用,它反過影來 Opcode 碼,這主要原因是原因:這種 Opcode 的操作數(shù)無法與 ModRM 得到良好的配合。從而決定了 Opcode 受制于 ModRM.reg。
  很典型的 FFh,這就是一個 Group 屬性的 Opcode 碼。 FFh 是一組指令的代表,F(xiàn)Fh 要由 ModRM.reg 才能決定它的指令功能。當 ModRM.reg = 010 時,F(xiàn)Fh 是 CALL 指令的 Opcode 碼。 當 ModRM.reg = 000 時,它是 INC 指令的 Opcode 碼。


更透徹一點:
  Opcode 的組是按照 Operands 的屬性進行分組的。
  說白了就是:這些 Opcode 提供的 Operands 尋址是不含寄存器。還就是說: 內(nèi)存尋址和立即數(shù)尋址或是 disp 值。






看看 mov 指令 8B Opcode 表是怎樣的:



  上圖圓圈所示是 Opcode 8B,它對應(yīng)的是 mov 的 mnemonic,表格中的 Gv, Ev 是描述這個 Opcode 碼所對應(yīng)的指令的 Operand 屬性。表示:

(1)兩個 Operands 分別是:目標操作數(shù) Gv,源操作數(shù) Ev

(2)Gv 表示:G 是寄存器操作數(shù),v 是表示操作數(shù)大小依賴于當前代碼的 Default Operand-Size,也就是CS.D??梢允?16 位,32位以及 64 位。

(3)Ev 表示:E 是寄存器或者內(nèi)存操作數(shù),具體要依賴于 ModRM.r/m,操作數(shù)大小和 Gv 一致。

  4 個字符便可以很直觀的表示出:操作數(shù)的個數(shù)以及尋址方式,更重要的信息是這個 Opcode 的操作數(shù)需要 ModRM 進行尋址。

  要看懂 Opcode 表必須學(xué)會分析和理解 Operand 屬性字符,Intel 和 AMD 的 Opcode 表前面都有對 Operands 屬性字符很仔細清晰的定義和說明。



記住以下兩點:

(1)Operands 屬性都有兩組字符來定義,前面的一組大寫字母是 Operand 類型,后面一組小定字母是 Operand Size。
  如:Gv 這里 G 是 Operand 類型,表示是 General-Purpose Register(GPR)通用寄存器,也就是 rax~r15 共 16 個。這是有別與 Segment Register、XMM 寄存器等。v 是 Operand 大小,這個 Size 是依賴于當前的 Default Operand-Size。

(2)操作數(shù)是直接編碼在 Opcode 中,這些操作數(shù)是寄存器。這些寄存器的 ID 值已在 Opcode 中,而無需指出。Operand Size 是依賴于當前 Default Operand-Size。




以下舉兩個例子。

(1)以典型的 Jmp Jz 為例,它的 Opcode 是 E9,Operand 屬性是 Jz,J 是代表基于 EIP 的相對尋址,也就是說,操作數(shù)尋址是偏移量(Offset)加上 EIP 得出。z 則表示 Operand-Size 是當前 Default Operand-Size,這個 Operand-Size 是不能被 override 的,不能被加 66h Prefix 來調(diào)整 Operand-Size?!∵@與 v 明顯不同:v 是可以調(diào)整,z 是固定的。

(2)另一個典型的例子是 call Ev,它的 Opcode 是FF,這個 Opcode 是個典型的 Group Opcode,為什么會定義為 Group,下面的將會有闡述。操作數(shù)的尋址是典型的 ModRM 尋址,準確地講是 ModRM.r/m 尋址。E 既可是 GPRs 也可以是 Mem。它同樣是 v 屬性的 Operand-Size。



下面列舉一些常見的 Operands 屬性字符,詳述請參考 Intel 或 AMD 手冊。

(1)、Operand類型字符

E:GPR 或 Mem,依賴于 ModRM.r/m
G:GPRs 具體 ID 依賴于 ModRM.reg
I:Immediate 直接在指令 encode 中
J:EIP 相對尋址操作數(shù),即:EIP+offset
O:絕對尋址和 Immediate 一樣,直接在指令 encode 中


(2)、Operand大小字符

b:One-byte
d:four-byte(doubledword)
q:eight-byte(quadword)
v:16 位、32 位、64 位依賴于 Default Operand-Size,可被 66h Prefix 改寫
z:16、32、64 位依賴于 Default Operand-Size,不可被 66h 改寫


以上所述通過查 Opcode 表能迅速得出該指令的 Opcode 及 Operands 詳情。




5.3、 透析 Opcode 的編碼規(guī)則

  如上所述:prefix 與 Opcode 共享 00~FF 的空間,由于 Prefix 部分是 可選的,當 CPU 取指單元從 ITLB 加載指令 L1-Icache和 prefetch buffer,預(yù)解碼單元通過 prefix 自已的 ID 值來解析 prefix。如讀入 66h 時是解析為 prefix 而不是 Opcode。同樣,讀入 0Fh 時被解析為是 2 個 字節(jié)的 Opcode 中的第 1 個字節(jié)。

  Opcode 的 operands 尋址一部分是在 Opcode 碼直接中指定,一部分是依賴 ModRM 給定,還有一部分不依賴 ModRM 給定。直接中 Opcode 指定的 operand 尋址的是 GPRs 尋址,如:inc eax 指令(Opcode 是 40h),還有串指令,如 loads 等。



5.3.1、 1 個 Operand 的 Opcode 碼編碼規(guī)則

(1)直接嵌入 Opcode 中
  一部分 Opcode 的操作數(shù)是直接嵌入 Opcode 中的,如:inc eax、push eax、pop eax 等。 這些指令編碼是 1 個字節(jié)。不依賴于 ModRM 尋址。是常見的指令。

(2)依賴于 ModRM 尋址,Group 屬性 Opcode
  對于單 Operand 的指令而又依賴于 ModRM 尋址。這種指令必定是 Group 屬性的指令。這種 Opcode 操作數(shù)的定義是 Ev 字符。ModRM.reg 決定 Opcode 操作碼,ModRM.r/m 決定尋址模式。如前面提到的典型 Call Ev 這種指令,操作數(shù)既可是寄存器,也可以是內(nèi)存操作數(shù),由 ModRM.r/m 來決定到底是 registers 還是 memory。

(3)不依賴于 ModRM 尋址,不是 Group 屬性 Opcode
  這種情況下的 Operand 既不嵌入 Opcode 中,也不依賴于 ModRM 進行尋址,那么它必定是 immediate 或者 displacement 值。它的 Operand 屬性字符是 Iv、Ib 或者 Jz。 這種指令很常見,如:push Iv、push Ib、Jmp Jz、Jmp Jb 等。
  push 0x12345678 這就是常見的這種指令,還非常常見的短跳轉(zhuǎn) jmp $+0x0c。




5.3.2、 2 個 Operands 的 Opcode 碼編碼規(guī)則。

(1)1 個 Operand 嵌入 Opcode,另一個 Operand 不依賴于 ModRM(非 Group 屬性)
  這種情況下,一個 Operand 必定是 GPRs,另一個不依賴于 ModRM 的 Operand 必定是 Immediate 或 Displacement。所以它不是 Group 屬性的??纯匆韵聝蓚€ Opcode:

  指令 mov rax, Iv 它的 Opcode 是 B8,目標操作數(shù)是由 Opcode 中指定的 GPRs(rax),源操作數(shù)不依賴于 ModRM 的 Immediate。是一個寄存器與立即數(shù)的尋址指令。

  在這種情況下,這個 Opcode 不依賴于 ModRM 進行尋址。
  所以:這個 Immediate 可以不受 4 字節(jié)限制,它可以是 64 位的值,即:mov rax, 0x1122334455667788 是完全正確的。


思考另一個問題,在 64 位下:
  mov qword ptr [rax],0x1122334455667788,這指令是的錯誤的。原因是它的尋址是依賴于 ModRM 的。此時,它要受限于 Immediate 最大為 4 個字節(jié)的限制。

  mov rax, qword ptr [0x1122334455667788],這條指令是完全正確的。它不依賴于 ModRM 尋址。此時,它不受于 displacement 最大為 4 個字節(jié)的限制。它的 displacement 值是直接嵌入指令編碼中。




(2)依賴于 ModRM 尋址,非 Group 屬性
  這種依賴于 ModRM 尋址而又非 Group 屬性的 2 個 Operands,絕大部分是:寄存器與內(nèi)存操作數(shù)之間或 2 個寄存器之間。它的 Operands 屬性字符是 Gv, Ev 或 Ev, Gv。
  典型的如: mov eax, ebx


(3)依賴于 ModRM 尋址,是 Group 屬性
  在這種 Opcode 編碼下,另一個操作數(shù)必定是 Immediate 或 Displacement。典型的如:mov ecx, 0x10 ,它的 Operands 屬性字符是 Ev, Iv 等。



5.3.3、 3 個 Operands 的 Opcode 編碼
  在 AMD 的 SSE5 指令集推出之前,是沒有第 3 個 Operand 是非寄存器或內(nèi)存操作數(shù)的情形。所以,第 3 個操作必定是 Immediate 值。這種指令很少。
  imul eax, ebx, 3 這是其中的一種形式。


  關(guān)于 AMD SSE5 指令集在后續(xù)中有稍為詳細的介紹。
 



  要深入掌握 Opcode 表,必要記住 2 點:一是 Opcode 表的基本元素,學(xué)會分析 Operands 屬性字符,二是理解上面所講的 Operand 尋址模式。
6、 x87 指令、3DNow 指令、SSEx 指令



  對于 GPIs 來說,這些指令的尋址模式會簡單些,但是 Opcode 碼的數(shù)量卻在增加,到現(xiàn)在的 SSE4 指令,3 個實 Opcode 加上 1 個 prefix Opcode,達到了 4 個 Opcode 數(shù)量。



6.1、 x87 float 指令集

  Opcode 范圍從 D8 ~ DF 是 x87 float 指令的 Opcode,實際上 x87 指令的 Opcode 是 2 個字節(jié)的,D8 ~ DF 是主字節(jié),ModRM 是補充字節(jié),協(xié)助主字節(jié)。
  因此,它是 Group 屬性的 Opcode,比起 GPIs 的 Group 屬性指令,x87 的 ModRM 進一步增強協(xié)助關(guān)系。

  x87 指令絕大部分是 float 寄存器(st0~st7)尋址的,實際是與 mmx0~mmx7 寄存器共用物理寄存器。



6.1.1、 x87 float 指令格式

Opcode + XXX = x87's Opcode
------ ---
| |
| |

D8 ~ DF ModRM.reg


x87 float 指令的編碼序列里,沒有 prefix 和 immediate 部分。x87 的 Group 屬性 Opcode 由上表格式所示:

D8 ~ DF: 共 8 個 Opcode 被安排為 x87 的 Opcode 碼。
ModRM.reg : 這部分是附加的 Opcode 碼,輔助 Opcode。


● 在 ModRM.mod = 11 模式下,ModRM.r/m 能提供 8 個不同組合的寄存器尋址(st0 ~ st7),它們?nèi)羌拇嫫鲗ぶ贰?br>● 在 ModRM.mod != 11 模式下,ModRM.reg 能提供 8 個不同的 Opcode 碼。 此時,x87 指令可以提供內(nèi)存操作數(shù)尋址。
● D8 ~ DF 共 8 個 Opcode 碼,理論上可提供 8 * 8 = 64 條 x87 指令。 在 ModRM.mod = 11 和 ModRM.mod != 11 之間提供兩組 Opcode 值,所以,x87 的數(shù)量理論上可達到 64 ~ 128 條。實際上,x87 float 指令約為 70 條左右。



  
6.1.2、 看看兩條常見的 x87 float 指令編碼。

(1)fstp st(1)    ; 將 st(0) 值復(fù)制到 st(1) 到,并置 stack 頂為 st(1)

  它是寄存器尋址,ModRM.mod = 11 提供寄存器尋址模式,ModRM.reg = 011 提供 fstp opcode,ModRM.r/m = 001 提供寄存器 ID 值。ModRM 的值是:11-011-001 = D9

  所以,這條指令的編碼是:dd d9


(2)fstp dword ptr [eax] ; 將 st(0) 值復(fù)制到 [eax],并置 stack 頂為 st(1)

  它是內(nèi)存尋址,ModRM.mod = 00 提供無 disp 內(nèi)存尋址,Mod.reg = 011 提供 fstp mem32 指令的 opcode,Mod.r/m = 000 提供 [eax] 內(nèi)存尋址。 ModRM 值是:00-011-000

  所以,這條指令的編碼是:d9 18




6.2、 AMD 3DNow 指令

  AMD 設(shè)計的 3Dnow 別開生面,在編碼上,實際 3 個 Opcode 的指令集,前 2 個 Opcode 是 0F 0F,這兩個 Opcode 是起引導(dǎo)為 3Dnow 的作用,第 3 個 Opcode 是主 Opcode。
  這個 Opcode 卻又不跟在第二個 Opcode 后面。 實際,第 3 個 Opcode 改由 Immediate 來充當,這個 Immediate 值是固定 為 1 個字節(jié)。


它是編碼序列是:

0F 0F ModRM SIB displacement Immediate




舉1個例子來示范:
  指令 pfcmpge mmx1, qword ptr [eax]

  這條指令的 Operand 助記符是:Pq,Qq 實際上與 Gv, Ev 情況一樣,只不過這里寄存器由 GPRs 變?yōu)?mmx 寄存器,操作數(shù)卻固定為 64 位。
  它的 Opcode 碼是:90 也就是 imme 為 90,由 ModRM 的 reg 提供 P 的尋址,r/m 提供 Q 的尋址。故 mod = 00,reg = 001, r/m = 000

  所以,這條指令的編碼是:0F 0F 08 90



6.3、 SSEx 指令

  一部分指令是 3 個 Opcode,一部分是 2 個 Opcode,而 3 個 Opcode 則是有一個 prefix 來充當。這是 Intel 設(shè)計的編碼架構(gòu),若是 AMD 設(shè)計的編碼則極大可能不同,AMD 設(shè)計的風(fēng)格是使用后面的 Imme 來充當?shù)?3 個 Opcode,而 Inte l則是使用 prefix,這里可以看出 AMD 與 Intel 風(fēng)格的不同。
  Intel 的 SSE4 指令集達到了 4 個 Opcode 的規(guī)模。真 3 個 Opcode 和 1 個 偽 Opcode(prefix)。
  這些指令使用 66h、F2 以及 F3 prefix 作為第 1 個 Opcode,0F 作為第 2 個 Opcode。

  SSEx 系列指令發(fā)展到 SSE4 數(shù)量龐大,編碼混亂。具體指令參考 Intel 文檔

舉 1 個例子:
  movntdq xmmword ptr [rax], xmm0

  這是一條復(fù)制 128 位的指令,從 xmm0 復(fù)制到 [rax] 內(nèi)存上,它采用 66 0F 前導(dǎo),第 3 個 Opcode 是E7,ModRM 提供尋址,mod = 00,reg = 000,r/m = 000,ModRM 的值是:00

所以,這條指令的編碼是 66 0F E7 00

7、 強悍的 AMD SSE5 指令集



  AMD 設(shè)計 3Dnow 與 SSE5 指令集的目的很明顯,想擺 Intel 的制約,在 x86 平臺上有屬于自已的東西,在 x86 平臺上能站穩(wěn)腳步。特別是 SSE5 指令集的推出,目的想讓 Intel 向自己靠。
  SSE5 指令集的數(shù)量不少,有八九十個之多。實事上AMD推出的 x86_64 的 64 位平滑擴展技術(shù)就成功的制約了 Intel,Intel 不得不跟隨 AMD 腳步。SSE5 能不能再創(chuàng)輝煌,引導(dǎo) Intel 向他靠還得拭目以待。



7.1、 AMD SSE5 指令集編碼的特點:

(1)SSE5 指令 Operands 可以增加到 4 個。這 4 個操作數(shù)可以全是寄存器操作數(shù),這是通過增加 DREX 字節(jié)來實現(xiàn)的。
(2)SSE5 指令編碼有 3 個 Opcode,其中 2 個是引導(dǎo) Opcode 和 1 個主導(dǎo) Opcode。
(3)SSE5 指令編碼中有 3 個字節(jié)來定位尋址操作數(shù)。在原有的 ModRM 和 SIB 的基礎(chǔ)上,增加了 Drex 字節(jié)來尋址。
(4)無需 prefix 以及 REX prefix 進行修飾。
(5)目標操作數(shù)固定為寄存器


  對比 Intel 的 SSE4 指令集,個人會 AMD 的設(shè)計更為優(yōu)越。SSE5 有 4 個操作數(shù),SSE4 是 2 個操作數(shù)。SSE5 取消掉 prefix 的修飾,SSE4 的 Opcode 達到了 4 個之多,雖然 1 個是偽 Opcode(prefix)。
  縱觀 SSEx 系列指令集,Opcode 布局混亂、指令繁多。沒有一個編碼統(tǒng)一的機制。若有新的指令集產(chǎn)生,恐怕還得修改編碼序列。
  編碼不統(tǒng)一是由于 CISC 的緣故,從編碼的角度來講,使得任一個 x86 平臺生產(chǎn)者都可以肆意添加新的指令集。


關(guān)于編碼不統(tǒng)一的話題:
  對當今的處理器來說,是 RISC 指令集,還是 CISC 指令集? 這已經(jīng)不重要了。對 CISC 架構(gòu)的 x86 處理器來說,這些 x86 指令集已經(jīng)不是原生執(zhí)行的指令集了。
  x86 指令在處理器內(nèi)部會被解碼為微指令行式執(zhí)行,這些微指令執(zhí)行單一功能。 AMD 可以在 x86 處理器上添加新的指令集。只要在 GPIs 指令集架構(gòu)(ISA)層面上保證兼容就行了。
  對于新增的指令集,這是只給了解碼器更多的解析工作而已。



7.2、 SSE5 指令結(jié)構(gòu)



  上圖是 SSE5 指令編碼序列

● 0F 24 和 0F 25 是引導(dǎo) Opcode,這兩個 Opcode 在原來是無效。
● Opcode3 是主導(dǎo) Opcode,定性 SSE5 的操作
● ModRM 與 SIB 意義和原來一致
● DREX 意即:Dest+REX,DREX.dest 是定義目標操作數(shù),REX 的含義和 REX prefix 一致。
● Displacement 含義和以前一致。
● Immediate 含義有些改動,在 SSE5 指令里只有 1 個字節(jié)大小。




7.3、 Opcode3 的結(jié)構(gòu)

位       含義
------------------------------------------------------------------
7 ~ 3 Opcode 碼
2 Oc1,它與 DREX 的 Oc0 組合起來控制操作數(shù)
1 ~ 0 OPS,它定義操縱數(shù)據(jù)的大小


Opcode: 定義 SSE5 指令的 Opcode 碼
OC1  : Opcode3.oc1 與 DREX.oc0 組合起來是 00 ~ 11
OPS : 指令執(zhí)行時對操縱的數(shù)據(jù)的大小。




7.4、 DREX 的結(jié)構(gòu)

位       含義
------------------------------------------------------------------
7 ~ 4 Dest 域,即目標操作數(shù)的 ID 值
3 Oc0,與 Opcode3 的 Oc1 組合起來控制操作數(shù)
2 即 REX.R
1 即 REX.X
0 即 REX.B


DREX.dest 是目標操作數(shù)的 ID,這個操作數(shù)必定是 xmm 寄存器。
DREX.RXB 的含義與 REX prefix 的含義是一致的,用來擴展 xmm8 ~ xmm15 寄存器




7.5、 控制操作數(shù)

  Opcode3.Oc1+DREX.Oc0 組合共 2 位值控制操作數(shù)分配,從 00 ~ 11

  對于 4 個操作數(shù)的指令來說,例:fmaddps dest, src1,src2,src3 


其控制的操作數(shù)分配如下:

值 含義
---------------------------------------------------------------------------------------
00 dest = DREX.dest,src1 = DREX.dest,src2 = ModRM.reg
src3 = ModRM.r/m

01 dest = DREX.dest, src1 = DREX.dest, src2 = ModRM.r/m
src3 = ModRM.reg

10 dest = DREX.dest,src1 = ModRM.reg,src2 = ModRM.r/m
src3 = DREX.dest

11 dest = DREX.dest,src1 = ModRM.r/m,src2 = ModRM.reg
src3 = DREX.dest

----------------------------------------------------------------------------------------------



像:fmaddps xmm1, xmm2, xmmword ptr [rax], xmm1 

這條指令的機器編碼是:0F 24 04 10 10


若:fmaddps xmm1,xmm2,xmmword ptr [rax+0x11223344], xmm1 

則是:0F 24 04 90 10 44 33 22 11
8、 指令編碼核心之:ModRM 尋址




前面多次提到:

  Opcode 對指令提供操作碼,ModRM 則對指令提供操作數(shù)尋址,SIB 對 ModRM 進行補充尋址。

有兩種情況下是無需用 ModRM 提供尋址的:

  第一是:操作數(shù)是寄存器,它直接嵌入 Opcode 中。

  第二是:操作數(shù)是立即數(shù),它直接嵌入指令編碼中。




  本節(jié)講解指令核心另一個重點:ModRM 尋址。這個 ModRM 尋址非常重要。是理解 x64 平臺上指令 Operands 的關(guān)鍵。



8.1、 ModRM 的含義

  ModRM 字節(jié)的組成部分為:mod-reg-r/m 三個部分,mod 為 2 位,reg 與 r/m 是 3,組成 2-3-3 的比例,在這整篇文檔中的寫法是:ModRM.mod、ModRM.reg 以及 ModRM.r/m。
  mod 是提供尋址的模式,這個模式以 displacement 值作區(qū)別的。reg 是提供寄存器尋址,reg 表示寄存器 ID 值。r/m 提供對內(nèi)存的尋址中的寄存器 ID 值或寄存器尋址中的 ID 值。



8.1.1、 mod 尋址模式。

  2 位組成 4 種尋址模式,總的來說,只有兩種尋址模式,就是:內(nèi)存尋址模式和寄存器尋址模式。mod = 11 時指出寄存器尋址模式,mod = 00 ~ 10 時指出內(nèi)存尋址模式:


值 含義
-------------------------------------------------------------------------------------
00 [register] 間接尋址,無 displacement 值。
01 [register + disp8],有 8 位 displacemnet 偏移值。
10 [register + disp32],有 32 位 displacement 偏移值。
11 registers 尋址





8.1.2、 reg 尋址寄存器

  3 位組成 8 個寄存器 ID 值,從 000 ~ 111,對應(yīng)于 RAX、RCX、RDX、RBX、RSP、RBP、RSI 以及 RDI。這個 ID 值可以被REX prefix 擴充為 4 位,范圍從 0000 ~ 1111 可表示 16 個寄存器。
  ModRM.reg 的另一含義是對 Opcode 的補充,對分為一組 Opcode 的進行選擇(Group 屬性)。

  ModRM.reg 是提供 registers 尋址。



8.1.3、r/m 尋址 register / memory

  r/m 意即:registers/memory,提供對 registers 或 memory 的尋址。
  000 ~ 111 也是用來表示寄存器 ID 值。當尋址 registers 時是寄存器 ID 值。當尋址 memory 時是寄存器間接尋址中的寄存器 ID 值。當 mod != 11 時,r/m 表示 [rax] ~ [rdi]。mod = 11 時,r/m 表示 rax ~ rdi
  REX prefix 用來擴充寄存器ID值。





8.2、 探討 2 個設(shè)計上的問題以及解決之道


8.2.1、如果像這條指令:mov eax, [eax+ecx*2+0x0c]   

  在這條指令里 eax 是 base 寄存器,ecx 是 Index 寄存器,2 是 scale,還有一個 displacement 這種內(nèi)存尋址是 base+index*scale+disp。
  這需要 SIB 字節(jié)來進行補充尋址,那么 ModRM 必須要有一個手段來引出后續(xù)的 SIB 字節(jié)。


  在 [rax] ~ [rdi] 的范圍里,Intel 選擇了原來應(yīng)屬于 [rsp] 的值用來引出 SIB 字節(jié),一是因為 [rsp] 并不常用吧。二是因為 rsp 設(shè)計為 stack top 指針,專用于指示 stack top 指針,設(shè)計的語義上是用來指示 stack top,而不是尋址。

  經(jīng)過衡量和考慮,把原來屬于 [rsp] 的領(lǐng)域騰空,用于 [SIB] 來實現(xiàn)引導(dǎo) SIB 字節(jié)。
  Mod.r/m = 100,這個領(lǐng)域被 [SIB] 替代了。
  事實上在 16 位機器原本是沒有 SIB 字節(jié)的,base+index*scale+disp 這種尋址是后來才增加的。16 位的 ModRM 上是沒有 SIB 引導(dǎo)域。



8.2.2、 另一種情況是 [disp32]

  如果內(nèi)存尋址中沒有 base 和 index,只有 disp 的話,如:mov ebx, [0x11223344]。對于這種直接尋址方式,在設(shè)計上 ModRM 還必須為它提供這種尋址模式。

  Intel 又作出修改,選擇了原來屬于 [rbp] 模式的領(lǐng)域提供給 [disp32]。

  選擇 [rbp] 讓給 [disp32],是因為 rbp 原本意圖就是設(shè)計為 stack frame 基址指針。[rbp] 尋址一般都要加上一個偏移量,也就是基于stack frame 指針的偏移量。
  所以,[rbp] 尋址顯得不合邏輯。


  [rbp + disp] 這種尋址模式是符合邏輯語義。在 ModRM.mod = 01 或 ModRM.mod = 10 中給出了這種尋址模式。那么,最終 ModRM.mod = 00 時,[rbp] 讓位給了 [disp32]。


在最終的 ModRM.mod = 00 上, r/m 尋址設(shè)計上,如下表格:

r/m 內(nèi)存尋址
--------------------------------
000 [rax]
001 [rcx]
010 [rdx]
011 [rbx]
100 [SIB] ---- 原本對應(yīng) [rsp]
101 [disp] ---- 原本對應(yīng) [rbp]
110 [rsi]
111 [rdi]



  而在 ModRM.mod = 01 及 ModRM = 10 上,ModRm.r/m = 101,設(shè)計為 [rbp + disp8] 及 [rbp + disp32],提供了對 [rbp+偏移量] 的支持,這樣也符合原本設(shè)計的語義。




8.3、16 位尋址下的 ModRM

  在 16 位尋址下是不支持 base+index*scale 這種尋址模式的,這樣在編碼序列里就無需提供 SIB 字節(jié)了。

  16 位的 ModRM 尋址支持的是基址和變址寄存器間接尋址和 基址+變址尋址模式?;芳拇嫫?2 個:就是 bx 和 bp。變址寄存器也是 2 個:就是 si 和 di。


  基于上述設(shè)計,基址+變址尋址的組合只有 4 個,也就是:[bx+si]、[bx+di]、[bp+si] 以及 [bp+di]。它們對應(yīng) ModRm.r/m 域就是 000 ~ 011。
  那么這 4 個寄存器的間接尋址就是:[si]、[di]、[bp] 以及 [bx],對應(yīng)于 Mod.r/m 的 100 ~ 111。
  同樣如上述,bp 是 stack frame 指針,一般使用需加 disp(offset),所以 [bp] 讓位給 [disp] 解決直接尋址的問題。


所以,16 位的 ModRM.mod = 00 下,ModRM 最終設(shè)計方案是:

r/m 內(nèi)存尋址
-------------------------------------------------
000 [bx+si]
001 [bx+di]
010 [bp+si]
011 [bp+di]
100 [si]
101 [di]
110 [disp] ------ [bp] 讓位給 [disp16]
111 [bx]



ModRM.mod = 01、ModRM.mod = 10 和 11 的情形下如前如述。





8.4、 64 位尋址下的 ModRM

  在 64 位下,ModRM 的含義與 32 位一致,改進的只是在原來基礎(chǔ)上增加了 8 個 GPRs,通過 REX prefix 進行對新增的寄存器進行訪問。
  寄存器的 ID 取值為:0000 ~ 1111。由 REX.R 以及 REX.B 位進行擴展訪問。






8.5、 結(jié)合Opcode來看尋址模式及Opcode的定位

  Opcode 定義指令的執(zhí)行碼,用于執(zhí)行什么操作,對于操作數(shù)尋址上,Opcode 結(jié)合 ModRM 來定義操作數(shù),這是一個經(jīng)過反復(fù)琢磨推敲的過程,而最終又影響到 Opcode 的定位。
  下面講講怎么影響到 Opcode 最終定位。


8.5.1、 一個操作數(shù)的 Opcode 定位

  操作數(shù)要么就是 registers,要么就是 memory,要么就是 Immediate 值。如果指令只有一個操作數(shù)。

(1)如它是 register 的話,Opcode 是無需 ModRM 配合確定尋址方式的。

記住 1:
  ModRM 是用于尋址 2 個操作數(shù),這兩個操作是必定是寄存器之間,以及寄存器與內(nèi)存操作數(shù)之間的尋址方式。


  在只有 1 個寄存器操作數(shù)的情況下,ModRM 無用武之處。所以,在這種情沖下,寄存器操作數(shù)絕大部分是嵌在 Opcode 里面,它由 Opcode 的寄存器域指出。如常見的 inc ecx、dec ecx、push eax 等。

記住 2:
  除了上述嵌入 Opcode 情況外,如果 ModRM 用于尋址 1 個操作數(shù),這條指令的 Opcode 必定是:Group 屬性。


  若是 Group 屬性,則 ModRM 有用武之地了。


(2)如它是 Immediate 的話,它絕對是無 ModRM。直接將 Immediate 值嵌入指令編碼里。

(3)如它是 memory 的話,它絕對是 Group 屬性,需要 ModRM.reg 來配合定位。那為什么不能是直接 offset 呢,直接 offset 尋址留給最常用的,最有用的 Opcode,以免 Opcode 占位,浪費資源。




8.5.2、 兩個操作數(shù)的 Opcode 定位

  兩個操作數(shù)大部分都需 ModRM 配合定位尋址。ModRM 提供的 2 個操作數(shù)尋址大有用武之地。

(1)2 個操作數(shù)中,其中 1 個是寄存器,另1個不是立即數(shù)的這種情況最直接簡單,由 ModRM 的 reg 及 r/m 提供尋址。
  若其中 1 個是 GPRs 另 1 個是 immediate 或 displacement 的情形下更簡單,GPRs 則直接由 Opcode 提供尋址。


(2)2 個操作數(shù)中,沒有寄存器的情形下,也就是要么是 memory,要么是 Immediate,它必然是個 Group 屬性,reg 域提供 Opcode 的定位,r/m 提供內(nèi)存尋址。Immediate 直接嵌入指令編碼中。



8.5.3、 三個操作數(shù)的 Opcode 定位

  三個操作數(shù)中有一個必定是 Immediate,在 AMD 的 SSE5 指令集推出之前,x86 平臺是無法提供第 3 個非 Immediate 操作數(shù)的定位。
  直至 AMD 的 SSE5 通過增加另一個描述操作數(shù)的字節(jié)來尋址第 3 個操作數(shù)。


  如上所述,Opcode 的設(shè)計要考慮到與 ModRM 的配合。主要表現(xiàn)在什么時候是 Group 屬性。


前面已提過:
  Opcode 的分組是按 Operands 屬性進行的。即:這一組 Opcode 擁有相同類型的 Operands。
  這個類型的 Operands 主要是單個內(nèi)存操作數(shù)或單個寄存器操作數(shù)而非嵌入 Opcode 中。


9、指令編碼核心之 SIB 尋址




  在 ModRM 無法提供更多內(nèi)存尋址方式時,使用 SIB 進行協(xié)助尋址。對內(nèi)存操作數(shù)尋址提供補充定義。SIB 提供的補充內(nèi)存尋址支持 base + index * scale + displacement 尋址,如:[rax + rcx * 8 + 0x11223344]。
  此時,SIB 字節(jié)是:scale = 11,index = 001,base = 000,組合起來是 c8



9.1、 SIB 的含義及結(jié)構(gòu)

  SIB 意即:Scale – Index – Base,用來定義 base+index*scale+disp 這種尋址模式。同樣按 2-3-3 比例組合。


Scale 索引因子的含義:

值 含義
----------------------------------------------------------------------
00     無 scale, 或者:index * 1
01     按 index * 2 比例
10     按 index * 4 比例
11     index * 8 比例



  Index 域指出 Index 寄存器的 ID 值,范圍從 000 ~ 111。Base 域指出 Base 寄存器的 ID 值,從 000 ~ 111。

  同樣,Index 與 Base 經(jīng)過 REX prefix 可以擴展為 0000 ~ 1111,可尋址 16 個寄存器。




9.2、 對 ModRM 尋址的補充

  前面提到 ModRM 中,ModRM.r/m = 100 時,[esp] 讓位給 [SIB] 提供引導(dǎo) SIB 字節(jié)。這種情況下,指令可使用 base+index 的尋址方式。


ModRM.r/m 含義 __ SIB.index 含義
------------------ /  ---------------------
... ... / 000 [rax + base]
100 [SIB] ---> SIB.scale = 00 / 001 [rcx + base]
... ... = 01 \ 010 [rdx + base]
= 10 \ 011 [rbx + base]
= 11 \ 100 [base]
--- 101 [rbp + base]
110 [rsi + base]
111 [rdi + base]






  SIB 提供了從 [base+index] 到 [base+index*8] 尋址范圍,那么這里延續(xù)上一節(jié)提到的 ModRM 設(shè)計上的問題:[rsp] 這種尋址方式將怎么解決呢? 在 ModRM 中拋棄了 [rsp] 的尋址模式,在 SIB 中將得到補救。

  在 index = 100 時,按規(guī)則應(yīng)該是 [rsp + base] 這種尋址模式吧?

  由于 rsp 的特殊性,rsp 寄存器只能做 base 寄存器,以 rsp 為基址。而不能作為 index 寄存器。所以:[rsp+base] 這種尋址模式中 rsp 被去除,那么將剩余 [base]。



  故當 index = 100 時,[rsp+base]具體形式將取決于 base。




9.2、 對 [rsp] 尋址的補救方式

  由于 ModRM 中沒提供 [rsp] 尋址,而又確實需要 [rsp] 尋址的話,在 SIB 中通過 index = 100 時提供,[rsp+base] 中去除掉 rsp,留下 [base] 尋址,所以在 base = 100 時就提供了對 [rsp] 的尋址。





9.3、 重復(fù)編碼

  在 index = 100,去除 rsp 后,變?yōu)?[base] 尋址,那么此時又提供了 [base] 的尋址方式,這樣就與 ModRM.mod = 00 時 ModRM.r/m 提供了完全相同的尋址方式。
  所不同的是一種由 ModRM.mod = 00 直接給出,另一種通過 ModRM./rm = 100 然后轉(zhuǎn)接到 SIB,再由 SIB.index = 100 時 SIB.base = 100 提供。

  Intel 設(shè)計的時候已不能顧及這么多了,反正重碼也沒什么多大的害處,只是麻煩了 processor 的解碼單元而已,但這樣就顯不得夠嚴謹,話說回來,本來 x86 平臺指令集就不是十分嚴謹。?




9.4、 對于 [rbp] 尋址模式的裁減

  那么當 index = 100,也就是 [base] 尋址,而 base = 101(rbp)時,按規(guī)則此時應(yīng)該為 [rbp] 尋址是吧?

  Intel 又變變花樣,應(yīng)是 [rbp] 時而又不是 [rbp],原因前面已經(jīng)提過,rbp 的語義應(yīng)該是 stack frame pointer,以 rbp 為基址加上 offset (displacement)進行尋址,這才符合語義。

  前面提到:當 ModRM.r/m = 100,[rsp] 讓位給 [SIB],而鏈接到 SIB 尋址。
  而 SIB.index = 100 ([base] 尋址)且 SIB.base = 101(rbp),Intel 對它作出了裁減,去掉 [rbp] 尋址模式。

Intel 為了保證 [rbp + disp] 尋址,兩處地方對 [rbp] 作出了裁減:
  (1) ModRM.mod = 00 & ModRM.r/m = 101,原來對應(yīng) [rbp] 變成了 [displacement32] 這種直接尋址模式。

  (2) ModRM.r/m = 100(引發(fā)[SIB]),SIB.index = 100(變?yōu)?[base]),SIB.base = 101(對應(yīng) [rbp]),作出了裁減:ModRM.mod = 00 時,SIB = xx-100-101 下,變成了 [displacement32] 尋址。ModRM.mod = 01,SIB(XX-100-101)變成了 [rbp + disp8]。ModRM.mod =10,SIB(XX-100-101),變成了 [rbp + disp32] 尋址。



  這樣,使用 ModRM 尋址和使用 ModRM + SIB 尋址下,保證了 [rbp+disp8] 及 [rbp+disp32] 這兩種尋址,去掉了 [rbp] 這種尋址模式。
  那么,在匯編語法中 [rbp] 這種尋址,只好變通一點:采用 [rbp + 0x00] 或 [rbp + 0x00000000] 這種形式了,編譯器會自動產(chǎn)生這種編碼模式。





9.5、 解開 SIB 尋址的迷惑


1、重碼問題

  x86 指令的內(nèi)存尋址有三個地方的重碼現(xiàn)象:

(1)[disp32] 尋址方式的重碼現(xiàn)象
● ModRM.mod = 00,ModRM.r/m = 101 提供了 [disp32] 尋址。
● ModRM.mod = 00,ModRM.r/m = 100,SIB.index = 100,SIB.base = 101 提供了 [disp32] 重碼尋址。
-----------------------------------------------------------------
即 ModRM = 00-XXX-101 與 ModRM = 00-XXX-100 + SIB = XX-100-101 下重碼

(2)[base] 尋址方式的重碼現(xiàn)象
● ModRM.mod = 00 提供了 [base] 尋址,也就是 [rax] 之類。
● ModRM.mod != 11 & ModRM.r/m = 100 ,SIB.index = 100 提供了 [base] 尋址,導(dǎo)致重碼。
-----------------------------------------------------------------------------------
即:ModRM = 00-XXX-XXX 與 ModRM = 00-XXX-100 + SIB = XX-100-XXX(不含101)下重碼


(3)[ebp+disp8] 與 [ebp+disp32] 的重碼現(xiàn)象
● ModRM.mod = 01 和 10,ModRM.r/m = 101,提供了相應(yīng)的 [ebp+disp8] 和 [ebp+disp32] 尋址。
● ModRM.mod 01 和 10,ModRM.r/m = 100。
  SIB.index = 100,SIB.base = 101,提供了相應(yīng)的 [ebp+disp8] 和 [ebp+disp32]
---------------------------------------------------------------------------------------------------------------
即:ModRM = 01-XXX-101 與 ModRM = 01-XXX-100 + SIB = XX-100-101 都提供了 [rbp+disp8]
ModRM = 10-XXX-101 與 ModRM = 10-XXX-100 + SIB = XX-100-101 都提供了 [rbp+disp32]




2、不支持 rsp 作為 index 寄存器

  因 rsp 設(shè)計為 stack top 指針,而不支持 [rsp+base] 這種尋址方式。 rsp 只能作為 base 尋址,即:[rsp]。 故 [rsp+base] 被去除 rsp,只剩下 [base],而引發(fā)重碼問題。




3、 SIB 同樣不支持 [rbp] 尋址方式

  須作為 stack frame pointer 加偏移量的尋址方式。這符合設(shè)計語義。程序中使用 [rbp] 則要變?yōu)椋篬rbp+disp8] 或者 [rbp + disp32] 這種方式。
  SIB 和 ModRM 回繞在一起,保證不提供 [rbp] 尋址。


    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多