1. 程式人生 > >Intel call指令

Intel call指令

比例 微機原理 跟著 x64 ets 模式 包含 復雜 數字

轉載:http://blog.ftofficer.com/2010/04/n-forms-of-call-instructions/

最近有一個需求,給你個地址,看看這個地址前面是不是一個CALL指令(請同學們自行聯想該需求的來源)。作為團隊的救火隊員+炮灰,這個簡單的事情自然落在了我的頭上。

這個事情很簡單,作為一個善於站在別人肩膀上的程序員我們可以考慮使用 libdisasm;如果要考慮x64,就試試udis86;如果需要用Python,就有Python包裝好的 pydasm。不過這兩個400KB+的庫,顯然不值得為了一個CALL指令導入到編譯出來大小僅僅100K不到的項目代碼裏面。

那麽就自己抽一個CALL指令解碼邏輯出來好了。這個邏輯的復雜性在於,你無法知道前面一個CALL指令有多長。因此,首先需要枚舉出所有的CALL指令格式。

Intel有公開的指令集格式文檔,你需要的是第二卷的上半部分,指令集從A到M。這篇文檔的難度超出一般人想象,裏面有眾多晦澀的標識、與硬件緊密相關的介紹,拿到這後,即使直接翻到目錄的CALL 指令一節,也不見得能夠弄清楚。不相信?我們就翻到那裏看看:

技術分享

CALL指令格式一覽表

雖然很明確的列出,第一列是指令的二進制形式,第二列是指令的匯編形式,但是面對著 E8 cw, FF/2這樣的標識,一樣不知道究竟對應的二進制格式是什麽樣的。

那好,我們就從理解這些標識開始。文檔向前翻,有一個專門的節(3.1.1 Instruction Format)講述這些標識的含義。這裏抽出其中兩個用得著的翻譯一下:

表格中的“Opcode”列列出了所有的所有可能的指令對應的二進制格式。有可能的話,指令代碼使用十六進制顯示它們在內存當中的字節。除了這些16進制代碼之外的部分使用下面的標記:

cb, cw, cd, cp, co, ct — opcode後面跟著的一個1字節(cb),2字節(cw),4字節(cd),6字節 (cp),8字節(co) 或者 10字節(ct) 的值。這個值用來表示代碼偏移地址,有可能的話還包括代碼段寄存器的值。

/digit — digit為0到7之間的數字,表示指令的 ModR/M byte 只使用 r/m字段作為操作數,而其reg字段作為opcode的一部分,使用digit指定的數字。

紅字部分不知道什麽含義?沒關系,我們先不看它。對於cb/cw之類的,基本上能夠簡單看明白其中的一些指令含義了:

E8 cw 的含義是:字節 0xE8 後面跟著一個2字節操作數表示要跳轉到的地址與當前地址的偏移量。
E8 cd 的含義是:字節 0xE8 後面跟著一個4字節的操作數表示要跳轉的地址與當前地址的偏移量。
9A cd 的含義是:字節 0x9A 後面跟著一個6字節的操作數表示要跳轉的地址和代碼段寄存器的值。

那麽,同樣的0xE8開頭的指令,CPU如何區分後面的操作數是2字節還是4字節?答案是和CPU的模式有關,在實模式下,0xE8接受2字節操作數,而32位保護模式下接受4個字節,64位保護模式下同樣接受4字節,同時需要對該操作數進行帶符號擴展。

因此,CALL指令的前兩種格式是:E8 xx xx xx xx,和 9A xx xx xx xx xx xx。一個是5字節長,一個是7字節長。其實E8 那種,就是我們在匯編指令裏面寫 CALL lable之後產生的,最常見的CALL指令。

然後是下面的FF /2。這個是0xFF字節後面跟上一個blablabla的東西。這個blablabla的東西是什麽呢?要解釋這個,首先需要知道紅字標出來的部分,即ModR/M是什麽東西。

這個要先回到最基本的一個問題:IA32的指令格式。

技術分享

IA-32,Intel 64指令格式

其中每個部分是什麽含義呢?

首先是指令前綴。有印象的應該記得當年學習微機原理的時候提到過得循環前綴 repnz/repne,這個前綴就是被編碼在指令的前面部分的。每個前綴最多一個字節,一條指令最多4個前綴。

然後是指令代碼(opcode),這部分標識了指令是什麽。這個是指令當中唯一必需的部分。前面例子當中的 0xE8,0xFF都是opcode。

再後面就是我們要重點關心的 ModR/M字段了,還有和它密切相關的SIB字節。手冊2.1.3當中有對於它們的詳細描述。

