本章將介紹匯編語言最大的優勢之一:基本的二進制移位和循環移位技術。實際上,位操作是計算機圖形學、數據加密和硬件控制的固有部分。實現位操作的指令是功能強大的工具,但是高級語言只能實現其中的一部分,并且由于高級語言要求與平臺無關,所以這些指令在一定程度上被弱化了。本章將展示一些對移位操作的應用,包括乘除法的優化。
并非所有的高級編程語言都支持任意長度整數的運算。但是匯編語言指令使得它能夠加減幾乎任何長度的整數。本章還將介紹執行壓縮十進制整數和整數字符串運算的專用指令。
7.5 ASCI和非壓縮十進制運算
(7.5節討論的指令只能用于32位模式編程。)到目前為止,本書討論的整數運算處理的都是二進制數。雖然CPU用二進制運算,但是也可以執行ASCI十進制串的運算。使用后者進行運算,對用戶而言既便于輸入也便于在控制臺窗口顯示,因為不用進行二進制轉換假設程序需要用戶輸人兩個數,并將它們相加。若用戶輸人3402和1256,則程序輸出如下所示:
輸入第一個數:3402
輸入第二個數:1256
和 數: 4658
有兩種方法可以計算并顯示和數:
1)將兩個操作數都轉換為二進制,進行二進制加法,再將和數從二進制轉換為ASCII數字串。
2)直接進行數字串的加法,按序相加每對ASCI數字(2+6、0+5、4+2、3+1)。和數為ASCII數字串,因此可以直接顯示在屏幕上。
第二種方法需要在執行每對ASCI數字相加后,用特殊指令來調整和數。有四類指令用于處理 ASCII加法、減法、乘法和除法,如下所示:
AAA | (執行加法后進行 ASCI 調整) | AAM | (執行乘法后進行 ASCI 調整) |
AAS | (執行減法后進行 ASCII 調整) | AAD | (執行除法前進行 ASCII 調整) |
ASCII 十進制數和非壓縮十進制數 非壓縮十進制整數的高4位總是為零,而ASCII十進制數的高4位則應該等于0011b。在任何情況下,這兩種類型的每個數字都占用一個字節。下面的例子展示了3402用這兩種類型存放的格式:
盡管ASCI運算執行速度比二進制運算要慢很多,但是它有兩個明顯的優點:
●不必在執行運算之前轉換串格式。
●使用假設的十進制小數點,使得實數操作不會出現浮點運算的舍入誤差的危險。
ASCII 加減法運行操作數為ASCI格式或非壓縮十進制格式,但是乘除法只能使用非壓縮十進制數。
7.5.1 AAA 指令
在 32位模式下,AAA(加法后的ASCI調整)指令調整ADD或ADC指令的二進制運算結果。設兩個ASCI數字相加,其二進制結果存放在AL中,則AAA將AL轉換為兩個非壓縮十進制數字存人AH和AL。一旦成為非壓縮格式,通過將AH和AL與30h進OR運算,很容易就能把它們轉換為 ASCII碼。
下例展示了如何用AAA指令正確地實現ASCI數字8加2。在執行加法之前,必須把AH清零,否則它將影響AAA執行的結果。最后一條指令將AH和AL轉換為ASCI數字:
mov ah, 0
mov al, '8' ;AX = 0038h
add al, '2' ;AX = 006Ah
aaa ;AX = 0100h(結果進行ASCII調整)
or ax, 3030h ;AX = 3130h ='10'(轉換為ASCII碼)
使用 AAA 實現多字節加法
現在來查看一個過程,其功能為實現包含了隱含小數點的ASCII十進制數值相加。由于每次數字相加的進位標志位都要傳遞到更高位,因此,過程的實現要比想象的更復雜一些。下面的偽代碼中,acc代表的是一個8位的累加寄存器:
esi (index)=length of first number - 1
edi (index)=length of first number
ecx =lengthoffirst number
set carry value to 0
Loopacc = first number[esi]add previous carry to accsave carry in carrylacc += second_number[esi]OR the carry with carry1sum[edi] = accdec edi
Until ecx == 0
Store last carry digit in sum
進位值必須總是被轉換為ASCI碼。將進位值與第一個操作數相加時,就需要用AAA來調整結果。程序清單如下:
;ASCII_add.asm ASCII加法
;對有隱含固定小數點的串執行ASCII運算。INCLUDE Irvine32.incDECIMAL_OFFSET = 5 ;距離串右側的偏移量
.data
decimal_one BYTE '100123456789765' ;1001234567.89765
decimal_two BYTE '900402076502015' ;9004020765.02015
sum BYTE (SIZEOF decimal_one + 1) DUP(0), 0.code
main PROC;從最后一個數字位開始mov esi, SIZEOF decimal_one - 1mov edi, SIZEOF decimal_onemov ecx, SIZEOF decimal_onemov bh, 0 ;進位值清零
L1: mov ah, 0 ;執行加法前清除AHmov al, decimal_one[esi] ;取第一個數字add al, bh ;加上之前的進位值aaa ;調整和數AH=進位值mov bh, ah ;將進位保存到carry1or bh, 30h ;將其轉換為ASCII碼add al, decimal_two[esi] ;加第二個數字aaa ;調整和數AH=進位值or bh, ah ;進位值與 carry1進行 OR運算or bh, 30h ;將其轉換為ASCII 碼or al, 30h ;將AL轉換為ASCII碼mov sum[edi], al ;將AL保存到sumdec esi ;后退一個數字dec edi loop L1mov sum[edi], bh ;保存最后的進位值;顯示和數字符串。mov edx, OFFSET sumcall WriteStringcall CrlfINVOKE ExitProcess,0
main ENDP
END main
程序輸出如下所示,和數沒有顯示十進制小數點:
7.5.2 AAS 指令
32位模式下,AAS(減法后的ASCII調整)指令緊隨SUB或SBB指令之后,這兩條指令執行兩個非壓縮十進制數的減法,并將結果保存到AL中。AAS指令將AL轉換為ASCII碼的數字形式。只有減法結果為負時,調整才是必需的。比如,下面的語句實現ASCI碼數字8減去9:
;7.5.2.asm 7.5.2 AAS 指令
;下面的語句實現ASCI碼數字8減去9:.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
val1 BYTE '8'
val2 BYTE '9'.code
main PROCmov ah, 0mov al, val1 ;AX = 0038hsub al, val2 ;AX = 00FFhaas ;AX = 0FF09hpushf ;保存進位標志位or al, 30h ;AX = 0FF39hpopf ;恢復進位標志位INVOKE ExitProcess,0
main ENDP
END main
執行SUB指令后,AX等于00FFh。AAS指令將AL轉換為09h,AH減1等于FFh并且把進位標志位置1。
7.5.3 AAM 指令
32位模式下,MUL執行非壓縮十進制乘法,AAM(乘法后的ASCII調整)指令轉換由其產生的二進制乘積。乘法只能使用非壓縮十進制數。下面的例子實現5乘以6,并調整AX中的結果。調整后,AX=0300h,非壓縮十進制表示為30:
;7.5.3.asm 7.5.3 AAM指令
;下面的例子實現5乘以6,并調整AX中的結果。
;調整后,AX=0300h,非壓縮十進制表示為30:.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
ascVal BYTE 05h, 06h.code
main PROCmov bl, ascVal ;第1個操作數mov al, [ascVal+1] ;第2個操作數mul bl ;AX=001Eh aam ;AX=0300h INVOKE ExitProcess,0
main ENDP
END main
7.5.4 AAD 指令
32位模式下,AAD(除法之前的ASCII調整)指令將AX中的非壓縮十進制被除數轉換為二進制,為執行DIV指令做準備。下面的例子把非壓縮0307h轉換為二進制數,然后除以5。DIV指令在AL中生成商07h,在AH中生成余數02h:
;7.5.4.asm 7.5.4 AAD指令
;下面的例子把非壓縮0307h轉換為二進制數,然后除以5。
;DIV指令在AL中生成商07h,在AH中生成余數02h:.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.data
quotient BYTE ?
remainder BYTE ?.code
main PROCmov ax, 0307h ;被除數aad ;AX = 0025hmov bl, 5 ;除數div bl ;AX=0207hmov quotient, almov remainder, ahINVOKE ExitProcess,0
main ENDP
END main
7.5.5 本節回顧
1.編寫一條指令,將 AX中的一個兩位非壓縮十進制整數轉換為十進制的 ASCII碼。
答:or ax, 3030h
2.編寫一條指令,將 AX中的一個兩位 ASCII碼十進制整數轉換為非壓縮十進制形式
答:and ax, 0F0Fh
3.編寫有兩條指令的序列,將 AX中的一個兩位 ASCII 碼十進制整數轉換為二進制。
答:and ax, 0F0Fh ;轉換為非壓縮形式
aad
4.編寫一條指令,將 AX中的一個無符號二進制整數轉換為非壓縮十進制數。
答:aam
7.6 壓縮十進制運算
(7.6節討論的指令僅用于32位編程模式。)壓縮十進制數的每個字節存放兩個十進制數字,每個數字用4位表示。如果數字個數為奇數,則最高的半字節用零填充。存儲大小可變:
bcd1 QWORD 2345673928737285h ;十進制數 2345673928737285
bcd2 DWORD 12345678h ;十進制數12345678
bcd3 DWORD 08723654h ;十進制數8723654
bcd4 WORD 9345h ;十進制數9345
bcd5 WORD 0237h ;十進制數237
bcd6 BYTE 34h ;十進制數34
壓縮十進制存儲至少有兩個優勢:
●數據幾乎可以包含任何個數的有效數字。這使得以很高的精度執行計算成為可能
●實現壓縮十進制數與 ASCII碼之間的相互轉換相對簡單。
DAA(加法后的十進制調整)和DAS(減法后的十進制調整)這兩條指令調整壓縮十進制數加減法的結果。可惜的是,目前還沒有與乘除法有關的相似指令。在這些情況下,相乘或相除的數必須是非壓縮的,執行后再壓縮。
7.6.1 DAA 指令
32位模式下,ADD或ADC指令在AL中生成二進制和數,DAA(加法后的十進制調整)指令將和數轉換為壓縮十進制格式。比如,下述指令執行壓縮十進制數35加48。二進制和數(7Dh)被調整為83h,即35和48的壓縮進制和數。
mov al, 35h
add al, 48h ;AL=7Dh
daa ;AL=83h(調整后的結果)
DAA的內部邏輯請參閱Intel指令集參考手冊。示例 下面的程序執行兩個16位壓縮十進制整數加法,并將和數保存在一個壓縮雙字中。加法要求和數變量的存儲大小比操作數多一個數字:
;AddPacked.asm 7.6.1 DAA指令 壓縮十進制示例
;下面的程序執行兩個16位壓縮十進制整數加法,并將和數保存在一個壓縮雙字中。
;加法要求和數變量的存儲大小比操作數多一個數字:INCLUDE Irvine32.inc.data
packed_1 WORD 4536h
packed_2 WORD 7207h
sum DWORD ?.code
main PROC;初始化和數與索引:mov sum, 0mov esi, 0;低字節相加。mov al, BYTE PTR packed_1[esi]add al, BYTE PTR packed_2[esi]daamov BYTE PTR sum[esi], al;高字節相加,包括進位標志位。inc esimov al, BYTE PTR packed_1[esi]adc al, BYTE PTR packed_2[esi]daamov BYTE PTR sum[esi], al;若還有進位,則加上該進位值。inc esimov al, 0adc al, 0mov BYTE PTR sum[esi], al;用十六進制顯示和數,mov eax, sumcall WriteHexcall Crlfexit;INVOKE ExitProcess,0
main ENDP
END main
顯然,這個程序包含重復代碼,因此建議使用循環結構。本章的一道習題將會要求編寫一個過程,實現任意大小的壓縮十進制整數加法。
7.6.2 DAS指令
32位模式下,SUB或SBB指令在AL中生成二進制結果,DAS(減法后的十進制調整)指令將其轉換為壓縮十進制格式。比如,下面的語句計算壓縮十進制數85減48,并調整結果:
mov bl, 48h
mov al, 85h
sub al, bl ;AL = 3Dh
das ;AL = 37h (調整后)
DAS的內部邏輯請參閱Intel指令集參考手冊.
7.6.3 本節回顧
1.舉例說明,什么情況下DAA指令會把進位標志位置1?
答:當壓縮十進制加法的和數大于99時,DAA將進位標志位置1,例如:
;7.6.3_1.asm 7.6.3 本節回顧
;1.舉例說明,什么情況下DAA指令會把進位標志位置1?.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.code
main PROCmov al, 56hadd al, 92h ;AL = E8hdaa ;AL = 48h, CF = 1INVOKE ExitProcess,0
main ENDP
END main
2.舉例說明,什么情況下DAS指令會把進位標志位置1?
答:若從小的壓縮十進制整數中減去大的壓縮十進制整數,則DAS將進位標志位置1.例如:
;7.6.3_2.asm 7.6.3 本節回顧
;2.舉例說明,什么情況下DAS指令會把進位標志位置1?.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD.code
main PROCmov al, 56hsub al, 92h ;AL = C4hdas ;AL = 64h, CF = 1INVOKE ExitProcess,0
main ENDP
END main
3.兩個長度為"字節的壓縮十進制整數相加時,和數應該保留多少字節?
答:和數應該保留n+1個字節。
7.7 本章小結
與前面章節介紹的位元指令一樣,移位指令也是匯編語言最顯著的特點之一。一個數移位就意味著把它的位元進行右移或左移。
SHL(左移)指令把目標操作數的每一位都向左移動,最低位用0填充。SHL最大的作用之一是快速實現與2的冪相乘。任何操作數左移位即為乘以2"。SHR(右移)指令則把每一位都向右移動,最高位用0填充。任何操作數右移位即為除以2"。
SAL(算術左移)和SAR(算術右移)是特別為有符號數移位設計的指令。
ROL(循環左移)指令把每一位向左移動,并將最高位復制到進位標志位和最低位。ROR(循環右移)指令把每一位向右移動,并將最低位復制到進位標志位和最高位。
RCL(帶進位循環左移)指令把每一位都左移,并先將進位標志位復制到移位結果的最低位,再將最高位復制到進位標志位。RCR(帶進位循環右移)指令把每一位都右移,并將最低位復制到進位標志位,而進位標志位則復制到結果的最高位。
x86處理器可使用的SHLD(雙精度左移)和SHRD(雙精度右移)指令對大數的移位非常有用。
32位模式下,MUL指令實現一個8位、16位或32位的操作數與AL、AX或EAX相乘64位模式下,一個數還可以實現與RAX寄存器相乘。IMUL指令執行有符號數乘法,它有三種格式:單操作數、雙操作數和三操作數。
32位模式下,DIV指令實現8位、16位或32位操作數的除法。64位模式下,還可以實現 64位除法。IDIV指令執行有符號數乘法,其格式與DIV指令相同。
CBW(字節轉字)指令把AL的符號位擴展到AH寄存器。CDO(雙字轉四字)指令把EAX的符號位擴展到EDX寄存器。CWD(字轉雙字)指令把AX的符號位擴展到DX寄存器。
擴展加減法是指加減任意大小的數,ADC和SBB指令可以用于實現這種加減運算ADC(帶進位加法)指令實現源操作數與進位標志位的內容和目的操作數相加。SBB(帶借位減法)指令實現目的操作數減去源操作數和進位標志位的值。
ASCII十進制數每個字節存放一個數字,并編碼為ASCI形式。AAA(加法后的ASCII調整)指令將ADD或ADC指令的二進制結果轉換為ASCII十進制。AAS(減法后的ASCII調整)指令將SUB或SBB指令的二進制結果轉換為ASCII十進制。所有這些指令都只能用于32位模式。
非壓縮十進制數每個字節存放一個十進制數字,表現為二進制數值。AAM(乘法后的ASCII 調整)指令轉換的是MUL指令執行非壓縮十進制數乘法所生成的二進制結果。AAD(除法前的 ASCI 調整)指令在執行 DIV指令之前,將非壓縮十進制被除數轉換為二進制。所有這些指令都只能用于32位模式。
壓縮十進制數每個字節存放兩個十進制數字。DAA(加法后的十進制調整)指令轉換的是 ADD或 ADC指令執行壓縮十進制加法所生成的二進制結果。DAS(減法后的十進制調整)指令轉換的是SUB或SBB指令執行壓縮十進制減法所生成的二進制結果。所有這些指令都只能用于 32 位模式,