本文源地址: http://blog.ftofficer.com/2010/04/n-forms-of-call-instructions/
作者: ftofficer(張聰)
最近有一個需求,給你個地址,看看這個地址前面是不是一個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種尋址模式。0×10 – 0×17 對應於寄存器尋址。例如指令 CALL dword ptr [eax] :[eax]尋址對應的是 0×10,因此,該指令對應的二進制就是 FF 10。同理, CALL dword ptr [ebx] 是 FF 13,CALL dword ptr [esi] 是 FF 16,這些指令都是2個字節。有人也許問 CALL word ptr [eax] 是什么?抱歉,這不是一個合法的32位指令。
0×50-0×57部分需要帶一個 disp8,即 8bit 立即數,也就是一個字節。這個是基地址+8位偏移量的尋址模式。例如 CALL dword ptr [eax+10] 就是 FF 50 10 。注意雖然表當中寫的是 [eax] + disp8 這種形式,但是並不表示是取得 eax 指向的地址當中的值再加上 disp8,而是在eax上加上disp8再進行尋址。因此寫成 [eax+disp8] 更不容易引起誤解。后面的disp32也是一樣的。這個類型指令是3個字節。
0×90 – 0×97部分需要帶 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個字節。
仔細的人也許主要到了,在表當中,0×14, 0×15, 0×54和0×94是不一樣的。0×15比較簡單,這個要求 ModR/M后面跟上一個32位立即數作為地址。即常見的 CALL dword ptr [004F778e] 這種格式的,直接跳轉到一個固定內存地址處存放的值,常見於調用Windows的導出表。對應的二進制是 FF 15 00 4F 77 8E ,有6個字節。
0×14,0×54,0×94部分是最復雜的,因為這個時候,ModR/M不足以指定尋址方式,而是需要一個額外的字節,這個字節就是指令當中的第4個字節,SIB。同樣在手冊的2.1.3,緊跟着ModR/M的定義:
某些特定的ModR/M字節需要一個后續字節,稱為SIB字節。32位指令的基地址+偏移量,以及 比例*偏移量 的形式的尋址方式需要SIB字節。 SIB字節包括下列信息:
- scale(比例)域指定了放大的比例。
- index(偏移)域指定了用來存放偏移量 的寄存器。
- base (基地址)域用來標識存放基地址的寄存器。
0×14, 0×54, 0×94就是這里所說的“特定的ModR/M字節。這個字節后面跟着的SIB表示了一個復雜的尋址方式,典型的見於虛函數調用:
CALL dword ptr [ecx+4*eax]
就是調用ecx指向的虛表當中的第eax個虛函數。這個指令當中,因為沒有立即數,因此FF后面的字節就是0×14,而 [ecx+4*eax] 就需要用SIB字節來表示。在這個指令當中,ecx就是 Base,4是Scale,eax是Index。
那么,Base, Scale和Index是如何確定的呢?手冊上同樣有一張表(又是巨大的表):
32位尋址模式當中的SIB字節
列是Base,行是Index*Scale,例如[ecx+4*eax] 就是0×81。
根據這張表,CALL dword ptr [ecx+4*eax] 就是 FF 14 81 。由此可見,對於 0×14系列的來說,CALL指令就是 3個字節。
而 0×54 帶 8bit 立即數,就是對應於 CALL指令:CALL dword ptr [ecx+4*eax+xx],這個指令就是 FF 54 81 xx,是4個字節。
同理,0×94帶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 |