本章將介紹匯編語言最大的優勢之一:基本的二進制移位和循環移位技術。實際上,位操作是計算機圖形學、數據加密和硬件控制的固有部分。實現位操作的指令是功能強大的工具,但是高級語言只能實現其中的一部分,并且由于高級語言要求與平臺無關,所以這些指令在一定程度上被弱化了。本章將展示一些對移位操作的應用,包括乘除法的優化。
并非所有的高級編程語言都支持任意長度整數的運算。但是匯編語言指令使得它能夠加減幾乎任何長度的整數。本章還將介紹執行壓縮十進制整數和整數字符串運算的專用指令。
7.3 乘法和除法指令
32位模式下,整數乘法可以實現32位、16位或8位的操作。64位模式下,還可以使用64位操作數。MUL和IMUL指令分別執行無符號數和有符號數乘法。DIV指令執行無符號數除法,IDIV指令執行有符號數除法。
7.3.1 MUL 指令
32位模式下,MUL(無符號數乘法)指令有三種類型:第一種執行8位操作數與AL寄存器的乘法;第二種執行16位操作數與AX寄存器的乘法:第三種執行32位操作數與EAX寄存器的乘法。乘數和被乘數的大小必須保持一致,乘積的大小則是它們的一倍。這三種類型都可以使用寄存器和內存操作數,但不能使用立即數:
MUL reg/mem8
MUL reg/mem16
MUL reg/mem32
MUL指令中的單操作數是乘數。表7-2按照乘數的大小,列出了默認的被乘數和乘積。由于目的操作數是被乘數和乘數大小的兩倍,因此不會發生溢出。如果乘積的高半部分不為零,則MUL會把進位標志位和溢出標志位置1。因為進位標志位常常用于無符號數的算術運算,在此我們也主要說明這種情況。例如,當AX乘以一個16 位操作數時,乘積存放在 DX和 AX寄存器對中。其中,乘積的高16位存放在DX,低16位存放在AX。如果DX不等于零則進位標志位置1,這就意味著隱含的目的操作數的低半部分容納不了整個乘積。
有個很好的理由要求在執行MUL后檢查進位標志位,即,確認忽略乘積的高半部分是否安全。
1.MUL 示例
下述語句實現AL乘以BL,乘積存放在AX中。由于AH(乘積的高半部分)等于零因此進位標志位被清除(CF=0):
mov al, 5h
mov bl, 10h
mul bl ;AX = 0050h, CF = 0
下圖展示了寄存器內容的變化:
下述語句實現16位值2000h乘以0100h。由于乘積的高半部分(存放于DX)不等于委,因此進位標志位被置1:
.data
val1 WORD 2000h
val2 WORD 0100h
.code
mov ax, val1 ;AX = 2000h
mul val2 ;DX:AX = 00200000h, CF = 1
下述語句實現12345h乘以1000h,產生的64位乘積存放在EDX和EAX寄存器對中。EDX中存放的乘積高半部分為零,因此進位標志位被清除:
mov eax, 12345h
mov ebx, 1000h
mul ebx ;EDX:EAX = 0000000012345000h, CF = 0
下圖展示了寄存器內容的變化:
完整代碼測試筆記
;7.3.1_1.asm 7.3.1 MUL 指令INCLUDE Irvine32.inc.data
val1 WORD 2000h
val2 WORD 0100h.code
main PROC;8位乘法,乘積存放在AX中。由于AH(乘積的高半部分)等于零因此進位標志位被清除(CF=0):mov al, 5hmov bl, 10hmul bl ;AX = 0050h, CF = 0;16位乘法。由于乘積的高半部分(存放于DX)不等于委,因此進位標志位被置1:mov ax, val1 ;AX = 2000hmul val2 ;DX:AX = 00200000h, CF = 1;32位乘法,產生的64位乘積存放在EDX和EAX寄存器對中。EDX中存放的乘積高半部分為零,因此進位標志位被清除:mov eax, 12345hmov ebx, 1000hmul ebx ;EDX:EAX = 0000000012345000h, CF = 0INVOKE ExitProcess,0
main ENDP
END main
運行調試:
16位乘法:
32位乘法:
2.在 64 位模式下使用 MUL
64位模式下,MUL指令可以使用64位操作數。一個64位寄存器或內存操作數與RAX相乘,產生的128位乘積存放到RDX:RAX寄存器中。下例中,RAX乘以2,就是將RAX中的每一位都左移一位。RAX的最高位溢出到RDX寄存器,使得RDX的值為0000 0000 0000 0001h:
mov rax, 0FFFF0000FFFF0000h
mov rbx, 2
mul rbx ;RDX:RAX = 0000000000000001FFFE0001FFFE0000h
下面的例子中,RAX乘以一個64位內存操作數。該寄存器的值乘以16,因此,其中的每個十六進制數字都左移一位(一次移動4個二進制位就相當于乘以16)。
.data
multiplier QWORD 10h
.code
mov rax, 0AABBBBCCCCDDDDh
mul multiplier ;RDX:RAX=00000000000000000AABBBBCCCCDDDD0h
完整代碼測試筆記
;7.3.1_2.asm 7.3.1 MUL 指令
;2.在 64 位模式下使用 MULExitProcess PROTO.data
multiplier QWORD 10h.code
main PROC;64位乘法,產生的128位乘積存放到RDX:RAX寄存器中。mov rax, 0FFFF0000FFFF0000hmov rbx, 2mul rbx ;RDX:RAX = 0000000000000001FFFE0001FFFE0000h;下面的例子中,RAX乘以一個64位內存操作數。該寄存器的值乘以16,;因此,其中的每個十六進制數字都左移一位(一次移動4個二進制位就相當于乘以16)。mov rax, 0AABBBBCCCCDDDDhmul multiplier ;RDX:RAX=00000000000000000AABBBBCCCCDDDD0hcall ExitProcess
main ENDP
END
運行調試:
7.3.2 IMUL指令
IMUL(有符號數乘法)指令執行有符號整數乘法。與MUL指令不同,IMUL會保留乘積的符號,實現的方法是,將乘積低半部分的最高位符號擴展到高半部分。x86指令集支持三種格式的IMUL 指令:單操作數、雙操作數和三操作數。單操作數格式中,乘數和被乘數大小相同,而乘積的大小是它們的兩倍。
單操作數格式 單操作數格式將乘積存放在AX、DX:AX或EDX:EAX中:
IMUL reg/mem8 ;AX=A *reg/mem8
IMUL reg/mem16 ;DX:AX=X*reg/mem16
IMUL reg/mem32 ;EDX:EAX=X*reg/mem32
和 MUL指令一樣,其乘積的存儲大小使得溢出不會發生。同時,如果乘積的高半部分不是其低半部分的符號擴展,則進位標志位和溢出標志位置1。利用這個特點可以決定是否忽略乘積的高半部分。
雙操作數格式(32 位模式) 32位模式中的雙操作數IMUL指令把乘積存放在第一個作數中,這個操作數必須是寄存器。第二個操作數(乘數)可以是寄存器、內存操作數和立即數。16位格式如下所示:
IMUL regl6,reg/mem16
IMUL reg16,imm8
IMUL regl6,imm16
32位操作數類型如下所示,乘數可以是32位寄存器、32位內存操作數或立即數(8位或32位):
IMULreg32,reg/mem32
IMULreg32,imm8
IMULreg32,imm32
雙操作數格式會按照目的操作數的大小來截取乘積。如果被丟棄的是有效位,則溢出標志位和進位標志位置1。因此,在執行了有兩個操作數的IMUL操作后,必須檢查這些標志位中的一個。
三操作數格式 32位模式下的三操作數格式將乘積保存在第一個操作數中。第二個操作數可以是16位寄存器或內存操作數,它與第三個操作數相乘,該操作數是一個8位或16位立即數:
IMUL reg16, reg/meml6, imm8
IMUL reg16, reg/meml6, imm16
而32位寄存器或內存操作數可以與8位或32位立即數相乘:
IMUL reg32, reg/mem32, imm8
IMUL reg32, reg/mem32, imm32
IMUL執行時,若乘積有效位被丟棄,則溢出標志位和進位標志位置1。因此,在執行了有三個操作數的IMU操作后,必須檢查這些標志位中的一個。
1.在 64 位模式下執行 IMUL
在64位模式下,IMUL指令可以使用64位操作數。在單操作數格式中,64位寄存器或內存操作數與RAX相乘,產生一個128位目符號擴展的乘積存放到RDX:RAX寄存器中在下面的例子中,RBX與RAX相乘,產生128位的乘積-16。
mov rax, -4
mov rbx, 4
imul rbx ;RDX=0FFFFFFFFFFFFFFFFh, RAX = -16
也就是說,十進制數-16在RAX中表示為十六進制FFFFFFFFFFFO,而RDX只包含了RAX的高位擴展,即它的符號位。
三操作數格式也可以用于64位模式。如下例所示,被乘數(-16)乘以4,生成RAX中的乘積-64:
.data
multiplicand QWORD -16
.code
imul rax, multiplicand, 4 ;RAX=0FFFFFFFFFFFFFC0 (-64)
無符號乘法 由于有符號數和無符號數乘積的低半部分是相同的,因此雙操作數和三操作數的IMUL指令也可以用于無符號乘法。但是這種做法也有一點不便的地方:進位標志位和溢出標志位將無法表示乘積的高半部分是否為零。
完整代碼測試筆記
;7.3.2_1.asm 7.3.2 IMUL指令
;1.在 64 位模式下執行 IMULExitProcess PROTO.data
multiplicand QWORD -16.code
main PROC;單操作數乘法mov rax, -4mov rbx, 4imul rbx ;RDX=0FFFFFFFFFFFFFFFFh, RAX = -16;3操作數乘法, 第1個操作數用來保存積的,第2個和第3個操作數才參與乘積計算imul rax, multiplicand, 4 ;RAX=0FFFFFFFFFFFFFC0 (-64)call ExitProcess
main ENDP
END
運行調試:
2.IMUL示例
下述指令執行48乘以4,乘積+192保存在AX中。雖然乘積是正確的,但是 AH不是AL的符號擴展,因此溢出標志位置1:
mov al, 48
mov bl, 4
imul bl ;AX=00C0h,OF=1
下述指令執行-4乘以4,乘積-16保存在AX中。AH是AL的符號擴展,因此溢出標志位清零:
mov al, -4
mov bl, 4
imul bl ;AX=FFF0h,OF=0
下述指令執行48乘以4,乘積+192保存在DX:AX中。DX是 AX的符號擴展,因此溢出標志位清零:
mov ax, 48
mov bx, 4
imul bx ;DX:AX=000000c0h,0F=0
下述指令執行32位有符號乘法(4823424*-423),乘積-2040308352保存在 EDX:EAX中。溢出標志位清零,因為EDX是EAX的符號擴展:
mov eax, +4823424
mov ebx, -423
imul ebx ;EDX:EAX=FFFFF86635D80h,OF=0
下述指令展示了雙操作數格式
.data
word1 SWORD 4
dword1 SDWORD 4
.code
mov ax, -16 ;AX=-16
mov bx, 2 ;BX =2
imul bx, ax ;BX =-32
imul bx, 2 ;BX =-64
imul bx, word1 ;BX=-256
mov eax, -16 ;EAX =-16
mov ebx, 2 ;EBX =2
imul ebx, eax ;EBX=-32
imul ebx, 2 ;EBX=-64
imul ebx, dword1 ;EBX=-256
雙操作數和三操作數IMUL指令的目的操作數大小與乘數大小相同。因此,有可能發生有符號溢出。執行這些類型的IMUL指令后,總要檢查溢出標志位。下面的雙操作數指令展示了有符號溢出,因為-64000不適合16位目的操作數:
mov ax, -32000
imul ax, 2 ;OF=1
下面的指令展示的是三操作數格式,包括了有符號溢出的例子:
.data
word1 SWORD 4
dword1 SDWORD 4
.code
imul bx, word1, -16 ;BX=wordl *-16
imul ebx, dword1, -16 ;EBX=dwordl*-16
imul ebx, dword1, -2000000000 ;有符號溢出!
完整代碼測試筆記
;7.3.2_2.asm 7.3.2 IMUL乘法INCLUDE Irvine32.inc.data
word1 SWORD 4
dword1 SDWORD 4.code
main PROC;下述指令執行48乘以4,乘積+192保存在AX中。 有溢出mov al, 48mov bl, 4imul bl ;AX=00C0h,OF=1;下述指令執行-4乘以4,乘積-16保存在AX中。無溢出mov al, -4mov bl, 4imul bl ;AX=FFF0h,OF=0;下述指令執行48乘以4,乘積+192保存在DX:AX中。無溢出mov ax, 48mov bx, 4imul bx ;DX:AX=000000c0h,0F=0;下述指令執行32位有符號乘法(4823424*-423),乘積-2040 308 352保存在 EDX:EAX中。溢出標志位清零,因為EDX是EAX的符號擴展:mov eax, +4823424mov ebx, -423imul ebx ;EDX:EAX=FFFFF86635D80h,OF=0;下述指令展示了雙操作數格式mov ax, -16 ;AX=-16mov bx, 2 ;BX =2imul bx, ax ;BX =-32imul bx, 2 ;BX =-64imul bx, word1 ;BX=-256mov eax, -16 ;EAX =-16mov ebx, 2 ;EBX =2imul ebx, eax ;EBX=-32imul ebx, 2 ;EBX=-64imul ebx, dword1 ;EBX=-256;下面的雙操作數指令展示了有符號溢出,因為-64000不適合16位目的操作數:mov ax, -32000imul ax, 2 ;OF=1;下面的指令展示的是三操作數格式,包括了有符號溢出的例子,imul bx, word1, -16 ;BX=wordl *-16imul ebx, dword1, -16 ;EBX=dwordl*-16imul ebx, dword1, -2000000000 ;有符號溢出!INVOKE ExitProcess,0
main ENDP
END main
7.3.3 測量程序執行時間
通常,程序員發現用測量執行時間的方法來比較一段代碼與另一段代碼執行的性能是很有用的。Microsoft Windows API為此提供了必要的工具,Irvine32庫中的GetMseconds 過程可使其變得更加方便使用。該過程獲取系統自午夜過后經過的毫秒數。在下面的代碼示例中,首先調用GetMseconds,這樣就可以記錄系統開始時間。然后調用想要測量其執行時間的過程(FirstProcedureToTest)。最后,再次調用GetMseconds,計算開始時間和當前毫秒數的差值:
.data
startTime DWORD
procTimel DWORD2
procTime2 DWORD?
.code
call GetMseconds ;獲得開始時間
mov startTime,eax
.
call FirstProcedureToTest
.
call GetMseconds ;獲得結束時間
sub eax,startTime ;計算執行花費的時間
mov procTimel,eax ;保存執行花費的時間
當然,兩次調用GetMseconds會消耗一點執行時間。但是在衡量兩個代碼實現的性能時間之比時,這點開銷是微不足道的。現在,調用另一個被測試的過程,并保存其執行時間(procTime2 ):
.code
call GetMseconds ;獲得開始時間
mov startTime,eax
.
call SecondProcedureToTest
.
call GetMseconds ;獲得結束時間
sub eax,startTime ;計算執行花費的時間
mov procTime2,eax ;保存執行花費的時間
則procTimel和procTime2的比值就可以表示這兩個過程的相對性能,
MUL、IMUL與移位的比較
對老的x86處理器來說,用移位操作實現乘法和用MUL、IMUL指令實現乘法之間有著明顯的性能差異。可以用 GetMseconds 過程比較這兩種類型乘法的執行時間。下面的兩個過程重復執行乘法,用常量LOOPCOUNT決定重復的次數:
;7.3.3.asm 7.3.3 測量程序執行時間INCLUDE Irvine32.inc.data
intval DWORD 5
startTime DWORD ?
LOOP_COUNT = 0FFFFFFFFh.code
;------------------------------------------------------------
;用SHL執行EAX乘以36,執行次數為LOOP_COUNT
;-------------------------------------------------------------
mult_by_shifting PROCmov ecx, LOOP_COUNT
L1: push eax ;保存原始EAXmov ebx, eaxshl eax, 5shl ebx, 2add eax, ebxpop eaxloop L1 ;恢復EAXret
mult_by_shifting ENDP;---------------------------------------------------------------
;用MUL執行EAX乘以36,執行次數為LOOP_COUNT
;---------------------------------------------------------------
mult_by_MUL PROCmov ecx, LOOP_COUNT
L1: push eax ;保存原始 EAXmov ebx, 36mul ebxpop eax ;恢復EAXloop L1ret
mult_by_MUL ENDP
;下述代碼調用multibyshifting,并顯示計時結果。完整的代碼實現參見本書第7章的CompareMult.asm程序:
main PROC;第1次調用call GetMseconds ;獲取開始時間mov startTime, eaxmov eax, intval ;開始乘法call mult_by_shiftingcall GetMseconds ;獲取結束時間sub eax, startTimecall WriteDec ;顯示乘法執行花費的時間call Crlf ;換行;第2次調用call GetMseconds ;獲取開始時間mov startTime, eaxmov eax, intval ;開始乘法call mult_by_MULcall GetMseconds ;獲取結束時間sub eax, startTimecall WriteDec ;顯示乘法執行花費的時間INVOKE ExitProcess,0
main ENDPEND main
運行調試:
intel CORE i5處理器測試效果, mul指令比移位指令快。下面是書上的結論:
用同樣的方法調用mult_by_MUL,在傳統的4GHz奔騰4處理器上運行的結果為:SHL方法執行時間是6.078秒,MUL方法執行時間是20.718秒。也就是說,使用MUL指令速度會慢 2.41倍。但是,在近期的處理器上運行同樣的程序,調用兩個函數的時間是完全一樣的。這個例子說明,Intel在近期的處理器上已經設法大大地優化了MUL和IMUL指令。
7.3.4 DIV 指令
32位模式下,DIV(無符號除法)指令執行8位、16位和32位無符號數除法。其中單寄存器或內存操作數是除數。格式如下:
DIV reg/mem8
DIV reg/mem16
DIV reg/mem32
下表給出了被除數、除數、商和余數之間的關系,
64 位模式下,DIV指令用RDX:RAX作被除數,用64位寄存器和內存操作數作除數商存放到RAX,余數存放在RDX中。
DIV 示例
下述指令執行8位無符號除法(83h/2),生成的商為41h,余數為1:
mov ax, 0083h ;被除數
mov bl, 2 ;除數
div bl ;AL= 41h, AH=01h
下圖展示了寄存器內容的變化:
下述指令執行16位無符號除法(8003h/100h),生成的商為80h,余數為3。DX包含的是被除數的高位部分,因此在執行 DIV指令之前,必須將其清零:
mov dx, 0 ;清除被除數高16位
mov ax, 8003h ;被除數的低16位
mov cx, 100h ;除數
div cx ;AX=0080h,DX= 0003h
下圖展示了寄存器內容的變化:
下述指令執行32位無符號除法,其除數為內存操作數:
.data
dividend QWORD 0000000800300020h
divisor DWORD 00000100h
.code
mov edx, DWORD PTR dividend + 4 ;高雙字
mov eax, DWORD PTR dividend ;低雙字
div divisor ;EAX=08003000h,EDX=00000020h
下圖展示了寄存器內容的變化:
下面的64位除法生成的商(0108000000003330h)在RAX中,余數(0000000000000020h)在RDX中:
.data
dividend_hi QWORD 0000000000000108h
dividend_lo QWORD 0000000033300020h
divisor QWORD 0000000000010000h
.code
mov rdx, dividend_hi
mov rax, dividend_lo
div divisor ;RAX=0108000000003330;RDX=0000000000000020
請注意,由于被64k除,被除數中的每個十六進制數字是如何右移4位的。(若被16除,則每個數字只需右移一位。)
完整代碼測試筆記:
;7.3.4.asm 7.3.4 DIV 指令 (64位環境)ExitProcess PROTO.data
dividend QWORD 0000000800300020h
divisor DWORD 00000100h
dividend_hi QWORD 0000000000000108h
dividend_lo QWORD 0000000033300020h
divisor_64b QWORD 0000000000010000h.code
main PROC;下述指令執行8位無符號除法(83h/2),生成的商為41h,余數為1:mov ax, 0083h ;被除數mov bl, 2 ;除數div bl ;AL= 41h, AH=01h;下述指令執行16位無符號除法(8003h/100h),生成的商為80h,余數為3。mov dx, 0 ;清除被除數高16位mov ax, 8003h ;被除數的低16位mov cx, 100h ;除數div cx ;AX=0080h,DX= 0003h;下述指令執行32位無符號除法,其除數為內存操作數:mov edx, DWORD PTR dividend + 4 ;高雙字mov eax, DWORD PTR dividend ;低雙字div divisor ;EAX=08003000h,EDX=00000020h;下面的64位除法生成的商(0108000000003330h)在RAX中,余數(0000000000000020h)在RDX中:mov rdx, dividend_himov rax, dividend_lodiv divisor_64bcall ExitProcess
main ENDPEND
7.3.5 有符號數除法
有符號除法幾乎與無符號除法相同,只有一個重要的區別:在執行除法之前,必須對被除數進行符號擴展。符號擴展是指將一個數的最高位復制到包含該數的變量或寄存器的所有高位中。為了說明為何有此必要,讓我們先不這么做。下面的代碼使用MOV把-101賦給AX,即 DX:AX的低半部分:
.data
wordVal SWORD -101 ;009Bh
.code
mov dx, 0
mov ax, wordVal ;DX:AX = 0000009Bh (+155)
mov bx, 2 ;BX是除數
idiv bx ;DX:AX除以BX(有符號操作)
可惜的是,DX:AX中的009Bh并不等于-101,它等于+155。因此,除法產生的商為+77,這不是所期望的結果。而解決該問題的正確方法是使用CWD(字轉雙字)指令,在進行除法之前在DX:AX中對AX進行符號擴展:
.data
wordVal SWORD -101 ;009Bh
.code
mov dx, 0
mov ax, wordVal ;DX:AX = 0000009Bh (+155)
cwd ;DX:AX = FFFFFF9Bh (-101)
mov bx, 2 ;BX是除數
idiv bx ;DX:AX除以BX(有符號操作)
AX為-52h的補, DX為-1的補碼
本書第4章與MOVSX指令一起介紹過符號擴展。x86指令集有幾種符號擴展指令。首先了解這些指令,然后再將其應用到有符號除法指令IDIV中。
完整代碼測試筆記
;7.3.5.asm 7.3.5 有符號數除法INCLUDE Irvine32.inc.data
wordVal SWORD -101 ;009Bh.code
main PROC;下面的代碼使用MOV把-101賦給AX,即 DX:AX的低半部分:mov dx, 0mov ax, wordVal ;DX:AX = 0000009Bh (+155)mov bx, 2 ;BX是除數idiv bx ;DX:AX除以BX(有符號操作);在進行除法之前在DX:AX中對AX進行符號擴展:mov dx, 0mov ax, wordVal ;DX:AX = 0000009Bh (+155)cwd ;DX:AX = FFFFFF9Bh (-101)mov bx, 2 ;BX是除數idiv bx ;DX:AX除以BX(有符號操作)INVOKE ExitProcess,0
main ENDPEND main
1.符號擴展指令(CBW、CWD、CDQ)
Intel提供了三種符號擴展指令:CBW、CWD和CDO。CBW(字節轉字)指令將AL的符號位擴展到 AH,保留了數據的符號。如下例所示,9Bh(AL中)和FF9Bh(AX中)都等于十進制的 -101:
.data
byteVal SBYTE -101 ;9Bh
.code
mov al, byteVal ;AL = 9Bh
cbw ;AX = FF9Bh
CWD(字轉雙字)指令將AX的符號位擴展到DX:
.data
byteVal SWORD -101 ;FF9Bh
.code
mov ax, byteVal ;AX = FF9Bh
cwd ;DX:AX = FFFFFF9Bh
CDQ(雙字轉四字)指令將EAX的符號位擴展到EDX:
.data
byteVal SDWORD -101 ;FFFFFF9Bh
.code
mov eax, byteVal
cdq ;EDX:EAX = FFFFFFFFFFFFFF9Bh
完整代碼測試筆記
;7.3.5_1.asm 7.3.5 有符號數除法
;1.符號擴展指令(CBW、CWD、CDQ)INCLUDE Irvine32.inc.data
byteVal SBYTE -101 ;9Bh
wordVal SWORD -101 ;FF9Bh
dwordVal SDWORD -101 ;FFFFFF9Bh.code
main PROC;CBW(字節轉字)指令將AL的符號位擴展到 AH,保留了數據的符號。mov al, byteVal ;AL = 9Bhcbw ;AX = FF9Bh;CWD(字轉雙字)指令將AX的符號位擴展到DX:mov ax, wordVal ;AX = FF9Bh (+155)cwd ;DX:AX = FFFFFF9Bh (-101);CDQ(雙字轉四字)指令將EAX的符號位擴展到EDX:mov eax, dwordValcdq ;EDX:EAX = FFFFFFFFFFFFFF9BhINVOKE ExitProcess,0
main ENDPEND main
2.IDIV 指令
IDIV(有符號除法)指令執行有符號整數除法,其操作數與DIV指令相同。執行8位除法之前,被除數(AX)必須完成符號擴展。余數的符號總是與被除數相同。
示例1 下述指令實現-48除以5。IDIV執行后,AL中的商為-9,AH 中的余數為 -3:
.data
byteVal SBYTE -48 ;D0 十六進制
.code
mov al, byteVal ;被除數的低字節
cbw ;AL擴展到AH
mov bl, +5 ;除數
idiv bl ;AL=-9,AH=-3
下圖展示了AL是如何通過CBW指令符號擴展為AX的:
為了理解被除數的符號擴展為什么這么重要,現在在不進行符號擴展的前提下重復之前的例子。下面的代碼將AH初始化為0,這樣它就有了確定值,然后沒有用CBW指令轉換被除數就直接進行了除法:
.data
byteVal SBYTE -48 ;D0 十六進制
.code
mov ah, 0 ;被除數高字節
mov al, byteVal ;被除數低字節
mov bl, +5 ;除數
idiv bl ;AL=41,AH=3
執行除法之前,AX=00D0h(十進制數208)。IDIV把這個數除以5,生成的商為十進制數41,余數為3。這顯然不是正確答案。
示例2 16位除法要求AX符號擴展到DX。下例執行-5000除以256:
.data
wordVal SWORD -5000
.code
mov ax, wordVal ;被除數的低字
cwd ;AX擴展到DX
mov bx, +256 ;除數
idiv bx ;商AX=-19,余數DX=-136
示例3 32位除法要求EAX符號擴展到EDX。下例執行50000除以-256:
.data
dwordVal SDWORD +50000
.code
mov eax, dwordVal ;被除數的低雙字
cdq ;EAX擴展到EDX
mov ebx, -256 ;除數
idiv ebx ;商EAX=-195,余數EDX=+80
執行DIV和IDIV后,所有算術運算狀態標志位的值都不確定。
完整代碼測試筆記
;7.3.5_2.asm 7.3.5 有符號數除法
;2.IDIV 指令INCLUDE Irvine32.inc.data
byteVal SBYTE -48 ;D0 十六進制
wordVal SWORD -5000
dwordVal SDWORD +50000.code
main PROC;示例1 下述指令實現-48除以5。IDIV執行后,AL中的商為-9,AH 中的余數為 -3:mov al, byteVal ;被除數的低字節cbw ;AL擴展到AHmov bl, +5 ;除數idiv bl ;AL=-9,AH=-3;為了理解被除數的符號擴展為什么這么重要,現在在不進行符號擴展的前提下重復之前的例子。;下面的代碼將AH初始化為0,這樣它就有了確定值,然后沒有用CBW指令轉換被除數就直接進行了除法:mov ah, 0 ;被除數高字節mov al, byteVal ;被除數低字節mov bl, +5 ;除數idiv bl ;AL=41,AH=3;示例2 16位除法要求AX符號擴展到DX。下例執行-5000除以256:mov ax, wordVal ;被除數的低字cwd ;AX擴展到DXmov bx, +256 ;除數idiv bx ;商AX=-19,余數DX=-136;示例3 32位除法要求EAX符號擴展到EDX。下例執行50000除以-256:mov eax, dwordVal ;被除數的低雙字cdq ;EAX擴展到EDXmov ebx, -256 ;除數idiv ebx ;商EAX=-195,余數EDX=+80INVOKE ExitProcess,0
main ENDPEND main
3.除法溢出
如果除法操作數生成的商不適合目的操作數,則產生除法溢出(divideoverflow)。這將導致處理器異常并暫停執行當前程序。例如,下面的指令就產生了除法溢出,因為它的商(100h)對8位的AL目標寄存器來說太大了:
mov ax, 1000h
mov bl, 10h
div bl ;AL無法容納100h
運行這段代碼時,Visual Studio 就會產生如圖7-1所示的結果錯誤對話框。如果試圖運行除以零的代碼,也會顯示相同的對話框。
圖7-1 除法溢出錯誤示例
對此有個建議:使用32位除數和64位被除數來減少出現除法溢出條件的可能性。如下面的代碼所示,除數為EBX,被除數在EDX和EAX組成的64位寄存器對中:
mov eax, 1000h
cdq
mov ebx, 10h
div ebx ;EAX =00000100h
要預防除以零的操作,則在進行除法之前檢查除數:
mov ax, dividendmov bl, divisorcmp bl, 0 ;檢查除數je NoDivideZero ;為零?顯示錯誤div bl ;不為零:繼續.,
NoDivideZero: ;顯示"Attmpt to divide by zero"
7.3.6 實現算術表達式
第4章介紹了如何用加減指令實現算術表達式,現在還可以再加上乘法和除法指令。初看上去,實現算術表達式的工作似乎最好是留給編譯器的編寫者,但是動手研究一下還是能學到不少東西。讀者可以學習編譯器怎樣優化代碼。此外,與典型編譯器在乘法操作后檢查乘積大小相比,還能實現更好的錯誤檢查。進行32位操作數相乘時,絕大多數高級語言編譯器都會忽略乘積的高32位。而在匯編語言中,可以用進位標志位和溢出標志位來說明乘積是否為 32 位。這些標志位用法的解釋參見7.4.1節和7.4.2節。
提示 有兩種簡單的方法可以查看C++編譯器生成的匯編代碼:一種方法是用Visual Studio 調試時,在調試窗口中右鍵點擊,選擇Go to Disassembly。另一種方法是,在 Project菜單中選擇Properties,生成一個列表文件。在Configuration Properties選擇Microsoft MacroAssembler,再選擇ListingFile。在對話窗口中,將GeneratePreprocessed Source Listing設置為Yes,List All Available Information 也設置為 Yes。
示例1 使用32位無符號整數,用匯編語言實現下述C++語句:
var4 =(varl+var2)* var3;
這個問題很簡單,因為可以從左到右來處理(先加法再乘法)。執行了第二條指令后EAX存放的是 var1與var2之和。第三條指令中,EAX乘以var3,乘積存放在 EAX中:
mov eax, var1add eax, var2mul var3 ;EAX=EAX*var3jc tooBig ;無符號溢出?mov var4, eaxjmp next
tooBig:
如果 MUL指令產生的乘積大于 32 位,則 JC指令跳轉到有標號指令來處理錯誤
示例2 使用32位無符號整數實現下述C++語句:
var4=(varl*5)/(var2 -3);
本例有兩個用括號括起來的子表達式。左邊的子表達式可以分配給EDX:EAX,因此不必檢查溢出。右邊的子表達式分配給EBX,最后用除法完成整個表達式:
mov eax, var1 ;左邊的子表達式
mov ebx, 5
mul ebx ;EDX:EAX=乘積
mov ebx, var2 ;右邊的子表達式
sub ebx, 3 ;var2 - 3
div ebx ;最后的除法
mov var4, eax
示例3 使用32位有符號整數實現下述C++語句:
var4=(varl*5)/(-var2 %var3);
與之前的例子相比,這個例子需要一些技巧。可以先從右邊的表達式開始,并將其保存在EBX中。由于操作數是有符號的,因此必須將被除數符號擴展到EDX,再使用IDIV指令:
mov eax, var2 ;開始計算右邊的表達式
neg eax
cdq ;符號擴展被除數
idiv var3 ;EDX=余數
mov ebx, edx ;EBX=右邊表達式的結果
第二步,計算左邊的表達式,并將乘積保存在EDX:EAX中:
mov eax, -5 ;開始計算左邊表達式
imul var1 ;EDX:EAX=左邊表達式的結果
最后,左邊表達式結果(EDX:EAX)除以右邊表達式結果(EBX):
idiv ebx ;最后計算除法
mov var4, eax ;商
完整代碼測試筆記
;7.3.6.asm 7.3.6 實現算術表達式INCLUDE Irvine32.inc.data
var1 DWORD 10h
var2 DWORD 20h
var3 DWORD 30h
var4 DWORD 0
errorTips BYTE "Integer Overflow",0.code
main PROC;示例1 使用32位無符號整數,用匯編語言實現下述C++語句: var4 =(varl+var2)* var3;mov eax, var1add eax, var2mul var3 ;EAX=EAX*var3jc tooBig ;無符號溢出?mov var4, eaxjmp next
tooBig:mov edx, OFFSET errorTipscall WriteString ;顯示錯誤消息call Crlf
next:nop;示例2 使用32位無符號整數實現下述C++語句: var4=(varl*5)/(var2 -3);mov eax, var1 ;左邊的子表達式mov ebx, 5mul ebx ;EDX:EAX=乘積mov ebx, var2 ;右邊的子表達式sub ebx, 3 ;var2 - 3div ebx ;最后的除法mov var4, eax;示例3 使用32位有符號整數實現下述C++語句: var4=(varl*5)/(-var2 %var3);mov eax, var2 ;開始計算右邊的表達式neg eaxcdq ;符號擴展被除數idiv var3 ;EDX=余數mov ebx, edx ;EBX=右邊表達式的結果;第二步,計算左邊的表達式,并將乘積保存在EDX:EAX中:mov eax, -5 ;開始計算左邊表達式imul var1 ;EDX:EAX=左邊表達式的結果;最后,左邊表達式結果(EDX:EAX)除以右邊表達式結果(EBX):idiv ebx ;最后計算除法mov var4, eax ;商INVOKE ExitProcess,0
main ENDPEND main
7.3.7 本節回顧
1.請說明執行 MUL 指令和單操作數的IMUL指令時,不會發生溢出的原因。
答:保存乘積的寄存器大小是乘數和被乘數大小的兩位。比如,計算0FFh乘以0FFh,則乘積(FE01h)很容易就擴展到16位。
2.生成乘積時,單操作數 IMUL指令與 MUL 指令有何不同?
答:當相乘結果正好可以完全存放在乘積的低位寄存器時,IMUL對乘積符號擴展到高位乘積寄存器。而MUL則對乘積進行全零擴展。
3.什么情況下單操作數IMUL指令會將進位標志位和溢出標志位置1?
答:對IMUL來說,若乘積的高半部分不是其低半部分的符號擴展,那么進位標志位和沒溢出標志位置1.
4.DIV 指令中,若EBX為操作數,則商保存在哪個寄存器中?
答:EAX中
5.DIV 指令中,若BX為操作數,則商保存在哪個寄存器中?
答:AX中
6.MUL指令中,若BL為操作數,則乘積保存在哪個寄存器中?
答:乘積保存AX中
7.舉例說明,在調用IDIV 指令前,如何對其16位操作數進行符號擴展。
答:cwd指令對16位操作數進行擴展。
mov ax, dividendLow
cwd ;被除數符號擴展
mov bx, divisor
idiv bx
7.4 擴展加減法
擴展精度加減法(extended precision addition and subtraction)是對基本沒有大小限制的數進行加減法的技術。比如,在C++中,沒有標準運算符會允許兩個1024位整數相加。但是在匯編語言中,ADC(帶進位加法)和SBB(帶借位減法)指令就很適合進行這類操作。
7.4.1 ADC指令
ADC(帶進位加法)指令將源操作數和進位標志位的值都與目的操作數相加。該指令格式與ADD指令一樣,且操作數大小必須相同:
ADC reg,reg
ADC mem,reg
ADC reg,mem
ADC mem,imm
ADC reg,imm
例如,下述指令實現兩個8位整數相加(FFh+FFh),產生的16位和數存入DL:AL.其值為01FEh:
mov dl, 0
mov al, 0FFh
add al, 0FFh ;AL = FEh
adc dl, 0 ;DL/AL = 01FEh
下圖展示了這兩個數相加過程中的數據活動。首先,FFh與AL相加,生成FEh存人AL寄存器,并將進位標志位置1。然后,將0和進位標志位與DL寄存器相加:
同樣,下述指令實現兩個32位整數相加(FFFFFFFFh+FFFFFFFFh),產生的64位和數存人EDX:EAX,其值為:00000001FFFFFFFEh:
mov edx,0
mov eax,0FFFFFFFFh
add eax,0FFFFFFFFh
adc edx,0
完整代碼測試筆記
;7.4.1.asm 7.4.1 ADC指令
;ADC(帶進位加法)指令將源操作數和進位標志位的值都與目的操作數相加。INCLUDE Irvine32.inc.code
main PROC;下述指令實現兩個8位整數相加(FFh+FFh),產生的16位和數存入DL:AL.其值為01FEh:mov dl, 0mov al, 0FFhadd al, 0FFh ;AL = FEhadc dl, 0 ;DL/AL = 01FEh;下述指令實現兩個32位整數相加(FFFFFFFFh+FFFFFFFFh),產生的64位和數存人EDX:EAX,其值為:00000001FFFFFFFEh:mov edx,0mov eax,0FFFFFFFFhadd eax,0FFFFFFFFhadc edx,0INVOKE ExitProcess,0
main ENDP
END main
7.4.2 擴展加法示例
接下來將說明過程Extended Add實現兩個大小相同的擴展整數的加法。利用循環,該過程將兩個擴展整數當作并行數組實現加法操作。數組中每對數值相加時,都要包括前一次循環迭代執行的加法所產生的進位標志位。實現過程時,假設整數存儲在字節數組中,不過本例很容易就能修改為雙字數組的加法。
該過程接收兩個指針,存入ESI和EDI,分別指向參與加法的兩個整數。EBX寄存器指向緩沖區,用于存放和數,該緩沖區的前提條件是必須比兩個加數大一個字節。此外,過程還用 ECX接收最長加數的長度。兩個加數都需要按小端順序存放,即其最低字節存放在該數組的起始地址。過程代碼如下所示,添加了代碼行編號便于進行詳細討論:
;---------------------------------------------------------------------
;
;計算兩個以字節數組存放的擴展整數之和。
;接收:ESI和EDI為兩個加數的指針,
; EBX為和數變量指針,ECX為
; 相加的字節數。
;和數存儲區必須比輸入的操作數多一個字節.
;
;返回:無
;
;----------------------------------------------------------------------
Extended_Add PROCpushadclc ;清除進位標志位L1: mov al, [esi] ;取第一個數adc al, [edi] ;與第二個數相加pushfd ;保存進位標志位mov [ebx], al ;保存部分和add esi, 1 ;三個指針都加1add edi, 1add ebx, 1popfd ;恢復進位標志位loop L1 ;重復循環mov byte ptr [ebx], 0adc byte ptr [ebx], 0 ;清除和數高字節popad ;加上其他的進位ret
Extended_Add ENDP
當第16行和第17行將兩個數組的最低字節相加時,加法運算可能會將進位標志位置1。因此,第18行將進位標志位壓人堆棧進行保存就很重要,因為在循環重復時會用到進位標志位。第19行保存了和數的第一個字節,第20~22行將三個指針(兩個操作數,一個和數)都加1。第23 行恢復進位標志位,第24行將循環返回到第16行。(LOOP指令不會修改CPU的狀態標志位。)再次循環時,第17行進行的是第二對字節的加法,其中包括進位標志位的值。因此,如果第一次循環過程產生了進位,則第二次循環就要包括該進位。按照這種方式循環,直到所有的字節都完成了加法。然后,最后的第26行和第27行檢查操作數最高字節相加是否產生進位,若產生了進位,就將該值加到和數多出來的那個字節中。
下面的代碼示例調用Extended Add,并向其傳遞兩個8字節的整數。要注意為和數多分配一個字節:
.data
op1 BYTE 34h, 12h, 98h, 74h, 06h, 0A4h, 0B2h, 0A2h
op2 BYTE 02h, 45h, 23h, 00h, 00h, 87h, 10h, 80h
sum BYTE 9 dup(0)
.code
main PROCmov esi, OFFSET op1 ;第一個操作數mov edi, OFFSET op2 ;第二個操作數mov ebx, OFFSET sum ;和數mov ecx, LENGTHOF op1 ;字節數call Extended_Add;顯示和數。mov esi, OFFSET summov ecx, LENGTHOF sumcall Display_sumcall CrlfINVOKE ExitProcess,0
main ENDP
END main
上述程序的輸出如下所示,加法產生了一個進位:
0122C32B0674BB5736
過程 Display_Sum(來自同一個程序)按照正確的順序顯示和數,即從最高字節開始依次顯示到最低字節:
Display_Sum PROCpushad;指向最后一個數組元素add esi, ecxsub esi, TYPE BYTEmov ebx, TYPE BYTE
L1: mov al, [esi] ;取一個數組字節call WriteHexB ;顯示該字節sub esi, TYPE BYTE ;指向前一個字節loop L1popadret
Display_Sum ENDP
7.4.3SBB 指令
SBB(帶借位減法)指令從目的操作數中減去源操作數和進位標志位的值。允許使用的操作數與ADC指令相同。下面的示例代碼用32位操作數實現64位減法,EDX:EAX的值為0000000700000001h,從該值中減去2。低32位先執行減法,并設置進位標志位,然后高32位再進行包括進位標志位的減法:
mov edx, 7 ;高32位
mov eax, 1 ;低32位
sub eax, 2 ;減2
sbb edx, 0 ;高32位減法
圖7-2展示了這兩個數相減過程中的數據活動。首先,EAX減2,差值FFFFFFFFh存放在EAX中。由于是從小數中減去大數,因此產生借位,將進位標志位置1。然后,用SBB指令從EDX中減去0和進位標志位。
完整代碼測試筆記
;7.4.3.asm 7.4.3 SBB指令
;SBB(帶借位減法)指令從目的操作數中減去源操作數和進位標志位的值。允許使用的操作數與ADC指令相同。INCLUDE Irvine32.inc.code
main PROCmov edx, 7 ;高32位mov eax, 1 ;低32位sub eax, 2 ;減2sbb edx, 0 ;高32位減法INVOKE ExitProcess,0
main ENDP
END main
7.4.4 本節回顧
1.請描述 ADC指令。
答:ADC指令將目的操作數與源操作數和進位標志位相加。
2.請描述SBB指令。
答:SBB指令將目的數減去源操作數和進位村志位
3.執行下述指令后,EDX:EAX中的值是多少?
mov edx, 10h
mov eax, 0A0000000h
add eax, 20000000h
adc edx,0
答:EDX:EAX的值為00000010 C0000000h
4.執行下述指令后,EDX:EAX中的值是多少?
mov edx, 100h
mov eax, 80000000h
sub eax, 90000000h
sbb edx, 0
答:EDX:EAX的值為000000FF F0000000h
5.執行下述指令后,DX中的值是多少(STC將進位標志位置1)?
mov dx, 5
stc ;進位標志
mov ax, 10h
adc dx, ax
答:DX中的值為10h+1+5 = 16h