Intel X86架構數據的運算主要由通用寄存器處理,但浮點數例外,浮點數的運算由專門的FPU寄存器處理。二進制浮點數由三部分組成:符號,有效數字和階碼。這些格式都出自由IEEE組織制定的標準754-1985:以下是三種浮點數的格式:
●32位單精度數值包含1位符號、8位階碼,以及23位有效數字的小數部分。
●64位雙精度數值包含1位符號、11位階碼,以及52位有效數字的小數部分。
●80 位擴展雙精度數值包含1位符號、16 位階碼,以及 63 位有效數字的小數部分。
本篇介紹浮點數處理與指令編碼。
12.3 x86指令編碼
若要完全理解匯編語言操作碼和操作數,就需要花些時間了解匯編指令翻譯成機器語言的方法。由于Intel 指令集使用了豐富多樣的指令和尋址模式,因此這個問題相當復雜。首先以實地址模式的8086/8088為例來說明,之后,再展示32 位處理器帶來的變化。
Intel 8086處理器是第一個使用復雜指令集計算機(Complex Instruction Set Computer CISC)設計的處理器。這種指令集中包含了各種各樣的內存尋址、移位、算術運算、數據傳送和邏輯操作。與RISC(精簡指令集計算機,Reduced Instruction Set Computer)指令相比,Intel 指令在編碼和解碼方面有些復雜。指令編碼(encode)是指將匯編語言指令及其操作數轉換為機器碼。指令解碼(decode)是指將機器指令轉換為匯編語言。對 Intel 指令編碼和解碼的逐步解釋至少將有助于喚起對MASM 作者們辛苦工作的理解和欣賞。
12.3.1 指令格式
一般的x86機器指令格式(圖12-6)包含了一個指令前綴字節、操作碼、Mod R/M 字節、伸縮索引字節(SIB)、地址位移和立即數。指令按小端順序存放,因此前綴字節位于指令的起始地址。每條指令都有一個操作碼,而其他字段則是可選的。少數指令包含了全部字段,平均來看,絕大多數指令都有2個或3個字節。下面是對指令字段的簡介:
●指令前綴 覆蓋默認操作數大小。
●操作碼 (操作代碼)指定指令的特定變體。比如,按照使用的參數類型,指令 ADD有 9種不同的操作碼。
●Mod R/M 字段指定尋址模式和操作數。符號“R/M”代表的是寄存器和模式。表12-18列出了Mod字段,表12-19給出了當Mod=10b時16 位應用程序的R/M 字段。
●伸縮索引字節 (scale indexbyte,SIB)用于計算數組索引偏移量。
●地址位移 字段保存了操作數的偏移量,在基址-偏移量或基址-變址-偏移量尋址模式中,該字段還可以與基址或變址寄存器相加。
●立即數 字段保存了常量操作數。
表12-18 Mod字段取值 | |
Mod | 位移 |
00 | DISP=0,位移低半部分和高半部分都無定義(除非r/m=110) |
01 | DISP=位移低半部分符號擴展到16位,位移高半部分無定義 |
10 | DISP=位移高半部分和低半部分都有效 |
11 | R/M 字段包含的是寄存器編號 |
表12-19 16位R/M字段取值(Mod=10) | |||
R/M | 有效地址 | R/M | 有效地址 |
000 | [BX+SI]+D16 | 100 | [SI]+D16 |
001 | [BX+DI]+D16 | 101 | [DI]+D16 |
010 | [BP+SI]+D16 | 110 | [BP]+D16 |
011 | [BP+DI]+D16 | 111 | [BX]+D16 |
D16表示偏移量是16位的。
12.3.2 單字節指令
沒有操作數或只有一個隱含操作數的指令是最簡單的指令。這種指令只需要操作碼字段,字段值由處理器的指令集預先確定。表12-20列出了幾個常見的單字節指令。在這些指令中,INCDX指令好像是不應該出現的,它出現的原因是:指令集的設計者決定為某些常用指令提供獨特的操作碼。其結果是,為了代碼量和執行速度要對寄存器增量操作進行優化。
表12-20 單字節指令 | |||
指令 | 操作碼 | 指令 | 操作碼 |
AAA | 37 | LODSB | AC |
AAS | 3F | XLAT | D7 |
CBW | 98 | INC DX | 42 |
12.3.3 立即數送寄存器
立即操作數(常數)按照小端順序(起始地址為最低字節)添加到指令。首先關注的是立即數送寄存器指令,暫不考慮內存尋址的復雜性。將一個立即字送寄存器的MOV指令的編碼格式為:B8+rw dw,其中操作碼字節的值為B8+rw,表示將一個寄存器編號(0~7)與B8相加;dw為立即字操作數,低字節在低地址。(表12-21列出了操作碼使用的寄存器編號。)下面例子中出現的所有數值都為十六進制。
表12-21 寄存器編號(8/16位) | |||
寄存器 | 編號 | 寄存器 | 編號 |
AX/AL | 0 | SP/AH | 4 |
CX/CL | 1 | BP/CH | 5 |
DX/DL | 2 | SI/DH | 6 |
BX/BL | 3 | DI/BH | 7 |
示例: PUSH CX 機器指令為51。編碼步驟如下:
1)帶一個16位寄存器操作數的PUSH指令編碼為50。
2)CX的寄存器編碼為1,因此1+50得到操作碼為51。
示例: MOV AX,1 機器指令為B8 01 00(十六進制)。編碼過程如下:
1)立即數送16位寄存器的操作碼為 B8。
2)AX的寄存器編號為0,將0加上B8(參見表12-21)。
3)立即操作數(0001)按小端順序添加到指令(01,00)。
示例: MOV BX,1234h 機器指令為BB 34 12。編碼過程如下:
1)立即數送16位寄存器的操作碼為B8。
2)BX 的寄存器編號為3,將3加上 B8 得到操作碼BB。
3)立即操作數字節為34 12。
從實踐的角度出發,建議手動匯編一些MOV立即數指令來提高能力,然后通過MASM的源列表文件中的生成代碼來檢查匯編結果。
完整代碼測試筆記
;12.3.3.asm 12.3.3 立即數送寄存器
;將一個立即字送寄存器的MOV指令的編碼格式為:B8+rw dw,其中操作碼字節的值為B8+rw,
;表示將一個寄存器編號(0~7)與B8相加;dw為立即字操作數,低字節在低地址。INCLUDE Irvine32.inc.code
main PROC;push指令編碼為50, DX的寄存器編碼為2,因此2+50得到操作碼52push dx;立即數送16位寄存器的操作碼為B8,DX的寄存器編號為2,相加得到操作碼BA;立即操作數22h按小端順序添加到指令(22 00),所以機器碼為 BA 22 00mov dx, 22h;立即數送16位寄存器的操作碼為B8,CX的寄存器編號為1,相加得到操作碼B9;立即操作數1234h按小端順序添加到指令(34 12),所以機器碼為 B9 34 12mov cx, 1234hINVOKE ExitProcess, 0
main ENDP
END main
查看反匯編:
12.3.4 寄存器模式指令
在使用寄存器操作數的指令中,ModR/M字節用一個3位的標識符來表示寄存器操作數。表12-22列出了寄存器的位編碼。操作碼字段的位0用于選擇8位或16位寄存器1表示16位寄存器,0表示8位存器。
表12-22 Mod R/M字段標識寄存器 | |||
R/M | 寄存器 | R/M | 寄存器 |
000 | AX or Al | 100 | SP or AH |
001 | CX or CL | 101 | BP or CH |
010 | DX or DL | 110 | SI or DH |
011 | BX or BL | 111 | DI or BH |
比如,MovAx,Bx的機器碼為89D8。寄存器送其他操作數的16位MOV指令的Intel編碼為89r,其中/r表示操作碼后面帶一個Mod R/M字節。Mod R/M字節有三個字段(mod、reg和r/m)。例如,若 Mod R/M 的值為D8,則它包含如下字段:
mod | reg | r/m |
11 | 011 | 000 |
●6~7是mod字段,指定尋址模式。mod字段為11表示r/m字段包含的是一個寄存器編號。
●位3~5是reg字段,指定源操作數。在本例中,BX就是編號為011的寄存器。
●位0~2是r/m字段,指定目的操作數。本例中,AX是編號為000的寄存器。
表12-23 列出了更多使用8位和16位寄存器操作數的例子,
表 12-23 MOV 指令編碼和寄存器操作數的示例 | ||||
指令 | 操作碼 | mod | reg | r/m |
mov ax, dx | 8B | 11 | 000 | 010 |
mov al,dl | 8A | 11 | 000 | 010 |
mov cx,dx | 8B | 11 | 001 | 010 |
mov cl,dl | 8A | 11 | 001 | 010 |
12.3.5 處理器操作數大小前綴
現在將注意力轉回到x86處理器(IA-32)的指令編碼。有些指令以操作數大小前綴開始,覆蓋了其修改指令的默認段屬性。問題是,為什么有指令前綴?在編寫8088/8086 指令集時,幾乎所有 256 個可能的操作碼都用于處理帶有8位和 16 位操作數的指令。當 Intel 開發32位處理器時,就需要想辦法發明新的操作碼來處理32位操作數,而同時還要保持與之前處理器的兼容性。對于面向 16 位處理器的程序,所有使用 32位操作數的指令都添加一個前綴字節。對于面向 32 位處理器的程序,默認為 32 位操作數,因此所有使用 16 位操作數的指令添加一個前綴字節。8 位操作數不需要前綴。
示例:16位操作數 現在對表 12-23 中的 MOV 指令進行匯編,以此為例來看看在16位模式下前綴字節是如何起作用的。.286 偽指令指明編譯代碼的目標處理器,確保不使用32位寄存器。下面的每條MOV指令都給出了其指令編碼:
.model small
.286
.stack 100h
.code
main PROCmov ax, dx ;8B C2mov al, dl ;8A C2
現在對32位處理器匯編相同的指令,使用.386 偽指令,默認操作數為 32 位。指令將包括16 位和 32 位操作數。第一條MOV指令(EAX、EDX)使用的是32 位操作數,因此不需要前綴。第二條MOV(AX、DX)指令由于使用的是16位操作數,因此需要操作數大小前綴(66):
.model small
.386
.stack 100h
.code
main PROCmov eax, edx ;8B C2mov ax, dx ;66 8B C2mov al, dl ;8A C2
12.3.6 內存模式指令
如果 Mod R/M 字節只用于標識寄存器操作數,那么 Intel 指令編碼就會相對簡單。實際上,Intel 匯編語言有著各種各樣的內存尋址模式,這就使得Mod R/M 字節編碼相當復雜。(指令集的復雜性是RISC設計支持者常見的批評理由。
Mod R/M 字節正好可以指定256 個不同組合的操作數。表 12-24 列出了Mod 00 時的Mod R/M字節(十六進制)。(完整的表格參見《Intel 64and IA-32 Architectures SoftwareDeveloper's Manual》,卷2A。)Mod R/M 字節編碼的作用如下:Mod 列中的兩位指定尋址模式的集合。比如,Mod 00 有 8 種可能的 R/M 數值(000b~111b),有效地址列給出了這些數值標識的操作數類型。
假設想要編碼MOV AX,[SI],Mod位為00b,R/M位為100b。從表12-19可知 AX的寄存器編號為000b,因此完整的Mod R/M 字節為00 000 100b 或04h:
mod | reg | r/m |
00 | 000 | 100 |
十六進制字節 04 在表12-24 的AX列第5 行。
MOV [SI],AL 的Mod R/M 字節還是一樣的(04h),因為寄存器AL 的編號也是000。現在對指令MOV [SI],AL進行編碼。8 位寄存器的傳送操作碼為88。Mod R/M 字節為04h,則機器碼為88 04。
MOV 指令示例
表12-25列出了8位和16位MOV指令所有的指令格式和操作碼。表 12-26 和表 12-27給出了表 12-25 中縮寫符號的補充信息。手動匯編 MOV指令時可以用這些表作為參考。(更多細節請參閱Intel 手冊。)
表12-28列出了更多的MOV指令,這些指令能手動匯編,且可以與表中的機器代碼比較。假設myWord的起始地址偏移量為0102h
12.3.7 本節回顧
1.寫出下列 MOV 指令的操作碼:
.data
myByte BYTE ?
myWord WORD ?
.codemov ax, @datamov ds, ax ;a. 8Emov ax, bx ;b. 8Bmov bl, al ;c. 8Amov al, [si] ;d. 8Amov myByte, al ;e. A2mov myWord, ax ;f. A3
2.寫出下列 MOV 指令的 Mod R/M 字節:
.data
array WORD 5 DUP(?)
.codemov ax, @datamov ds, ax ;a. D8mov dl, bl ;b. D3mov bl, [di] ;c. 1Dmov ax, [si+2] ;d. 44mov ax, array[si] ;e. 84mov array[di], ax ;f. 85
12.4 本章小結
二進制浮點數由三部分組成:符號、有效數字和階碼。Intel處理器使用了三種浮點數一進制存儲格式,這些格式都出自由IEEE組織制定的標準754-1985:二進制浮點數運算:
●32位單精度數值包含1位符號、8位階碼,以及23位有效數字的小數部分。
●64位雙精度數值包含1位符號、11位階碼,以及52位有效數字的小數部分。
●80 位擴展雙精度數值包含1位符號、16 位階碼,以及 63 位有效數字的小數部分。
若符號位為 1,則數值為負數;若該位為 0,則數值為正數。
浮點數的有效數字由小數點左右兩邊的十進制數字構成。
并非所有處于0到1之間的實數都可以在計算機內表示為浮點數,其原因是有效位的個數是有限的。
規格化有限數是指,能夠編碼為0到無窮之間的規格化實數的所有非零有限數值。正無窮(+∞)代表最大正實數,負無窮(-∞)代表最大負實數。NaN 為位模式,不表示有效浮點數。
Intel 8086 處理器被設計為只處理整數運算,因此 Intel 提供了獨立的 8087 浮點數協處理器(floating-pointcoprocessor)芯片與8086一起置于計算機的主板上。隨著Intel486的出現,浮點操作被整合到主 CPU 內,稱為浮點單元(FPU)。
FPU 有 8 個相互獨立的可尋址的 80 位寄存器,分別命名為RO~R7,它們構成一個寄存器堆棧。在計算時,浮點操作數以擴展實數的形式保存在FPU 堆棧中。內存操作數也可以用于計算。FPU 在把算術運算操作的結果保存到內存時,會把結果轉換為下述格式之一:整數、長整數、單精度數、雙精度數或者 BCD 碼。
Intel 浮點指令助記符用字母F開始,以區別于 CPU 指令。指令的第二個字母(通常為B或I)表示如何解釋內存操作數:B表示為操作數為二進制編碼的十進制數(BCD 碼),I表示操作數為二進制整數。如果沒有指定,那么內存操作數就假設為實數格式。
Intel 8086 是第一個使用復雜指令集計算機(CISC)設計的處理器。其龐大的指令集包含了各種各樣的內存尋址、移位、算術運算、數據傳送和邏輯操作。
指令編碼是指把匯編語言指令及其操作數轉換為機器碼。指令解碼是指將機器碼指令轉換為匯編語言指令及操作數。
x86機器指令格式包含一個可選的前綴字節、一個操作碼字段、一個可選的Mod R/M字節、若干可選的立即數字節,以及若干可選的內存偏移量字節。具備全部這些字段的指令很少。前綴字節覆蓋了目標處理器默認的操作數大小。操作碼字段是指令獨一無二的操作編碼。Mod R/M 字段指定了尋址模式和操作數。在使用寄存器操作數的指令中,Mod R/M 字節用一個3位標識符來表示每個寄存器操作數。