許多指令需要引用到一個在內存當中的值作為操作數,這種指令需要一個稱為尋址模式標識字節(addressing-form specifier byte),或者叫做ModR/M字節緊跟在主opcode後面。ModR/M字節包含下面三個部分的信息:

  • mod(模式)域,連同r/m(寄存器/內存)域共同構成了32個可能的值:8個寄存器和24個尋址模式。
  • reg/opcode(寄存器/操作數)域指定了8個寄存器或者額外的3個字節的opcode。究竟這三個字節用來做什麽由主opcode指定。
  • r/m(寄存器/內存)域可以指定一個寄存器作為操作數,或者可以和mod域聯合用來指定尋址模式。有時候,它和mod域一起用來為某些指令指定額外的信息。

這一段有些晦澀。其意思解釋一下是這樣的:一個指令往往需要引用一個在內存當中的值,典型的就是如mov:

MOV eax, dword ptr [123456]
MOV eax, dword ptr [esi]

這其中的 123456 或者 esi 就是 MOV 指令引用的內存地址,而MOV關心的是這個地址當中的內容。這個時候,需要某種方式來為指令指定這個操作數的類型:是一個立即數表示的地址,還是一個存放在寄存器當中的地址,或者,就是寄存器本身。

這個用來區分操作數類型的指令字節就是 ModR/M,確切的說是其中的5個位,即mod和r/m域。剩下的三個位,可能用來做額外的指令字節。因為,IA32的指令個數已經遠超過一個字節所能表示的256個了。因此,有的指令就要復用第一個字節,然後依據ModR/M當中的reg/opcode域進行區分。

現在回頭看前面的紅字標識的部分,能不能理解 /digit 這種表示法了?

對於SIB的介紹,我們先忽略,看看對於CALL指令的枚舉我們已經能做什麽了。

CALL指令的表示法:FF /2,是 0xFF 後面跟著一個 /digit 表示的東西。就是說,0xFF後面需要跟一個 ModR/M 字節,ModR/M字節使用 reg/opcode 域 = 2 。那麽,reg/opcode = 2 的字節有32個,正如ModR/M的解釋,這32個值代表了32種不同的尋址方式。是哪32種呢?手冊上面有張表:

技術分享

32字節尋址模式下的ModR/M字節

非常復雜的一張表。現在就看看這張表怎麽讀。

首先是列的定義。由於 reg/opcode 域可以用來表示opcode,也可以用來表示reg,因此同一個值在不同的指令當中可能代表不同的含義。在表當中,就表現為每一列的表頭都有很多個不同的表示。我們需要關心的就是 opcode 這一個。註意看我用紅圈圈出來的部分,這一列就是 opcode=2 的一列。而我們需要的 CALL 指令,也就是在這一列當中,0xFF後面需要跟著的內容。

行的定義就是不同的尋址模式。正如手冊所說,mod + R/M域,共5個字節,定義了32種尋址模式。0x10 – 0x17 對應於寄存器尋址。例如指令 CALL dword ptr [eax] :[eax]尋址對應的是 0x10,因此,該指令對應的二進制就是 FF 10。同理, CALL dword ptr [ebx] 是 FF 13,CALL dword ptr [esi] 是 FF 16,這些指令都是2個字節。有人也許問 CALL word ptr [eax] 是什麽?抱歉,這不是一個合法的32位指令。

0x50-0x57部分需要帶一個 disp8,即 8bit 立即數,也就是一個字節。這個是基地址+8位偏移量的尋址模式。例如 CALL dword ptr [eax+10] 就是 FF 50 10 。註意雖然表當中寫的是 [eax] + disp8 這種形式,但是並不表示是取得 eax 指向的地址當中的值再加上 disp8,而是在eax上加上disp8再進行尋址。因此寫成 [eax+disp8] 更不容易引起誤解。後面的disp32也是一樣的。這個類型指令是3個字節。

0x90 – 0x97部分需要帶 disp32,即4字節立即數。這個是基地址+32位偏移量。例如 CALL dword ptr [eax+12345] 就是 FF 90 00 01 23 45。有趣的是, CALL dword ptr [eax+10] 也可以寫成 FF 90 00 00 00 10。至於匯編成哪個二進制形式,這是匯編器的選擇。這個類型的指令是6個字節。

0xD0 – 0xD7部分則直接是寄存器。這邊引用的寄存器的類型有很多,但是在CALL指令當中只能引用通用寄存器,因此 CALL eax 就是 FF D0,臭名昭著的 CALL esp 就是 FF D4。註意 CALL eax 和 CALL [eax] 是不一樣的。這些指令也是2個字節。

