Intel汇编语言程序设计学习-第五章 过程-下
5.3.3 ?庫測試程序
測試程序#1:整數I/O
? ? 該測試程序把輸出文本的顏色改為藍底黃字,然后以十六進制數顯示七個數組的內容,最后提示用戶輸入一個有符號整數,再分別以十進制、十六進制和二進制格式重復顯示該整數:
TITLE Library Test #1: Integer I/O (TestLib1.asm)
;Test the Clrscr,Crlf,DumpMem,ReadInt,
;SetTextColor,WaiMsg,WriteBin,WriteHex,
;and WriteString procedures.
INCLUDE Irvine32.inc
.data
arrayD DWORD 1000h,2000h,3000h
prompt1 BYTE "Enter a 32-bit stgned integer:",0
dwordVal DWORD ?
.code
start: call main
main PROC
mov eax ,yellow + (blue * 16)
call SetTextColor
call Clrscr ????????????????????
?
mov esi ,OFFSET arrayD
mov ecx ,LENGTHOF arrayD
mov ebx ,TYPE arrayD
call DumpMem
call Crlf
?
mov edx ,OFFSET prompt1
call WriteString
call ReadInt
mov dwordVal ,eax
?
call Crlf
call WriteInt
call Crlf
call WriteHex
call Crlf
call WriteBin
call Crlf
call WaitMsg
?
mov eax ,lightGray + (black * 16)
call SetTextColor
call Clrscr
exit
main ENDP
end start
運行結果:
?
測試程序#2:隨機整數
第2個庫測試程演示隨機數使用過程。首先,隨機產生10個在0~4294967294內的無符號整數,接著隨機再生成10個在范圍-50~+49內的有符號整數:
TITLE Link Library Test #2 (TestLib2.asm)
INCLUDE Irvine32.inc
TAB = 9
.code
main PROC
????call ?Randomize
call ?Rand1
call ?Rand2
exit
main ENDP
?
Rand1 PROC
????mov ?ecx ,10
L1: call ?Random32
????call ?WriteDec
mov ??al ,TAB
call ?WriteChar
loop L1
call Crlf
ret
Rand1 ENDP
?
Rand2 PROC
????mov ?ecx ,10
L1 :mov ?eax ,100
????call RandomRange
sub ?eax ,50
call WriteInt
mov ?al ,TAB
call WriteChar
loop L1
call Crlf
ret
Rand2 ENDP
END main
運行結果:
測試程序#3:性能度量
? ? 匯編語言常用于優化對程序性能而言至關重要的代碼。GetMseconds過程返回自午夜以來逝去的毫秒數,在循環之前調用了GetMseconds過程,然后執行嵌套循環約170億次,在循環結束后再次調用GetMsgconds過程并報告用掉的時間:
TITLE Link Library Test #3
INCLUDE Irvine32.inc
OUTER_LOOP_COUNT = 3
.data
startTime DWORD ?
msg1 BYTE "Please wait..." ,0dh ,0ah ,0;
msg2 BYTE "Elapsed milliseconds:" ,0
.code
main PROC
????mov ?edx ,OFFSET msg1
call WriteString
call GetMSeconds
mov ?startTime ,eax
mov ?ecx ,OUTER_LOOP_COUNT
L1: call innerLoop
????loop L1
call GetMSeconds
sub ?eax ,startTime
mov ?edx ,OFFSET msg2
call WriteString
call WriteDec
call Crlf
exit
main ENDP
innerLoop PROC
????push ecx
mov ?ecx ,0FFFFFFFFh
L1: mov ?eax ,eax
????loop L1
pop ecx
ret
innerLoop ENDP
END main
執行結果:
5.4 ?堆棧操作
堆棧的定義不解釋了,后進先出。
5.4.1 ?運行時棧
運行時棧是由CPU直接管理的內存數組,它使用兩個寄存器:SS和ESP。在保護模式下,SS寄存器存放的是段選擇子,用戶模式程序不應對其進行修改。ESP寄存器存放的是指向堆棧內特定位置的一個32位偏移值。我們很少需要直接操縱ESP的值,相反,ESP寄存器的值通常是由CALL,RET,PUSH和POP等指令間接修改的。
堆棧指令寄存器(ESP)指向最后壓入(或添加)堆棧上的數據。
? ? 這里討論的運行時棧同程序設計課程中講述的堆棧抽象數據類型(stack ADT)是不同的。運行時棧在系統層上(由硬件直接實現)處理子過程調用;堆棧抽象數據類型通常用于實現依賴于先進后出操作的算法,一般使用高級語言如C++、Java等編寫。
壓棧操作
32位的壓棧(PUSH)操作首先將堆棧指針減4,然后把要壓棧的值賦值到堆棧指針所指向的位置處。
?
出棧操作
????出棧與壓棧相反
? ??堆棧中ESP之下的區域從邏輯上講是空白的,在程序下次執行任何要壓棧的指令時該區域將被覆蓋重寫。
堆棧的應用
寄存器在做多種用途的時候,堆棧可以方便的作為臨時保存區域,在寄存器使用完畢之后,可通過堆棧恢復其原始值。
CALL指令執行的時候,CPU用堆棧保存當前被調用過程的返回地址。
調用過程的時候,可以通過壓棧傳遞輸入值(成為參數)。
過程內的局部變量在堆棧上創建,過程結束時,這些變量被丟棄。
5.4.2 ?PUSH和POP指令
PUSH指令
PUSH指令首先減少ESP的值,然后再把一個16位或32位的源操作數復制到堆棧上。對于16位的操作數,ESP值將減2;對于32位操作數,ESP值將減4.PUSH指令有一下三種格式:
PUSH ?r/m16
PUSH ?r/m32
PUSH ?imm32
如果程序調用Irvine32中的過程,應總是壓入32位值,否則庫中使用的Win32控制數將不能正常運行。如果程序調用Irvine16中的庫過程(實地址模式下)則可壓入16位或32位的值。
在保護模式下立即數總是32位。在實地址模式下,如果未使用.386(或更高的)處理器偽指令,立即數默認是16位的。
POP指令
POP指令首先將ESP所指的堆棧元素復制到16位或32位的目的操作數中,然后增加ESP的值。如果操作數是16位的,ESP值將加2;如果操作數是32位的,ESP值將加4.其格式如下:
POP ?r/m16
POP ?r/m32
PUSHFD和POPFD指令
PUSHFD指令在堆棧上壓入32位的EFLAGS寄存器的值,POPFD指令將堆棧頂部彈出并送至EFLAGS寄存器:
pushfd
popfd
實地址則是16位的。
MOV指令不能復制標志寄存器的值至變量或寄存器中,因此使用PUSHFD指令可能就是保存寄存器的最佳方式了。有時保存標志的備份以便后面進行恢復是很有用的,這通常可以是用PUSHFD和POPFD指令把一塊指令包圍起來:
pushfd ?????????????;保存標志
;
;......
;
popfd ??????????????;恢復標志
在使用這種類型的標志壓棧和標志出棧指令的時候,必須確保程序的執行路徑不會跳過POPFD指令。隨著時間的推移,在修改程序時很難激情所有的壓棧指令放在哪里。因此,編寫準確的文檔是非常關鍵的!
可以完成同樣功能但或許可減少犯錯誤的方法是將標志保存在變量中:
.data
saveFlags DWORD ?
.code
pushfd ????????????????;標志入棧
pop saveFlags ??????????;復制到變量
下列語句從同一變量中回復標志值:
push saveFlags ??????????;將保存的標志入棧
popfd ?????????????????;回復標志
PUSHAD,PUSHA,POPAD和POPA指令
PUSHAD指令在堆棧上按下列循序壓入所有的32為通用寄存器:
EAX,ECX,EDX,EBX,RSP(執行PUSHAD指令之前的值),EBP,ESI和EDI;POPAD指令以相反的循序從堆棧中彈出這些通用寄存器。于此類似,80286處理器引入的PUSHA指令以括號中列表的循序壓入所有的16位寄存器(AX,CX,DX,BX,SP,BP,SI和DI)。POPA指令則以相反順序彈出這些寄存器。
如果在過程中修改了很多32位寄存器,那么可以在過程的開始和結束的位置分別用PUSHAD和POPAD指令保存和恢復寄存器的值。
?
MySub PROC
????pushad
????.
????.
????mov ?eax ,...
????mov ?edx ,...
????mov ?ecx ,...
????.
????.
????popad
????ret
MySub ENDP
對上面的例子,有一個例外情況必須指出:過程通過一個或多個寄存器返回結果時不應該使用PUSHA或PUSHAD指令。假設下面的RcadValue過程想要通過EAX返回一個整數但對POPAD的調用將覆蓋EAX中的返回值:
ReadValue PROC
????pushad ?????????????;保存通用寄存器
????.
????.
????mov ?eax ,return_value
????.
????.
????popad
????ret
?ReadValue ENDP
例子:反轉字符串
RevStr.asm程序循環遍歷字符串并把每個字符串都壓入堆棧,然后取出來。
TITLE Reversing a String ?(RevStr.asm)
INCLUDE ?Irvine32.inc
.data
aName BYTE "Abraham Lincoln",0
nameSize = ($ - aName) - 1
.code
main PROC
?????mov ?ecx ,nameSize
mov ?esi ,0
mov ?eax ,0
L1: mov ?al,aName[esi]
????push eax
inc ?esi
loop L1
????
mov ?ecx ,nameSize
mov ?esi ,0
L2: pop ?eax
????mov ?aName[esi] ,al
inc ?esi
loop L2
mov ?edx ,OFFSET aName
call WriteString
call Crlf
exit
main ENDP
END main
運行結果:
?
? ? TIP:書中上面的代碼有第一個地方有問題(L1: mov eax,aName[esi]),如果這么寫編譯器會彈出編譯錯誤,原因是aName[esi]是8位,eax是32位的,但是eax是32位,ax是第16位,al是第八位,所以直接eax換成al就行了,但是記住之前要把eax清零,因為堆棧是接收32位的,我們處理al之后把eax壓入堆棧可能把前面的高位壓進去(但是上面這個程序壓進去結果也看不出來,因為我們始終只操作al),但是壓入eax進棧本身就是錯誤的。所以需要eax清零。在使用低位。
5.5 ?過程的定義和使用
可以理解成是C++或是其它語言里的函數或者方法等。
5.5.1 ?PROC偽指令
過程的定義
可以把過程非正式地定義為以返回語句結束的命令語句塊。過程使用PROC偽指令和ENDP偽指令來聲明,另外還必須給過程定義一個名字。到現在寫的所有程序都包含一個名為main的過程,例如:
main PROC
.
.
.
main ENDP
程序啟動過程之外的其他過程以RET指令結束,以強制CPU返回到過程被調用的地方:
sample ?PROC
??.
??.
??ret
sample ?ENDP
啟動過程(main)是個特例,它以exit語句結束。如果程序中使用了INCLUD Irvine32.inc語句的話,exit語句實際上就是對ExitProcess函數的調用,ExitProcess是用來終止程序的系統函數
INVOKE ExitProcess ,0
????如果在程序中使用了INCLUDE Irvine16.inc語句,那么exit被翻譯成.EXIT偽指令。匯編器為.EXIT生成下面兩條語句:
????mov ?ah,4C00h ??;調用MS-DOS的4c00h功能
int 21h ?????????;終止程序
例子:三個整數之和
我們創建一個名為SumOf的過程來計算3個32位整數之和,假設合適的整數在過程被調用以前已經存放在EAX、EBX和ECX寄存器中了,過程在EAX中發回和:
SumOf PROC
????add ?eax ,ebx
????add ?eax ,ecx
????ret
SumOf ENDP
為過程添加文檔
應該養成的良好編程習慣之一就是為程序添加清晰易讀的文檔。下面是對放在每個過程開始處的文檔信息的幾點建議:
過程完成的所有任務的描述。
輸入參數的清單使用方法。
過程返回值的描述。
列出特殊要求。
;------------------------------------------------------------
SumOf PROC
;
;Calculates and returns the sum of three 32-bit integers.
;Receines:EAX,EBX,ECX,the three integers,May be signed or unsigned.
;Retuens: EAX = sum
;--------------------------------------------------------------
????add ?eax ,ebx
????add ?eax ,ecx
????ret
SumOf ENDP
????用C/C++之類的高級語言編寫的函數,典型情況下在AL中返回8位值,在AX中返回16位置,在EAX中返回32位值。
5.5.2 ?CALL和RET指令
CALL指令只是處理器在新的內存地址執行指令,以實現過程的調用。過程使用RET(從過程返回)指令使處理器返回到程序過程被調用的地方繼續執行。從底層細節角度來講,CALL指令把返回地址壓入堆棧并把被調用過程的地址復制到指令寄存器中。當程序返回時,RET指令從堆棧中彈出返回地址并送到指令寄存器中。在32位模式下,CPU總是執行EIP(指令指針寄存器)所指向的內存出的指令;在16位模式下,CPU總是執行IP寄存器指向的指令。
調用和返回的例子
假設在main中,CALL語句位于偏移00000020處。通常CALL指令的機器碼需要5字節,因此下一條指令位于偏移00000025處:
??????main PROC
00000020 ?????call ?MySub
00000025 ?????mov eax ,ebx
接下來,假設MySub中的第一條指令位于偏移00000040處:
??????????MySub PROC
00000040 ???mov ?eax ,edx
????????????.
????????????.
????????????ret
???????????MySub ENDP
當CALL指令執行的時候,金針CALL指令的地址(00000025)被壓入堆棧,而MySub的地址被裝入EIP。MySub內的指令開始執行,一直到RET指令位置。當RET指令被執行的時候,ESP所指的堆棧值被彈出并送至EIP。第二部,ESP的值將減少以指向堆棧上的前一個值。
?
Sub3過程結束的時候執行RET指令,從堆棧中彈出[ESP]處的值送至指令寄存器,這將使得CPU從緊跟調用Sub3之后的指令處恢復執行,下圖顯示了在從Sub3過程返回之前的堆棧狀況:
?
返回之后,ESP指向相鄰的堆棧表項,在Sub2末尾RET指令準備執行時,堆棧如下表示:
? ? TIP:看到這我就一直在想一個問題,如果我自己寫了一個函數,然后我在里面直接更改了堆棧,但是我并沒有還原相關,也就是我更改堆棧會不會導致這個函數return不回去(因為我不確定我用的堆棧和CPU調度用的堆棧是不是同一個堆棧,也就是作用域的問題),于是我做了這個嘗試:
TITLE TEST STACK (teststack.asm)
INCLUDE Irvine32.inc
.data
strStart BYTE "Start!" ,0dh ,0ah,0
strEnd ??BYTE "End!" ,0dh ,0ah ,0
strTest ?BYTE "RunTestFun!",0dh ,0ah ,0
.code
main PROC
????mov ?edx ,OFFSET strStart
????call WriteString
????call TestFun
????mov ?edx ,OFFSET strEnd
????call WriteString
????exit
main ENDP
?
TESTFun PROC
????mov edx ,OFFSET strTest
call WriteString
pop edx
ret
TESTFun ENDP
END main
????我在函數里直接POP了棧里的東西,如果用的是同一個棧,那么這樣應該是跳轉不回去的。結果也應該是不可預知的。然后操作的結果卻是是這樣。直接沒有return成功,我還用vs反匯編看了下地址,在函數里面POP出來的那個值就是call TestFun接下來那個call writestring的地址。so...
過程的嵌套調用
被調用的過程在返回之前又調用了其他過程時,就發生了過程嵌套調用。假設main調用了過程Sub1,Sub1執行的時候又調用了過程Sub2,Sub2執行的時候又調用了Sub3,這個過程如下圖:
最后,當Sub1返回時,堆棧中的[ESP]被彈出送指令指針寄存器,CPU在main中回復繼續執行:
顯然,堆棧已經被證明是存儲信息(如嵌套過程調用的相關信息)的有效工具。通常堆棧適用于程序要以特定順序回溯執行某些步驟的情況。
向過程傳遞參數
如果想要編寫一個執行某些標準操作的過程,如計算整理數組之和的過程,那么在過程之內引用特定的變量并不是什么好主意。如果那么做的話,該過程就不可能用于其他數組了。一個較好的辦法就是向過程傳遞參數。在匯編語言中,通過通用寄存器傳遞參數的做法是很普遍的。
上節中我們編寫了一個把EAX,EBX和ECX寄存器中整數相加的過程SumOf。在main中調用SumOf之前,首先為EAX,EBX和ECX寄存器賦值:
data
theSum ?DWORD ?
.code
main PROC
??mov eax ,10000h
??mov ebx ,20000h
??mov ecx ,30000h
??call SumOf
??mov theSum ,eax
在CALL語句之后,可以把EAX中的和復制到一個變量中保存。
5.5.3 ?例子:對整數數組求和
一種非常常見的類型的循環是計算整數數組之和,或許讀者用C++或Java編寫過,在匯編語言中是非常易于實現的,經過精心編寫,循環可以以盡可能快的速度運行。比如我們可以在循環中使用寄存器而不是變量。
下面創建一個名為AraySum的過程,它從調用程序那里接受連個參數:一個指向32位整數數組的指針和一個包含數組元素數目的技術,ArraySum計算數組之和并通過EAX寄存器返回:
;----------------------------------
ArraySum PROC
;
;calculates the sum of array of 32-bit integers.
;Receives: ESI = the array offset
; ?????????ECX = number of elements in the array
;Returns : EAX = sum of the array elements
;-----------------------------------
?push ?esi
push ?ecx
mov ??eax ,0
L1:
????add ??eax ,[esi]
add ??esi ,TYPE DWORD
loop ?L1
pop ?ecx
pop ?esi
ret
ArraySum ?ENDP
注意該過程中沒有任何東西與特定數組的名字或大小相關,所以它可用于任何需要計算32位整數數組和的程序。無論何時只要有可能的話,讀者應盡量編寫靈活和易于修改的過程。
?調用ArraySum:下面是一個調用ArraySum的過程的例子,通過ESI傳遞array的地址,并通過ECX傳遞數組元素數目。在調用之后,把EAX中的和復制到一個變量中。
INCLUDE Irvine32.inc
.data
????array ?DWORD 10000h ,20000h ,30000h ,40000h ,50000h
????theSum DWORD ?
.code
main PROC
????mov esi ,OFFSET array
????mov ecx ,LENGTHOF array
????call ArraySum
????mov theSum ,eax
main ENDP
5.5.4 ?流程圖
流程圖是以圖形化的方式描述程序邏輯的有效方法。流程圖中的每個圖形都表示一個邏輯步驟,把圖形連接起來的帶箭頭的先顯示了邏輯步驟之間的次序:
來一個ArraySum過程設計一個簡單的流程圖。
5.5.5 ?保存和恢復寄存器
讀者已經可能注意到在ArraySum過程的開始處ECX和ESI被壓入堆棧,過程結束的時候又被彈出,絕大多數修改寄存器值的過程都使用這種方式。修改寄存器值的過程應該總是保存和恢復寄存器值,以確保調用程序本身的寄存器值不會覆蓋改寫。這個規則的一種例外情況是用寄存器發回結果時,這時不要對這個寄存器進行保存和恢復工作。
USER操作符
與PROC偽指令配套使用的USER操作符允許列出被過程修改的所有寄存器,它只是編譯器做兩件事:首先,在過程開始處生成PUSH指令在堆棧上保存寄存器;其次,在過程結束的處生成POP指令恢復這些寄存器的值。USER操作符應該緊跟PROC偽指令,其后跟由空格和制表符分割的寄存器列表。
5.5.3節中的ArraySum過程使用PUSH和POP指令保存和恢復被過程修改的寄存器ESI和ECX。使用USER操作符做相同的事情更簡單一些:
ArraySum PROC USER esi ,ecx
???mov ?eax ,0
L1:
???add ?eax ,[esi]
???????add ?esi ,4
???????loop L1
???????ret
ArraySum ENDP
匯編生成的相應代碼顯示了使用USER操作符的效果:
?
Array PROC
push ?esi
push ?ecx
mov ??eax ,0
L1:
add ?eax ,[esi]
add ?esi ,4
loop ?L1
pop ?ecx
pop ?esi
ret
Array ENDP
5.6.1 ?整數求和程序(設計)
寫一個程序,提示用戶輸入3個32位整數,將其保存在數組中,計算數組內的元素的和并在屏幕上顯示。
TITLE Integer Summation Program ?(Sum2.asm)
; This program prompts the user for three integers,
; stores them in an array, calculates the sum of the
; array ,and displays the sum.
INCLUDE Irvine32.inc
INTEGER_COUNT = 3
.data
str1 ?BYTE ?"Enter a signed integer:" ,0
str2 ?BYTE ?"The sum of the integers is:" ,0
array DWORD INTEGER_COUNT DUP(?)
.code
main PROC
???call Clrscr
mov ?esi ,OFFSET array
mov ?ecx ,INTEGER_COUNT
call PromptForIntegers
call ArraySum
call DisplaySum
exit
main ENDP
?
;-----------------------------------------
PromptForIntegers PROC USES ecx edx esi
;
; Prompts the user for an arbitrary number of integers
; and inserts the integers into an array.
; Receives: ESI points to the array ,ECX = array size
; Return: nothing
;-----------------------------------------
???mov ?edx ,OFFSET str1
L1: call WriteString
???call ReadInt
call Crlf
mov ?[esi] ,eax
add ?esi ,TYPE DWORD
loop L1
ret
PromptForIntegers ENDP
?
;----------------------------------------
ArraySum PROC USES esi ecx
;
; Calculates the sum of an array of 32-bit integers.
; Receives : ESI points to the array, ECX = number
; ?of array elements
; Returns: EAX = sum of the array elements
;------------------------------------------
???mov ?eax ,0
L1: add ?eax ,[esi]
???add ?esi ,TYPE DWORD
loop L1
ret
ArraySum ENDP
?
;------------------------------------------
DisplaySum PROC USES edx
;
; Displays the sum on the screen
; Receives :EAX = the sum
; Returns ?nothing
;-------------------------------------------
???mov ?edx ,OFFSET str2
call WriteString
call WriteInt
call Crlf
ret
DisplaySum ENDP
END main
運行結果:
?
?5.7 ?本章小結
?
?
總結
以上是生活随笔為你收集整理的Intel汇编语言程序设计学习-第五章 过程-下的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: windows核心编程-第二章 Unic
- 下一篇: Windows核心编程 第三章 内核对象