7.3 移位和循環移位的應用
7.3.1 多雙字移位
要對擴充精度整數(長整數)進行移位操作,可把它劃分為位元組數組、字數組或雙字數組,然後再對該數組進行移位操作。在記憶體中存儲數字時通常采用的方式是最低位元組在最低的位址位置上(小尾順序)。下面的步驟以一個雙位元組數組為例,說明了如何把這樣的一個數組右移移位:
ArraySize = 3
.data
array DWORD ArraySize DUP(?)
1.把ESI的值設定為array的偏移。
2.把最高位置[ESI+8]處的雙字右移一位,最低位複制到進位标志中。
3.把[ESI+4]處的值右移一位,最高位自動以進位标志的值填充,最低位複制到進位标志中。
4.把[ESI+0]處的雙字右移一位,其最高位自動以進位标志填充,其最低位複制到進位标志中。
下圖顯示了數組的内容及使用ESI間接引用的表示:
實作程式MultiShf.asm的代碼如下,程式中使用的是RCR指令,也可以使用SHRD指令:
.data
ArraySize = 3
array DWORD ArraySize DUP(99999999h) ;1001 1001...
.code
mov esi ,0
shr array[esi + 8]
rcr array[esi + 4]
rcr array[esi],1
...蛋疼,上面的那個我的卡了好幾分鐘才明白,我靠,一開始了解錯了。
7.3.2 二進制乘法
的二進制乘法指令(MUL和IMUL)相對于其他機器指令來說是比較緩慢的。彙編語言程式員通常會尋找更好的進行二進制乘法的方法,有時候移移位操作的優越性是顯而易見的。我們已經知道,在乘數是2的次幂的情況下,用SHL指令進行無符号數的乘法是相當高效的。無符号整數左移n位就相當于乘以2的n次幂。
例如,為了計算EAX乘以32
,就可以把36分解成(2的5cifang+2的2次方)
,然後用乘法配置設定率進行運算:
EAX * 32 = EAX * (32 + 4)
= (EAX * 32) + (EAX * 4)
下圖描述了123*32的乘法過程,積為4428:
乘數36的位2和位5是1,這些恰好是例子中的移位次數。
.code
mov eax , 123
mov ebx , eax
shl eax , 5
shl ebx , 2
add eax , ebx
7.3.3 顯示二進制數的資料位
一類常見的程式設計任務是要求把二進制整數轉換成ASCII二進制字元串以進行顯示。SHL指令這時就很有用了,因為SHL指令在每次操作數左移的時候,都會把最高位複制到進位标志中,下面的BinToAsc過程是一個簡單的實作。
;---------------------------------------------------------------
BinToAsc PROC
;
;Converts 32-bit binary integer to ASCII binary.
;Receives:EAX = binary integer,ESIpoints to buffer
;Returns: buffer filled with ASCII binary digits
;---------------------------------------------------------------
push ecx
push esi
中資料位的數目
L1: shl eax ,1 ;左移高位至進位标志中
選擇0作為預設數字
如果無進位,跳轉到L2
否則1送緩沖區
L2: inc esi ;下一個緩沖區位置
繼續循環,另外一位左移
pop esi
pop ecx
ret
BinToAsc ENDP
7.4 乘法和除法指令
和IMUL指令分别進行有符号整數和無符号整數的乘法操作。DIV指令進行無符号整數的除法操作,IDIV進行有符号整數的除法操作。
7.4.1 MUL指令
(無符号乘法)指令有三種格式:第一種将8位操作數與AL相乘;第二種将16位的操作數與AX相乘;第三種将32位的操作數與EAX相乘。乘數和被乘數大小必須相同,乘積的尺寸是乘數/被乘數大小的兩倍。三種格式都既接受寄存器操作數,也接收記憶體操作數,但是不接受立即數操作數。
MUL r/m8
MUL r/m16
MUL r/m32
指令中唯一的一個操作數是乘數。表7.2根據乘數大小的不同列出了被乘數和乘積,由于目的操作數(乘積)是乘數/被乘數大小的兩倍,是以不會發生溢出。如果積的高半部分不為0,就設定進位和溢出标志。由于進位标志通常用于服務号算術運算符,是以我們主要關注該标志。例如當AX與16位操作數相乘的時候,積存儲在DX:AX中。如果DX不為0,則進位标志置位。
指令後要檢查進位标志的一個理由:有時我們需要知道乘積的高半部分是否可被安全的忽略。
MUL指令的例子
下面的語句把AL和BL相乘,積在AX中,進位标志清零(CF=0),因為AH(乘積的高半部分)等于0:
mov al ,5h
mov bl ,10h
mul bl ;AX =50h ,CF = 0
下面的語句将16位數2000h和100h相乘,CF=1,因為乘積的高半部分DX不等于0;
.data
val1 WORD 2000h
val2 WORD 0100h
.code
mov ax ,val1 ;AX = 2000h
mul val2 ;DX:AX = 00200000h ,CF = 1
下面的語句将32位數12345h和1000h相乘得到一個64位的積,由EDX=0知CF=0;
mov eax ,12345h
mov ebx ,1000h ;EDX:EAX = 000012345000h ,CF = 0
mul ebx
7.4.2 IMUL指令
(有符号乘法)指令執行有符号整數的乘法運算,保留了成績的符号位。IMUL指令在IA-32指令集中由三種格式:單操作數、雙操作數和三操作數。在單操作數格式中,乘數和被乘數尺寸大小相同,乘積的大小是乘數/被乘數大小的兩倍(8086/8088處理器隻支援這種格式)。
單操作數格式:單操作數格式把乘積存儲在累加器(AX,DX:AX,EDX:EAX)中:
IMUL r/m8
IMUL r/m16
IMUL r/m32
和MUL指令一樣,IMUL指令的單操作數格式中乘積的尺寸大小是的溢出不可能發生。如果乘積的高半部分不是低半部分的符号擴充,進位标志和溢出标志位,可以用該特點确定乘積的高半部分是否可以忽略。
雙操作數格式:雙操作數格式中成績存儲在第一個操作數中,第一個操作數必須是寄存器,第二個操作數可以是寄存器、記憶體書或立即數,下面是16位操作數的格式:
IMUL r16,r/m16
IMUL r16 ,imm8
IMUL r16 ,imm16
下面是32位操作數的格式,乘數必須是一個32位的寄存器、32位的記憶體操作數或立即數(8位或者32位):
IMUL r32 ,r/m32
IMUL r32, imm8
IMUL r32 ,imm32
雙操作數格式會根據目的操作數的大小剪裁乘積。如果有效位丢失,則溢出标志和進位标志置位。使用雙操作數格式時,無比在執行完IMUL操作後檢查這些标志的值
三操作數格式:三操作數格式把乘積存儲在第一個操作數中,一個16位的寄存器可被一個8位或16位的立即數乘:
IMUL r16 ,r/m16 ,imm8
IMUL r16 ,r/m16 ,imm16
一個32位的寄存器可被一個8位或32位的立即數乘:
IMUL r32 ,r/m32 ,imm8
IMUL r32 ,r/m32 ,imm32
如果有效位丢失,則溢出标志和進位标志置位。使用三操作數格式時,務必在執行完IMUL操作後檢查這些标志的值。
無符号乘法:雙操作數和三操作數格式的IMUL指令也可用于進行無符号乘法。不過這樣做有一個缺陷:進位标志和溢出标志不能用來訓示乘積的高半部分是否為0.
7.4.3 乘法操作的基準(性能)測試
比較MUL,IMUL和移位做乘法指令的速度:
INCLUDE Irvine32.inc
.data
LOOP_COUNT = 0FFFFFFFFh
.data
intval DWORD 5
startTime DWORD ?
.code
main PROC
call GetMseconds
mov startTime ,eax
mov eax ,intval
call mult_by_MUL
call GetMseconds
sub eax ,startTime
call WriteDec
main ENDP
mult_by_shifting PROC
;
;EAX乘以36,使用SHL指令,重複LOOP_COUNT次
mov ecx ,LOOP_COUNT
L1: push eax
mov ebx ,eax
shl eax ,5
shl ebx ,2
add eax ,ebx
pop eax
loop L1
ret
mult_by_shifting ENDP
mult_by_MUL PROC
;
;EAX乘以36,使用SHL指令,重複LOOP_COUNT次
mov ecx ,LOOP_COUNT
L1 :
push eax
mov ebx ,36
mul ebx
pop eax
loop L1
ret
mult_by_MUL ENDP
END main
按照書中作者的測試結果是,移位乘法
用時6.078s而mul則用了20.718s,但是我的測試結果是兩個差不多,但是這個并不和作者說的事情沖突,因為我目前使用的換将是vs2012+masm
vs會對代碼進行優化處理。
7.4.4 DIV指令
DIV(無符号除法)指令執行8位、16位和32位無符号整數的除法運算。指令中唯一的一個寄存器或記憶體操作數是除數,DIV的指令格式是:
DIV r/m8
DIV r/m16
DIV r/m32
下表顯示了被除數、除數、商以及餘數之間的關系。
DIV例子
下面的指令執行8位無符号數的除法(83h/2),上市401,餘數是1:
被除數
除數
div bl ;AL = 41h ,AH = 01h
執行16位無符号除法(8003h/100h),商是80h,餘數是3。DX中存放的是被除數的高位,是以在執行DIV指令之前DX必須首先清零:
mov dx ,0 ;清除被除數的高位
mov ax ,8003h ;被除數的地位
mov cx ,100h ;除數
div cx ;AX = 0080h ,DX = 003h
執行32位無符号除法,指令使用記憶體操作數作為除數:
.data
dividend QWORD 0000000800300020h
divisor DWORD 00000100h
.code
高雙子
低雙字
div divisor ;EAX = 08003000h ,EDX = 00000020h
7.4.5 有符号整數除法
有符号整數除法和無符号幾乎完全相同,唯一不同:在進行除法操作之前,隐含的被除數北徐進行符号擴充。
符号擴充指令(CBW,CWD,CDQ)
有符号除法指令中的被除數在進行除法操作之前通常要進行符号擴充。Intel提供了三條符号擴充指令:CBW CWD CDQ。CBW指令(位元組符号擴充至字)擴充AL的符号位值AH中,保留了數字的符号。在下面的例子中(AL中的)9Bh和(AX中的)FF9Bf都等于-101:
.data
byteVal SBYTE -101 ;9Bh
.code
mov al ,byteVal ;AL = 9Bh
cbw ;AX = FF9Bh
同理 CWD(字元号擴充至雙字)指令擴充AX的符号位至DX中:
...
CDQ(雙子位元組擴充至8位元組)指令擴充EAX的符号位至EDX中:
...
IDIV指令
IDIV(有符号除法)指令進行有符号整數的除法運算,使用的操作數格式與DIV指令相同。在進行8位除法之前,被除數(AX)必須進行符号擴充,餘數的符号和被除數總是相同。
例子:
.data
byteVal SBYTE -48
.code
mov al ,byteVal ;被除數
cbw ;擴充AL至AH
mov bl ,5 ;除數
idiv bl ;AL = -9 ,AH = -3
.data
wordVal SWORD -5000
.code
mov ax ,wordVal ;被除數的低半部分
cwd ;擴充AX值DX
mov bx ,+256 ;除數
idiv bx ;商AX = -19
餘數DX = -136
.data
dwordVal SDWORD +50000
.code
mov eax ,dwordVal ;被除數的低半部分
cdq ;擴充EAX至EDX
mov ebx ,-256 ;除數
idiv ebx ;EAX = -195 餘數 EDX =
+80 (注意此時餘數符号)
在執行DIV和IDIV指令後所有的算術狀态标志都是不确定的。
除法溢出
在除法操作産生的上太大,目的操作數無法容納的時候,就會導緻除法溢出,這會導緻CPU觸發一個中斷,目前程式将被終止。例如下面:
mov ax ,1000h
mov bl ,10h
div bl ;AL不能容納100h
7.4.6 算術表達式的實作
var4 = (var1 + var2) * var3
用彙編實作下:
mov eax ,var1
add eax ,var2
mul var3 ;EAX = EAX * var3
無符号溢出?
mov var4 ,eax
jmp next
tooBig: ;顯示錯誤資訊
然後看下用C++編譯之後
vs反彙編會是什麼樣?