仔細的人也許主要到了,在表當中,0x14, 0x15, 0x54和0x94是不一樣的。0x15比較簡單,這個要求 ModR/M後面跟上一個32位立即數作為地址。即常見的 CALL dword ptr [004F778e] 這種格式的,直接跳轉到一個固定內存地址處存放的值,常見於調用Windows的導出表。對應的二進制是 FF 15 00 4F 77 8E ,有6個字節。

0x14,0x54,0x94部分是最復雜的,因為這個時候,ModR/M不足以指定尋址方式,而是需要一個額外的字節,這個字節就是指令當中的第4個字節,SIB。同樣在手冊的2.1.3,緊跟著ModR/M的定義:

某些特定的ModR/M字節需要一個後續字節,稱為SIB字節。32位指令的基地址+偏移量,以及 比例*偏移量 的形式的尋址方式需要SIB字節。 SIB字節包括下列信息:

  • scale(比例)域指定了放大的比例。
  • index(偏移)域指定了用來存放偏移量 的寄存器。
  • base (基地址)域用來標識存放基地址的寄存器。

0x14, 0x54, 0x94就是這裏所說的“特定的ModR/M字節。這個字節後面跟著的SIB表示了一個復雜的尋址方式,典型的見於虛函數調用:

CALL dword ptr [ecx+4*eax]

就是調用ecx指向的虛表當中的第eax個虛函數。這個指令當中,因為沒有立即數,因此FF後面的字節就是0x14,而 [ecx+4*eax] 就需要用SIB字節來表示。在這個指令當中,ecx就是 Base,4是Scale,eax是Index。

那麽,Base, Scale和Index是如何確定的呢?手冊上同樣有一張表(又是巨大的表):

技術分享

32位尋址模式當中的SIB字節

列是Base,行是Index*Scale,例如[ecx+4*eax] 就是0x81。

根據這張表,CALL dword ptr [ecx+4*eax] 就是 FF 14 81 。由此可見,對於 0x14系列的來說,CALL指令就是 3個字節。
而 0x54 帶 8bit 立即數,就是對應於 CALL指令:CALL dword ptr [ecx+4*eax+xx],這個指令就是 FF 54 81 xx,是4個字節。
同理,0x94帶32位立即數,對應於CALL指令:CALL dword ptr [ecx+4*eax+xxxxxxxx],這個指令就是 FF 94 81 xx xx xx xx,是7個字節。

OK,截止到目前,我們基本上能夠列出常見的CALL指令的格式了:

指令 二進制形式
CALL rel32 E8 xx xx xx xx
CALL dword ptr [EAX] FF 10
CALL dword ptr [ECX] FF 11
CALL dword ptr [EDX] FF 12
CALL dword ptr [EBX] FF 13
CALL dword ptr [REG*SCALE+BASE] FF 14 xx
CALL dword ptr [abs32] FF 15 xx xx xx xx
CALL dword ptr [ESI] FF 16
CALL dword ptr [EDI] FF 17
CALL dword ptr [EAX+xx] FF 50 xx
CALL dword ptr [ECX+xx] FF 51 xx
CALL dword ptr [EDX+xx] FF 52 xx
CALL dword ptr [EBX+xx] FF 53 xx
CALL dword ptr [REG*SCALE+BASE+off8] FF 54 xx xx
CALL dword ptr [EBP+xx] FF 55 xx
CALL dword ptr [ESI+xx] FF 56 xx
CALL dword ptr [EDI+xx] FF 57 xx
CALL dword ptr [EAX+xxxxxxxx] FF 90 xx xx xx xx
CALL dword ptr [ECX+xxxxxxxx] FF 91 xx xx xx xx
CALL dword ptr [EDX+xxxxxxxx] FF 92 xx xx xx xx
CALL dword ptr [EBX+xxxxxxxx] FF 93 xx xx xx xx
CALL dword ptr [REG*SCALE+BASE+off32] FF 94 xx xx xx xx xx
CALL dword ptr [EBP+xxxxxxxx] FF 95 xx xx xx xx
CALL dword ptr [ESI+xxxxxxxx] FF 96 xx xx xx xx
CALL dword ptr [EDI+xxxxxxxx] FF 97 xx xx xx xx
CALL EAX FF D0
CALL ECX FF D1
CALL EDX FF D2
CALL EBX FF D3
CALL ESP FF D4
CALL EBP FF D5
CALL ESI FF D6
CALL EDI FF D7
CALL FAR seg16:abs32 9A xx xx xx xx xx xx

Intel call指令