本章向程序員的匯編語言工具箱中引入一個重要的內容,使得編寫出來的程序具備作決策的功能。幾乎所有的程序都需要這種能力。首先,介紹布爾操作,由于能影響CPU狀態標志,它們是所有條件指令的核心。然后,說明怎樣使用演繹CPU狀態標志的條件跳轉和循環指令。接著演示如何用本章介紹的工具來實現理論計算機科學中最根本的結構之一:有限狀態機。本章最后展示的是MASM內置的32位編程的邏輯結構。
6.4 條件循環指令
6.4.1 LOOPZ和 LOOPE 指令
LOOPZ(為零跳轉)指令的工作和LOOP指令相同,只是有一個附加條件:為零控制轉向目的標號,零標志位必須置1。指令語法如下:
LO0PZ destination
LOOPE(相等跳轉)指令相當于LOOPZ,它們有相同的操作碼。這兩條指令執行如下任務:
ECX=ECX-1
if ECX > 0 and F =1, jump to destination
否則,不發生跳轉,并將控制傳遞到下一條指令。LOOPZ和LOOPE不影響任何狀態標志位。32位模式下,ECX是循環計數器;64位模式下,RCX是循環計數器。
完整代碼測試筆記
;6.4.1.asm LOOPNZ指令 和LOOPNE指令用法學習
;找出數組中第1個小寫字母.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
array WORD 'A', 'C', 'S', 'P', 'a', 'y', 'h', 'x'
value WORD 0.code
main PROCmov ecx, LENGTHOF arraymov esi, 0
L1:test array[esi], 00100000b ;無符號數比較,影響零標志和進位標志pushfdadd esi, TYPE arraypopfdloopz L1sub esi, TYPE arraymov ax, WORD PTR array[esi]mov value, axINVOKE ExitProcess,0
main ENDP
END main
運行調試
6.4.2 LOOPNZ和 LOOPNE 指令
LOOPNZ(非零跳轉)指令與LOOPZ相對應。當ECX中無符號數值大于零(減1操作之后)且零標志位等于零時,繼續循環。指令語法如下:
LOOPNZ destination
LOOPNE(不等跳轉)指令相當于LOOPNZ,它們有相同的操作碼。這兩條指令執行如下任務:
ECX=ECX-1
if ECX > 0 and F =0, jump to destination
否則,不發生跳轉,并將控制傳遞到下一條指令。
完整代碼測試筆記:
;6.4.2.asm LOOPNZ指令 和LOOPNE指令學習
;與Loopnz.asm功能一樣.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
array SWORD -1, -6, -1, -10, 10, 30, 40, 4
sentinel SWORD 0.code
main PROCmov esi, OFFSET arraymov ecx, LENGTHOF array
L1: test WORD PTR [esi], 8000h ;測試符號位,該指令影響符號標志,零標志,奇偶標志jns quit ;無符號跳轉(表示非負數)add esi,TYPE array ;移動到下一個位置loopnz L1 ;繼續循環jnz notFind ;沒有發現非負數
quit:movzx eax, WORD PTR[esi]mov sentinel, ax
notFind:nopINVOKE ExitProcess,0main ENDP
END main
運行調試:
如果找到一個非負數,ESI會指向該數值。如果沒有找到一個正數,則只有當ECX=0時才終止循環。在這種情況下,JNZ指令跳轉到標號quit,同時ESI指向標記值(0),其在內存中的位置正好緊接著該數組。
6.4.3 本節回顧
1.(真/假):當(且僅當)零標志位被清除時,LOOPE指令跳轉到標號
答:假。當ZF=1并且ECX > 0時才跳轉
2.(真/假):32位模式下,當ECX大于零且零標志位被清除時,LOOPNZ指令跳轉到標號。
答:真。
3.(真/假):LOOPZ指令的目的標號必須處在距離其后指令的-128到+127字節范圍之內。
答:真
4.修改6.4.2節中的LOOPNZ示例,使之掃描數組并搜索其中的第一個負數。改變數組的初始化,用正數作為其起始值。
答:修改后的代碼如下
;6.4.3_4.asm 6.4.3 本節回顧 4.修改6.4.2節中的LOOPNZ示例,
;使之掃描數組并搜索其中的第一個負數。改變數組的初始化,用正數作為其起始值。.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
array SWORD 10, 30, 40, 4, -56, -17, -98
sentinel SWORD 0.code
main PROCmov esi, OFFSET arraymov ecx, LENGTHOF array
L1: test WORD PTR [esi], 8000h ;測試符號位,該指令影響符號標志,零標志,奇偶標志pushfd ;標志位入棧add esi,TYPE array ;移動到下一個位置popfd ;標志位出棧loopz L1 ;繼續循環jz quit ;沒有發現負數sub esi, TYPE array ;ESI指向數值
quit:movzx eax, WORD PTR[esi]mov sentinel, axINVOKE ExitProcess,0main ENDP
END main
運行調試:
5.挑戰:6.4.2節的LOOPNZ示例依靠一個標記值來處理沒有發現正數的可能性。如果把這個標記值去掉,會發生什么?
答:如果沒有發現匹配值,ESI將以指向數組末層之外作為結束。若指向了一個未定義的內存位置,那么程序運行就可能導致運行時錯誤。
6.5 條件結構
條件結構被定義為,能夠在不同的邏輯分支中觸發選擇的一個或多個條件表達式。每一個分支都執行不同的指令序列。毫無疑問,在高級編程語言中已經使用了條件結構,但是你可能并不了解語言編譯器是如何將條件結構轉換為低級機器代碼的。現在就來討論這個轉換過程.
6.5.1 塊結構的 IF 語句
IF結構包含一個布爾表達式,其后有兩個語句列表:一個是當表達式為真時執行,另一個是當表達式為假時執行:
if(boolean - expression)statement - list - 1
elsestatement - list - 2
結構中的else部分是可選的。在匯編語言中,則是用多個步驟來實現這種結構的。首先,對布爾表達式求值,這樣一來某個CPU狀態標志位會受到影響。然后,根據相關CPU狀態標志位的值,構建一系列跳轉把控制傳遞給兩個語句列表。
示例1下面的C++代碼中,如果op1等于op2,則執行兩條賦值語句:
if(op1 == op2)
{X = 1;Y = 2;
}
在匯編語言中,這種正語句轉換為條件跳轉和CMP指令。由于op1和op2都是內存操作數(變量),因此,在執行CMP之前,要將其中的一個操作數送人寄存器。下面實現IF語句的程序是高效的,當邏輯表達式為真時,它允許代碼“通過”直達兩條期望被執行的MOV 指令:
;6.5.1_1.asm 塊結構的 IF 語句 示例1 用匯編語言實現下面c++語句
;if(op1 == op2) {
; X = 1;
; Y = 2;
;}.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
op1 DWORD 45
op2 DWORD 45
X DWORD ?
Y DWORD ?.code
main PROC;1.jne的方式。它允許代碼“通過”直達兩條期望被執行的MOV 指令:
; mov eax, op1
; cmp eax, op2 ;op1 == op2 ?
; jne L1 ;否:跳過后續指令
; mov X, 1 ;是:X,Y賦值
; mov Y, 2
; jmp quit
;L1: mov X, 10
; mov Y, 20;2.je的方式。用正來實現==運算符,生成的代碼就沒有那么緊湊了(6條指令,而非5條指令):mov eax, op1cmp eax, op2 ; op1 == op2 ?je L1 ;是:跳轉到L1jmp L2 ;否:跳過賦值語句
L1: mov X, 1 ;X,Y賦值mov Y, 2 jmp quit
L2: mov X, 10 mov Y, 20
quit:nopINVOKE ExitProcess,0main ENDP
END main
方式1調試:
方式2調試:
從上面的例子可以看出,相同的條件結構在匯編語言中有多種實現方法。本章給出的編譯代碼示例只代表一種假想的編譯器可能產生的結果
示例2? NTFS文件存儲系統中,磁盤簇的大小取決于磁盤卷的總容量。如下面的偽代碼所示,如果卷大小(用變量terrabytes存放)不超過16TB,則簇大小設置為4096。否則簇大小設置為8192
clustersize=8192
if terrabytes <16clusterSize = 4096:
用匯編語言實現該偽代碼:
;6.5.1_2.asm 塊結構的 IF 語句 示例2 用匯編語言實現下面語句
;clustersize = 8192
;if terrabytes < 16
; clusterSize = 4096:.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
clusterSize WORD ? ;簇大小
terrabytes WORD 1 ;磁盤卷大小,單位TB.code
main PROCmov clusterSize, 8192 ;假設較大的磁盤簇cmp terrabytes, 16 ;小于16TB ?jae next ;大于或等于跳轉 mov clusterSize, 4096 ;切換到較小的磁盤簇
next: nopINVOKE ExitProcess,0main ENDP
END main
運行調試:
示例3 下面的偽代碼有兩個分支:
if op1 > op2call Routine1
elsecall Routine2
end if
用匯編語言翻譯這段偽代碼,設op1和op2是有符號雙字變量。對這兩個變量比較時其中一個必須送入寄存器:
;6.5.1_3.asm 塊結構的 IF 語句 示例3 用匯編語言實現下面語句
;if op1 > op2
; call Routine1
;else
; call Routine2
;end if.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
op1 DWORD 55
op2 DWORD 51.code
main PROCmov eax, op1 ;op1送入寄存器cmp eax, op2 ;op1 >op2?jg A1 ;是:調用 Routine1call Routine2 ;否:調用 Routine2jmp A2 ;退出IE語句
A1: call Routine1
A2: nopINVOKE ExitProcess,0
main ENDP Routine1 PROCmov edx, op1ret
Routine1 ENDPRoutine2 PROCmov edx, op2ret
Routine2 ENDP
END main
運行調試:
白盒測試
復雜條件語句可能有多個執行路徑,這使得它們難以進行調試檢查(查看代碼)。程序員經常使用的技術稱為白盒測試,用來驗證子程序的輸入和相應的輸出。白盒測試需要源代碼,并對輸人變量進行不同的賦值。對每個輸入組合,要手動跟蹤源代碼,驗證其執行路徑和子程序產生的輸出。下面,通過嵌套正語句的匯編程序來看看這個測試過程:
if op1 == op2if X > Ycall Routine1elsecall Routine2
elsecall Routine3
end if
下面是可能的匯編語言翻譯,加上了參考行號。程序改變了初始條件(op1=-op2),并立即跳轉到 ELSE部分。剩下要翻譯的內容是內層IF-ELSE 語句:
mov eax, op1 cmp eax, op2 ;op1 == op2 ?jne L2 ;否:調用Routine3, 不等于跳轉到L2mov eax, X ;處理內層IF-ELSE 語句cmp eax, Y ;X > Y ?jg L1 ;是:調用 Routine1, 大于跳轉到L1 call Routine2 ;否:調用 Routine2jmp L3 ;退出
L1: call Routine1 ;調用 Routine1jmp L3 ;退出
L2: call Routine3
L3:
表6-6給出了示例代碼的白盒測試結果。前四列對op1、op2、X和Y進行測試賦值。
第5列和第6列對生成的執行路徑進行了驗證
6.5.2 復合表達式
匯編語言很容易實現包含AND運算符的復合布爾表達式。考慮下面的偽代碼,假設其中進行比較的是無符號整數:
if (al > bl) AND (bl > cl)X = 1
else if
短路求值 下面的例子是短路求值的簡單實現,如果第一個表達式為假,則不需計算第二個表達式。高級語言的規范如下:
;6.5.2_1.asm 復合表達式 1.邏輯 AND 運算符
;if (al > bl) AND (bl > cl)
; X = 1;
;end if.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
X BYTE 23.code
main PROCmov al, 5mov bl, 4mov cl, 3;方式1cmp al, bl ;第一個表達式…ja L1 ;大于跳轉到L1jmp next ;否則退出if
L1: cmp bl, cl ;第二個表達式…ja L2 ;大于跳轉到L2jmp next ;否則退出if
L2: mov X, 1 ;全為真:將X置1
next:jmp quit;方式2, 如果把第一條 JA指令替換為JBE,就可以把代碼減少到5條:cmp al, bl ;第一個表達式…jbe quit ;如果假,則退出 小于或等于跳轉到quit,退出if, jbe無符號比較cmp bl, cl 第二個表達式…jbe quit ;如果假,則退出 小于或等于跳轉到quit,退出if, jle有符號比較mov X, 1 ;全為真
quit: nopINVOKE ExitProcess,0main ENDP
END main
運行調試
方式1:
方式2:
若第一個JBE不執行,CPU可以直接執行第二個CMP指令,這樣就能夠減少29%的代碼量(指令數從7條減少到5條)。
2.邏輯 OR 運算符
當復合表達式包含的子表達式是用OR運算符連接的,那么只要一個子表達式為真,則整個復合表達式就為真。以如下偽代碼為例:
if (al > bl) OR (bl > cl)X = 1
在下面的實現過程中,如果第一個表達式為真,則代碼分支到L1;否則代碼直接執行第二個CMP指令。第二個表達式翻轉了>運算符,并使用了JBE指令:
【代碼】
對于一個給定的復合表達式而言,匯編語句有多種實現方法。
2.邏輯 OR 運算符
當復合表達式包含的子表達式是用OR運算符連接的,那么只要一個子表達式為真,則整個復合表達式就為真。以如下偽代碼為例:
if (al > bl) OR (bl > cl)X = 1
在下面的實現過程中,如果第一個表達式為真,則代碼分支到L1;否則代碼直接執行第二個CMP指令。第二個表達式翻轉了>運算符,并使用了JBE指令:
;6.5.2_2.asm 復合表達式 2.邏輯 OR 運算符
;if (al > bl) OR (bl > cl)
; X = 1;.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
X BYTE 23.code
main PROCmov al, 2mov bl, 4mov cl, 3cmp al, bl ;1:比較AL和 BLja L1 ;如果真,跳過第二個表達式cmp bl, cl ;2:比較BL和CLjbe next ;假:跳過下一條語句
L1: mov X, 1 ;真:將X置1
next: nopINVOKE ExitProcess,0main ENDP
END main
運行調試:
對于一個給定的復合表達式而言,匯編語句有多種實現方法。