Win32匯編--使用MASM使用MASM
Win32匯編源程序的結構
任何種類(lèi)的語(yǔ)言,總是有基本的源程序結構規范。
下面以經(jīng)典的Hello World程序為例,展示一個(gè)C語(yǔ)言、DOS匯編、Win32匯編三種寫(xiě)法。同學(xué)位好好體會(huì )一下。
如果沒(méi)有匯編基礎,建議看一下王爽老師的《匯編語(yǔ)言》這本書(shū)。
C語(yǔ)言中的HelloWorld程序:
#include <stdio.h>
main()
{
printf(“Hello, world\n”);
}
像這樣的一個(gè)程序,就說(shuō)明了C語(yǔ)言中最基本的格式,main()中的括號和下面的花括號說(shuō)明了一個(gè)函數的定義方法,printf語(yǔ)句說(shuō)明了一個(gè)函數的調用方法,調用函數語(yǔ)句后面的分號也是基本的格式。C是一種高級語(yǔ)言,在C源程序中,不必為堆棧段、數據段和代碼段的定義而擔心,編譯器會(huì )把程序中的字符串和語(yǔ)句代碼分別放到它們該去的地方,程序開(kāi)始執行的時(shí)候也會(huì )自己找到main()函數。而匯編是低級語(yǔ)言,必須為所有的東西找到它們該去的地方,所以在DOS的匯編中,Hello World又長(cháng)成了這樣一副模板:
;分號后面是注釋
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 堆棧段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
stack segment
db 100 dup (?) ;定義100個(gè)字節的內存存儲單元空間,默認值為?
stack ends
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 數據段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
data segment
szHello db ‘Hello, world’,0dh,0ah,’$’
;szHello為數據標號,它標記了存儲數據的單元的地址和長(cháng)度。
;天哪,這太像高級語(yǔ)言中的變量了?。?!
data ends
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代碼段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
code segment
assume cs:code,ds:data,ss:stack
start:
mov ax,data
mov ds,ax
mov ah,9
mov dx,offset szHello
int 21h
mov ah,4ch
int 21h
code ends
end start
在這個(gè)源程序中,stack段為堆棧找了個(gè)家,hello world字符串則跑到數據段中去了,代碼則放在代碼段中,程序的開(kāi)始語(yǔ)句必須由最后一句end start來(lái)說(shuō)明應該從start這個(gè)標號開(kāi)始執行,整個(gè)程序在使用過(guò)DOS匯編的程序員眼里是非常的熟悉。(一個(gè)月前我不熟悉,現在我熟悉了。感謝王爽老師。)
到了Win32匯編的時(shí)候,程序的基本結構還是如此,先來(lái)看一看這個(gè)看起來(lái)很新鮮的Win32的Hello world程序。
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定義
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 數據段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
szCaption db ‘A MessageBox!’,0
szText db ‘Hello, World!’,0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代碼段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
start:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
invoke ExitProcess, NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
怎么樣,看來(lái)和上面的C以及DOS匯編又不同了吧!但從include, .data和.code等語(yǔ)句,顧名思義,也能看出一點(diǎn)苗頭來(lái),include應該就是包含別的文件,.data想必是數據段,.code應該就是代碼段了吧!接下來(lái)通過(guò)這個(gè)例子程序逐段介紹Win32匯編程序的結構。
模式定義
程序的第一部分是模式和源程序格式的定義語(yǔ)句:
.386
.model flat,stdcall
option casemap:none
這些指令定義了程序使用的指令集、工作模式和格式。
1)指定使用的指令集
.386語(yǔ)句是匯編語(yǔ)句的偽指令,它在低版本的宏匯編中就已經(jīng)存在,類(lèi)似的指令還有:.8086、.186、.286、.386/.386p、.486/..486p和.586/.586p等,用于告訴編譯器在本程序中使用的指令集。在DOS的匯編中默認使用的是8086指令集,那時(shí)候如果在源程序中寫(xiě)入80386所特有的指令或使用32位的寄存器就會(huì )報錯,為了在DOS環(huán)境下進(jìn)行保護模式編程或僅為了使用32位寄存器,常在DOS的匯編中使用.386來(lái)定義。Win32環(huán)境工作在80386及以上的處理器中,所以這一句.386是必不可少的。
后面帶p的偽指令則表示程序中可以使用特權指令,如:
mov cr0,eax
這一類(lèi)指令必須在特權級0上運行,如果只指定.386,那么使用普通的指令是可以的,編譯時(shí)到這一句就會(huì )報錯,如果我們要寫(xiě)的程序是VxD等驅動(dòng)程序,中間要用到特權指令,那么必須定義.386p,在應用程序級別的Win32編程中,程序都是運行在優(yōu)先級3上,不會(huì )用到特權指令,只需定義.386就夠了。80486和Pentium處理器指令是80386處理器指令的超集,同樣道理,如果程序中要用80486處理器或Pentium處理器的指令,則必須定義.486或.586。
另外,Intel公司的80x86系列處理器從Pentium MMX開(kāi)始增加了MMX指令集,為了使用MMX指令,除了定義.586之外,還要加上一句.mmx偽指令:
.386
.mmx
2)model語(yǔ)句
.model語(yǔ)句在低版本的宏匯編中已經(jīng)存在,用來(lái)定義程序工作的模式,它的使用方法是:
.model 內存模式 [,語(yǔ)言模式] [,其他模式]
內存模式的定義影響最后生成的可執行文件,可執行文件的規模從小到大,可以有很多種類(lèi)型,在DOS的可執行程序中,有只用到64KB的.com文件,也有大大小小的.exe文件。到了Win32環(huán)境下,又有了可以用4GB內存的PE格式可執行文件,編寫(xiě)不同類(lèi)型的可執行文件要用.model語(yǔ)句定義不同的參數,具體如下 表所示。
內存模式
模式
內存使用方式
tiny
用來(lái)建立.com文件,所有的代碼、數據和堆棧都在同一個(gè)64KB段內
small
建立代碼和數據分別用一個(gè)64KB段的.exe文件
medium
代碼段可以有多個(gè)64KB段,數據段只有一個(gè)64KB段
compact
代碼段只有一個(gè)64KB,數據段可以有多個(gè)64KB段
large
代碼段和數據段都可以有多個(gè)64KB段
huge
同large,并且數據段中的一個(gè)數組也可以超過(guò)64KB
float
Win32程序使用的模式,代碼和數據使用同一個(gè)4GB段
Windows 程序運行在保護模式下,系統把每一個(gè)Win32應用程序都放到分開(kāi)的虛擬地址空間中去運行,也就是說(shuō),每一個(gè)應用程序都擁有其相互獨立的4GB地址空間,對Win32程序來(lái)說(shuō),只有一種內存模式,即flat(平坦)模式,意思是內存是很平坦地從0延伸到4GB,再沒(méi)有64KB段大小限制。對比一下DOS的Hello World和Win32的Hello World開(kāi)始部分的不同,DOS程序中有這樣語(yǔ)句:
mov ax,data
mov ds,ax
意思是把數據段寄存器DS指向data數據段,data數據段在前面已經(jīng)用data segment語(yǔ)句定義,只要DS不重新設置,那么從此以后指令中涉及的數據默認將從data數據段中取得,所以下面的語(yǔ)句是從data數據段取出szHello字符串的地址后再顯示:
mov ah,9
mov dx,offset szHello
int 21h
縱觀(guān)Win32匯編的源程序,沒(méi)有一處可以找到ds或es等段寄存器的使用,因為所有的4GB空間用32位的寄存器全部都能訪(fǎng)問(wèn)到了,不必在頭腦中隨時(shí)記著(zhù)當前使用的是哪個(gè)數據段,這就是平坦內存模式帶來(lái)的好處。
如果定義了.model flat,MASM自動(dòng)為各種段寄存器做了如下定義:
ASSUME cs:FLAT,ds:FLAT,ss:FLAT,es:FLAT,fs:ERROR,gs:ERROR
也就是說(shuō),CS,DS,SS和ES段全部使用平坦模式,FS和GS寄存默認不使用,這時(shí)若在源程序中使用FS或GS,在編譯時(shí)會(huì )報錯。如果有必要使用它們,只需在使用前用下面的語(yǔ)句聲明一下就可以了:
assume fs:nothing,gs:nothing 或者 assume fs:flat,gs:flat
在Win32匯編中,.model語(yǔ)句中還應該指定語(yǔ)言模式,即子程序和調用方式,例子中用的是stdcall,它指出了調用子程序或Win32 API時(shí)參數傳遞的次序和堆棧平衡的方法,相對于stdcall,不同的語(yǔ)言類(lèi)型還有C,SysCall,BASIC,FORTRAN和PASCALL,雖然各種高級語(yǔ)言在調用子程序時(shí)都是使用堆棧來(lái)傳遞參數。Windows的API調用使用是的stdcall格式,所以在Win32匯編中沒(méi)有選擇,必須在.model中加上stdcall參數。
3)option語(yǔ)句
option casemap:none
用option語(yǔ)句定義的選項有很多,如option language定義和option segment定義等,在Win32匯編程序中,需要的只是定義option casemap:none,這個(gè)語(yǔ)句定義了程序中的變量和子程序名是否對大小寫(xiě)每感,由于Win32 API中的API名稱(chēng)是區分大小寫(xiě)的,所以必須指定這個(gè)選項,否則在調用API的時(shí)候會(huì )有問(wèn)題。
段的定義
段的概念
把上面的Win32的Hello World源程序中的語(yǔ)句歸納精簡(jiǎn)一下,再列在下面:
.386
.model flat,stdcall
option casemap:none
<一些include語(yǔ)句>
.data
<一些字符串、變量定義>
.code
<代碼>
<開(kāi)始標號>
<其他語(yǔ)句>
end 開(kāi)始標號
模式定義中的模式、選項等定義并不會(huì )在編譯好的可執行程序中產(chǎn)生什么東西,它們只是說(shuō)明,而真正的數據和代碼是定義在各個(gè)段中的,如上面的.data段和.code段,考慮到不同的數據類(lèi)型,還可以有其他種類(lèi)的數據段,下面是包含全部段的源程序結構:
.386
.model flat,stdcall
option casemap:none
<一些include語(yǔ)句>
.stack [堆棧段的大小]
.data
<一些初始化過(guò)的變量定義>
.data?
<一些沒(méi)有初始化過(guò)的變量定義>
.const
<一些常量定義>
.code
<代碼>
<開(kāi)始標號>
<其他語(yǔ)句>
end 開(kāi)始標號
.stack、.data、.data?、.const和.code是分段偽指令,Win32中實(shí)際上只有代碼和數據之分,.data,.data?和.const是數據段,.code是代碼段,和DOS匯編不同,Win32匯編不必考慮堆棧,系統會(huì )為程序分配一個(gè)向下擴展的、足夠大的段作為堆棧段,所以.stack段定義常常被忽略。
注意,前面不是說(shuō)過(guò)Win32環(huán)境下不用段了嗎?是的,這些“段”,實(shí)際上并不是DOS匯編中那種意義的段,而是內存的“分段”。上一個(gè)段的結束就是下一個(gè)段的開(kāi)始,所有的分段,合起來(lái),包括系統使用的地址空間,就組成了整個(gè)可以尋址的4GB空間。Win32匯編的內存管理使用了80386處理器的分頁(yè)機制,每個(gè)頁(yè)(4KB大?。┛梢宰杂芍付▽傩?,所以上一個(gè)4KB可能是代碼,屬性是可執行但不可寫(xiě),下一個(gè)4KB就有可能是既可讀也可寫(xiě)但不可執行的數據,再下面呢?有可能是可讀不可寫(xiě)也不可執行的數據。Win32匯編源程序中“分段”的概念實(shí)際上是把不同類(lèi)型的數據或代碼歸類(lèi),再放到不同屬性的內存頁(yè)(也就是不同的“分段”)中,這中間不涉及使用不同的段選擇器。雖然使用和DOS匯編同樣的.code和.data語(yǔ)句來(lái)定義,意思可是完全不同了!
數據段
.data、.data?和.const定義的是數據段,分別對應不同方式的數據定義,在最后生成的可執行文件中也分別放在不同的節區(Section)中。程序中的數據定義一段可以歸納為3類(lèi):
1)第一類(lèi)是可讀可寫(xiě)的已定義變量。這些數據在源程序中已經(jīng)被定義了初始值,而且在程序的執行中有可能被更改,如一些標志等,這些數據必須定義在.data段中,.data段是已初始化數據段,其中定義的數據是可讀可寫(xiě)的,在程序裝入完成的時(shí)候,這些值就已經(jīng)在內存中了,.data段存放在可執行文件的_DATA節區內。
2)第二類(lèi)是可讀可寫(xiě)的未定義變量。這些變量一般是當做緩沖區或者在程序執行后才開(kāi)始使用的,這些數據可以定義在.data段中,也可以定義在.data?段中,但一般把它放到.data?段中。雖然定義在這兩種段中都可以正常使用,但定義在.data?段中不會(huì )增大.exe文件的大小。舉例說(shuō)明,如果要用到一個(gè)100KB的緩沖區,可以在數據段中定義:
szBuffer db 100 * 1024 dup (?)
如果放在.data段中,編譯器認為這些數據在程序裝入時(shí)就必須有效,所以它在生成可執行文件的時(shí)候保留了所有的100KB的內容,即使它們是全零!如果程序其他部分的大小是50KB,那么最后的.exe文件就會(huì )是150KB大小,如果緩沖區定義為1MB,那么.exe文件會(huì )增大到1050KB。.data?段則不同,其中的內容編譯器會(huì )認為程序在開(kāi)始執行后才會(huì )用到,所以在生成可執行文件的時(shí)候只保留了大小信息,不會(huì )為它浪費磁盤(pán)空間。和上面同樣的情況下,即使緩沖區定義為1MB,可執行文件同樣只有50KB!總之,.data?段是未初始化數據段,其中的數據也是可讀可寫(xiě)的,但在可執行文件中不占空間,.data?段在可執行文件中存放在_BSS節區中。
3)第三類(lèi)數據是一些常量。如一些要顯示的字符串信息,它們在程序裝入的時(shí)候也已經(jīng)有效,但在整個(gè)執行過(guò)程中不需要修改,這些數據可以放在.const段中,.const段是常量段,它是可讀不可寫(xiě)的。一般為了方便起見(jiàn),在小程序中常常把常量一起定義到.data段中,而不另外定義一個(gè).const段。在程序中如果不小心寫(xiě)了對.const段中的數據做寫(xiě)操作的指令,會(huì )引起保護錯誤,Windows會(huì )顯示一個(gè)提示框并結束程序。
Hello.exe – 應用程序錯誤
“0x00401000”指令引用 的”0x00402010”內存。該內存不能為”written”。
要終止程序,請單擊”確定”。
要調試程序,請單擊”取消”。
如果不怕程序可讀性不佳的話(huà),把.const段中定義的東西混到.code段中去也可以正常使用,因為.code段也是可以讀的。
代碼段
.code段是代碼段,所有的指令都必須寫(xiě)在代碼段中,在可執行文件中,代碼段是放在_TEXT節區中的。Win32環(huán)境中的數據段是不可執行的,只有代碼段有可執行的屬性。對于工作在特權級3的應用程序來(lái)說(shuō),.code段是不可寫(xiě)的,在編寫(xiě)DOS匯編程序的時(shí)候,好事的程序員往往有個(gè)習慣,就是靠改動(dòng)代碼段中的代碼來(lái)做一些反跟蹤的事情,如果企圖在Win32匯編下做同樣的事情,結果就是和上面同樣 “非法操作”。
當然事物總有兩面性,在Windows95下,在特權級0下運行的程序對所有的段都有讀寫(xiě)的權利,包括代碼段。另外,在優(yōu)先級3下運行的程序也不是一定不能寫(xiě)代碼段,代碼段的屬性是由可執行文件PE頭部中的屬性位決定的,通過(guò)編輯磁盤(pán)上的.exe文件,把代碼段屬性位改成可寫(xiě),那么在程序中就允許修改自己的代碼段。一個(gè)典型的應用就是一些針對可執行文件的壓縮軟件和加殼軟件,如Upx和PeCompact等,這些軟件靠把代碼段進(jìn)行變換來(lái)達到解壓縮和解密的目的,被處理過(guò)的可執行文件在執行時(shí)需要由解壓代碼來(lái)將代碼段解壓縮,這就需要寫(xiě)代碼段,所以這些軟件對可執行文件代碼段的屬性預先做修改。
程序結束和程序入口
在C語(yǔ)言源程序中,程序不必顯式地指定程序由哪里開(kāi)始執行,編譯器已經(jīng)約定好從main()函數開(kāi)始執行了。而在匯編程序中,并沒(méi)有一個(gè)main函數,程序員可以指定從代碼段的任何一個(gè)地方開(kāi)始執行,這個(gè)地方由程序最后一句的end語(yǔ)句來(lái)指定:
end [開(kāi)始地址]
這句語(yǔ)句同時(shí)表示源程序結束,所有的代碼必須在end語(yǔ)句之前。
end start
上述語(yǔ)句指定程序從start這個(gè)標號開(kāi)始執行。當然,start標號必須在程序的代碼段中有所定義。
但是,一個(gè)源程序不必非要指定入口標號,這時(shí)候可以把開(kāi)始地址忽略不寫(xiě),這種情況發(fā)生在編寫(xiě)多模塊程序的單個(gè)模塊的時(shí)候。當分開(kāi)寫(xiě)多個(gè)程序模塊時(shí),每個(gè)模塊的源程序中也可以包括.data、.data?、.const和.code段,結構就和上面的Win32 Hello World一樣,只是其他模塊最后的end語(yǔ)句必須不帶開(kāi)始地址。當最后把多個(gè)模塊鏈接在一起的時(shí)候,只能有一個(gè)主模塊指定入口地址,在多個(gè)模塊中指定入口地址或者沒(méi)有一個(gè)模塊指定了入口地址,鏈接程序都會(huì )報錯。
注釋和換行
注釋是源程序中不可忽略的一部分,匯編源程序的注釋以分號(;)開(kāi)始,注釋既可以在一行的頭部,也可以在一行的中間,一行中所有在分號之后的字符全部當做注釋處理,但在字符串的字義中包含的引號內的分號不當做是注釋的開(kāi)始。
;這里是注釋
call _PrintChar ;這里是注釋
szChar db ‘Hello, world; ’,0dh,0ah ;world后面的分號不是注釋?zhuān)竺娴牟攀?div style="height:15px;">
當源程序的某一行過(guò)長(cháng),不利于閱讀的時(shí)候,可以分行書(shū)寫(xiě),分行的辦法是在一行的最后用反斜杠(\)做換行符,如:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
可以寫(xiě)為:
invoke MessageBox, \
NULL, \ ;父窗口句柄
offset szText, \ ;消息框中的文字
offset szCaption, \ ;標題文字
MB_OK
一行的最后,指的是最后一個(gè)有用的字符,反斜杠后面多幾個(gè)空格或加上注釋并不影響換行符的使用,如上例所示,這一點(diǎn)和makefile文件中換行符的規定有所不同。
調用API
API是什么?
Win32程序是構筑在Win32 API基礎上的。在Win32 API中,包括了大量的函數、結構和消息等,它不僅為應用程序所調用,也是Windows自身的一部分,Windows自身的運行也調用這些API函數。
在DOS下,操作系統的功能是通過(guò)各種軟中斷來(lái)實(shí)現的,如大家都知道int 21h是DOS中斷,int 13h和int 10h是BIOS中的磁盤(pán)中斷和視頻中斷。當應用程序要引用系統功能時(shí),要把相應的參數放在各個(gè)寄存器中再調用相應的中斷,程序控制權轉到中斷中去執行,完成以后會(huì )通過(guò)iret中斷返回指令回到應用程序中。如DOS匯編下的Hello World程序中有下列語(yǔ)句:
mov ah,9
mov dx,offset szHello
int 21h
這3條語(yǔ)句調用DOS系統模塊中的屏幕顯示功能,功能號放在ah中,9號功能表示屏幕顯示,要輸出到屏幕上的內容的地址放在dx中,然后去調用int 21h,字符串就會(huì )顯示到屏幕上。
這個(gè)例子說(shuō)明了應用程序調用系統功能的一般過(guò)程。首先,系統提供功能模塊并約定參數的定義方法,同時(shí)約定調用的方式,同時(shí)約定調用的方式,應用程序按照這個(gè)約定來(lái)調用系統功能。在這里,ah中放功能號9,dx中放字符串地址就是約定的參數,int 21h是約定的調用方式。
下面來(lái)看看這種方法的不便這處。首先,所有的功能號定義是冷冰冰的數字,int 21h的說(shuō)明文檔是這樣的:
Int 21 Functions:
00 Programe termination
01 Keyboard input
02 Display output
03 AUX input
04 AUX output
05 Printer output
06 Direct console I/O
07 Direct STDIN input, no echo
08 Keyboard input, no echo
09 Print string
0A Buffered keyboard input
0B Check standard input status
再進(jìn)入09號功能看使用方法:
Print string (Func 09)
AH = 09h
DS:DX -> string terminated by “$”
這就是DOS時(shí)代匯編程序員都有一厚本《中斷大全》的原因,因為所有的功能編號包括使用的參數定義僅從字面上看,是看不出一點(diǎn)頭緒來(lái)的。
另外,80x86系列處理器能處理的中斷最多只能有256個(gè),不同的系統服務(wù)程序使用了不同的中斷號,這少得可憐的中斷數量就顯得太少了,結果到最后是中斷掛中斷,大家搶來(lái)?yè)屓サ?,把好好的一個(gè)系統搞得像接力賽跑一樣。
對于這些弱點(diǎn),程序員們都有個(gè)愿望:系統功能如果能以功能名作為子程序名直接調用就好了,參數也最好定義的有意義一點(diǎn),這樣一來(lái)寫(xiě)程序就會(huì )方便得多,編系統擴展模塊也就不必老是擔心往哪個(gè)中斷上面掛了,最好能把上面int 21h/ah=9的調用寫(xiě)成下面這副樣子:
call PrintString, addr szHello
終于,好消息出來(lái)了,Win32環(huán)境中的編程接口就是這個(gè)樣子,這就是API,它實(shí)際上是以一種新的方法代替了DOS中用軟中斷的方式。和DOS的結構相比,Win32的系統功能模塊放在Windows的動(dòng)態(tài)鏈接庫(DLL)中,DLL是一種Windows的可執行文件,采用的是和.exe文件同樣的PE格式,在PE格式文件頭的導出表中,以字符串形式指出了這個(gè)DLL能提供的函數列表。應用程序使用字符串類(lèi)型的函數名指定要調用的函數。
應用程序在使用的時(shí)候由Windows自動(dòng)載入DLL程序并調用相應的函數。
實(shí)際上,Win32的基礎就是由DLL組成的。Win32 API的核心由3個(gè)DLL提供,它們是:
KERNEL32.DLL——系統服務(wù)功能。包括內存管理、任務(wù)管理和動(dòng)態(tài)鏈接等。
GDI32.DLL——圖形設備接口。利用VGA與DRV之類(lèi)的顯示設備驅動(dòng)程序完成顯示文本和矩形等功能。
USER32.DLL——用戶(hù)接口服務(wù)。建立窗口和傳送消息等。
當然,Win32 API還包括其他很多函數,這些也是由DLL提供的,不同的DLL提供了不同的系統功能。如使用TCP/IP協(xié)議進(jìn)行網(wǎng)絡(luò )通信的DLL是Wsock32.dll,它所提供的API稱(chēng)為Socket API;專(zhuān)用于電話(huà)服務(wù)方面的API稱(chēng)為T(mén)API(Telephony API),包含在Tapi32.dll中,所有的這些DLL提供的函數組成了現在使用的Win32編程環(huán)境。
調用API
和在DOS中用中斷方式調用系統功能一樣,用API方式調用存放在DLL中的函數必須同樣約定一個(gè)規范,用來(lái)定義函數的調用方法、參數的傳遞方法和參數的定義,洋洋灑灑幾百MB的Windows系統比起才幾百KB規模的DOS,其系統函數的規模和復雜程度都上了一個(gè)數量級,所在使用一個(gè)API時(shí),帶的參數數量多達十幾個(gè)是常有的事,在DOS下用寄存來(lái)傳遞參數的方法顯然已經(jīng)不能勝任了。
Win32 API是用堆棧來(lái)傳遞參數的,調用者把參數一個(gè)個(gè)壓入堆棧,DLL中的函數程序再從堆棧中取出參數處理,并在返回之前將堆棧中已經(jīng)無(wú)用的參數丟棄。在Microsoft發(fā)布的《Microsoft Win32 Programmer’s Reference》中定義了常用API的參數和函數聲明,先來(lái)看消息框函數的聲明:
int MessageBox(
HWND hWnd, //handle to owner window
LPCTSTR lpText, //text in message box
LPCTSTR lpCaption, //message box title
UINT uType //message box style
);
最后還有一句說(shuō)明:
Library: Use User32.lib。
上述函數聲明說(shuō)明了MessageBox有4個(gè)參數,它們分別是HWND類(lèi)型的窗口句柄(hWnd),LPCTSTR類(lèi)型的要顯示的字符串地址(lpText)和標題字符串地址(lpCaption),還有UINT類(lèi)型的消息框類(lèi)型(uType)。這些數據類(lèi)型看起來(lái)很復雜,但有一點(diǎn)是很重要的,對于匯編語(yǔ)言來(lái)說(shuō),Win32環(huán)境中的參數實(shí)際上只有一種類(lèi)型,那就是一個(gè)32位的整數,所以這些HWND,LPCTSTR和UINT實(shí)際上就是匯編中的dword(double word,雙字型,4個(gè)字節,兩個(gè)字,32位),之所以定義為不同的模樣,是用來(lái)說(shuō)明了用途。由于Windows是用C寫(xiě)成的,世界上的程序員好像也是用C語(yǔ)言的最多,所以Windows所有編程資料發(fā)布的格式也是C格式。
上面的聲明用匯編的格式來(lái)表達就是:
MessageBox Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword
上面最后一句Library:Use User32.lib則說(shuō)明了這個(gè)函數包括在User32.dll中。
有了函數原型的定義后,就是調用的問(wèn)題了,Win32 API調用中要把參數放入堆棧,順序是最后一個(gè)參數最先進(jìn)棧,在匯編中調用MessageBox函數的方法是:
push uType
push lpCaption
push lpText
push hWnd
call MessageBox
在源程序編譯鏈接成可執行文件后,call MessageBox語(yǔ)句中的MessageBox會(huì )被換成一個(gè)地址,指向可執行文件中的導入表,導入表中指向MessageBox函數的實(shí)際地址會(huì )在程序裝入內存的時(shí)候,根據User32.dll在內存中的位置由Windows系統動(dòng)態(tài)填入。
使用invoke語(yǔ)句
API是可以調用了,另一個(gè)煩人的問(wèn)題又出現了,Win32的API動(dòng)輒就是十幾個(gè)參數,整個(gè)源程序一眼看上去基本上都是把參數壓堆棧的push指令,參數的個(gè)數和順序很容易搞錯,由此引起的莫名其妙的錯誤源源不斷,源程序的可讀性看上去也很差。如果寫(xiě)的時(shí)候少寫(xiě)了一句push指令,程序在編譯和鏈接的時(shí)候都不會(huì )報錯,但在執行的時(shí)候必定會(huì )崩潰,原因是堆棧對不齊了。
有不有解決的辦法呢?最好是像C語(yǔ)言一樣,能在同一句中打入所有的參數,并在參數使用錯誤的時(shí)候能夠提示。
好消息又來(lái)了,Microsoft終于做了一件好事,在MASM中提供了一個(gè)偽指令實(shí)現了這個(gè)功能,那就是invoke偽指令,它的格式是:
invoke 函數名 [,參數1][,參數2]…[,參數n]
對MessageBox的調用在MASM中可以寫(xiě)成:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
注意,invoke并不是80386處理器的指令,而是一個(gè)MASM編譯器的偽指令,在編譯的時(shí)候它把上面的指令展開(kāi)成我們需要的4個(gè)push指令和一個(gè)call指令,同時(shí),進(jìn)行參數數量的檢查工作,如果帶的參數數量和聲明時(shí)的數量不符,編譯器報錯:
error A2137: too few arguments to INVOKE
編譯時(shí)看到這樣的錯誤報告,首先要檢查的是有沒(méi)有少寫(xiě)一個(gè)參數。對于不帶參數的API調用,invoke偽指令的參數檢查功能可有可無(wú),所以既可以用call API_Name這樣的語(yǔ)法,也可以用invoke API_Name這樣的語(yǔ)法。
API函數的返回值
有的API函數有返回值,如MessageBox定義的返回值是int類(lèi)型的數,返回值的類(lèi)型對匯編程序來(lái)說(shuō)也只有dword一種類(lèi)型,它永遠放在eax中。如果要返回的內容不是一個(gè)eax所能容納的,Win32 API采用的方法一般是返回一個(gè)指針,或者在調用參數中提供一個(gè)緩沖區地址,干脆把數據直接返回到緩沖區中去。
函數的聲明
在調用API函數的時(shí)候,函數原型也必須預先聲明,否則,編譯器會(huì )不認這個(gè)函數。invoke偽指令也無(wú)法檢查參數個(gè)數。聲明函數的格式是:
函數名 proto [距離] [語(yǔ)言] [參數1]:數據類(lèi)型, [參數2]:數據類(lèi)型,
句中的proto是函數聲明的偽指令,距離可以是NEAR,FAR,NEAR16,NEAR32,FAR16或FAR32,Win32中只有一個(gè)平坦的段,無(wú)所謂距離,所以在定義時(shí)是忽略的;語(yǔ)言類(lèi)型就是.model那些類(lèi)型,如果忽略,則使用.model定義的默認值。
后面就是參數的列表了,對Win32匯編來(lái)說(shuō)只存在dword類(lèi)型的參數,所以所有參數的數據類(lèi)型永遠是dword,另外對于編譯器來(lái)說(shuō),它只關(guān)心參數的數量,參數的名稱(chēng)在這里是無(wú)用的,僅是為了可讀性而設置的,可以省略掉,所以下面兩句消息框函數的定義實(shí)際上是一樣的:
MessageBox Proto hWnd:dword, lpText:dword, lpCaption:dword, uType:dword
MessageBox Proto :dword, :dword, :dword, :dword
在Win32環(huán)境中,和字符串相關(guān)的API共有兩類(lèi),分別對應兩個(gè)字符集:一類(lèi)是處理ANSI字符集的,另一類(lèi)是處理Unicode字符集的。前一類(lèi)函數名字的尾部帶一個(gè)A字符,處理Unicode的則帶一個(gè)W字符。
我們比較熟悉的ANSI字符串是以NULL結尾的一串字符數組,每一個(gè)ANSI字符占一個(gè)字節寬。對于歐洲語(yǔ)言體系,ANSI字符集已足夠了,但對于有成千上萬(wàn)個(gè)不同字符的幾種東方語(yǔ)言體系來(lái)說(shuō),Unicode字符集更有用。每一個(gè)Unicode字符占兩個(gè)字節的寬度,這樣一來(lái)就可以在一個(gè)字符串中使用65536個(gè)不同的字符了。
MessageBox和顯示字符串有關(guān),同樣它有兩個(gè)版本,嚴格地說(shuō),系統中有兩個(gè)定義:
MessageBoxA Proto hWnd:dword, lpText:dword, lpCaption:dword, uType:dword
MessageBoxB Proto hWnd:dword, lpText:dword, lpCaption:dword, uType:dword
雖然《Microsoft Win32 Programmer’s Reference》中只有一個(gè)MessageBox定義,但User32.dll中確確實(shí)實(shí)沒(méi)有MessageBox,而只有MessageBoxA和MessageBoxW,那么為什么還是可以使用MessageBox呢?實(shí)際上在程序的頭文件user32.inc中有一句:
MessageBox equ <MessageBoxA>
它把MessageBox偷梁換柱變成了MessageBoxA。在源程序中繼續沿用MessageBox是為了程序的可讀性以及保持和手冊的一致性,但對于編譯器來(lái)說(shuō),實(shí)際是在使用MessageBoxA。
由于并不是每個(gè)Win32系統都支持W系統的API,在Windows 9x系列中,對Unicode是不支持的,很多的API只有ANSI版本,只有Windows NT系列才對Unicode完全支持。為了編寫(xiě)在幾個(gè)平臺中通用的程序,一般應用程序都使用ANSI版本的API函數集。
為了使程序更有移植性,在源程序中一般不直接指明使用Unicode還是ANSI版本,而是使用宏匯編中的條件匯編功能來(lái)統一替換,如在源程序中使用MessageBox,但在頭文件中定義:
if UNICODE
MessageBox equ <MessageBoxW>
else
MessageBox equ <MessageBoxA>
endif
所有涉及版本問(wèn)題的API都可以按此方法定義,然后在源程序的頭指定UNICODE=1或UNICODE=0,重新編譯后就能產(chǎn)生不同的版本。
include語(yǔ)句
對于所有要用到的API函數,在程序的開(kāi)始部分都必須預先聲明,但這一個(gè)步驟顯然是比較麻煩的,為了簡(jiǎn)化操作,可以采用各種語(yǔ)言通用的解決辦法,就是把所有的聲明預先放在一個(gè)文件中,在用到的時(shí)候再用include語(yǔ)句包含進(jìn)來(lái)?,F在回到Win32 Hello World程序,這個(gè)程序用到了兩個(gè)API函數:MessageBox和ExitProcess,它們分別在User32.dll和Kernel32.dll中,在MASM32工具包中已經(jīng)包括了所有DLL的API函數聲明列表,每個(gè)DLL對應<DLL名.inc>文件,在源程序中只要使用include語(yǔ)句包含進(jìn)來(lái)就可以了:
include user32.inc
include kernel32.inc
當用到其他的API函數時(shí),只需相應增加對應的include語(yǔ)句。
include語(yǔ)句還用來(lái)在源程序中包含別的文件,當多個(gè)源程序用到相同的函數定義、常量定義、甚至源代碼時(shí),可以把相同的部分寫(xiě)成一個(gè)文件,然后在不同的源程序中用include語(yǔ)句包含進(jìn)來(lái)。
編譯器對include語(yǔ)句的處理僅是簡(jiǎn)單地把這一行用指定的文件內容替換掉而而已。
include語(yǔ)句的語(yǔ)法是:
include 文件名
或 include <文件名>
當遇到要包括的文件名和MASM的關(guān)鍵字同名等可能會(huì )引起編譯器混淆的情況時(shí),可以用<>將文件名括起來(lái)。
includelib語(yǔ)句
在DOS匯編中,使用中斷調用系統功能是不必聲明的,處理器自己知道到中斷向量表中去取中斷地址。在Win32匯編中使用API函數,程序必須知道調用的API函數存在于哪個(gè)DLL中,否則,操作系統必須搜索系統中存在的所有DLL,并且無(wú)法處理不同DLL中的同名函數,這顯然是不現實(shí)的,所以,必須有個(gè)文件包括DLL庫正確的定位信息,這個(gè)任務(wù)是由導入庫來(lái)實(shí)現的。
在使用外部函數的時(shí)候,DOS下有函數庫的概念,那時(shí)的函數庫實(shí)際上是靜態(tài)庫,靜態(tài)庫是一組已經(jīng)編寫(xiě)好的代碼模塊,在程序中可以自由引用,在源程序編譯成目標文件,最后要鏈接可執行文件的時(shí)候,由link程序從庫中找出相應的函數代碼,一起鏈接到最后的可執行文件中。DOS下C語(yǔ)言的函數庫就是典型的靜態(tài)庫。庫的出現為程序員節省了大量的開(kāi)發(fā)時(shí)間,缺點(diǎn)就是每個(gè)可執行文件中都包括了要用到的相同函數的代碼,占用了大量的磁盤(pán)空間,在執行的時(shí)候,這些代碼同樣重復占用了寶貴的內存。
Win32環(huán)境中,程序鏈接的時(shí)候仍然要使用函數庫來(lái)定位函數信息,只不過(guò)由于函數代碼放在DLL文件中,庫文件中只留有函數的定位信息和參數數目等簡(jiǎn)單信息,這種庫文件叫做導入庫,一個(gè)DLL文件對應一個(gè)導入庫,如User32.dll文件用于編程的導入庫是User32.lib,MASM32工具包中包含了所有DLL的導入庫。
為了告訴鏈接程序使用哪個(gè)導入庫,使用的語(yǔ)句是:
includelib 庫文件名
或 includelib <庫文件名>
和include的用法一樣,在要包括讓編譯器混淆的文件名時(shí)加括號。
Win32 Hello World程序用到的兩個(gè)API函數MessageBox和ExitProcess分別在User32.dll和Kernel32.dll中,那么在源程序使用的相應語(yǔ)句為:
includelib user32.lib
includelib kernel32.lib
和include語(yǔ)句的處理不同,includelib不會(huì )把.lib文件插入到源程序中,它只是告訴鏈接器在鏈接的時(shí)候到指定的庫文件中去找而已。
API參數中的等值定義
再回過(guò)頭來(lái)看顯示消息框的語(yǔ)句:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
在uType這個(gè)參數中使用了MB_OK,這個(gè)MB_OK是什么意思?
在《Microsoft Win32 Programmer’s Reference》中的說(shuō)明:
uType——定義對話(huà)框的類(lèi)型,這個(gè)參數可以是以下標志的合集:
要定義消息框上顯示按鈕,用下面的某一個(gè)標志:
MB_ABORTRETRYIGNORE——消息框有三個(gè)按鈕:終止,重試和忽略
MB_HELP——消息框上顯示一個(gè)幫助按鈕,按下后發(fā)送WM_HELP消息
MB_OK——消息框上顯示一個(gè)確定按鈕,這是默認值
……
要在消息框中顯示圖標,用下面的某一個(gè)標志:
MB_ICONWARNING——顯示驚嘆號圖標
MB_ICONINFORMATION——顯示消息圖標
……
這些是uType參數說(shuō)明中的一小半,可以看出,參數可以用的值有很多種。
MB_ICONWARING和MB_YESNO等參數究竟是什么意思呢?
在Visual C++的目錄下中,可以找到頭文件WinUser.h,里面定義了如下一段內容:
/* MessageBox() Flags */
#define MB_OK 0x00000000L
#define MB_OKCANCEL 0x00000001L
#define MB_ABORTRETRYIGNORE 0x00000002L
#define MB_YESNOCANCEL 0x00000003L
#define MB_YESNO 0x00000004L
#define MB_RETRYCANCEL 0x00000005L
#define MB_ICONHAND 0x00000010L
#define MB_ICONQUESTION 0X00000020L
……
顯然,MB_YESNO就是4,MB——ICONWARNING就是30h,默認的MB_OK就是0,Win32 API的參數使用這樣的定義方法顯然是為了免除程序員死記數值定義的麻煩。在編寫(xiě)Win32匯編程序的時(shí)候,MASM32工具包中的Windows.inc也包括了所有這些參數的定義,只要在程序的開(kāi)頭包含這個(gè)定義文件:
include windows.inc
就可以方便地完全按照API手冊來(lái)使用Win32函數。
打開(kāi)\masm32\include 目錄下的Windows.inc查看一下,可以發(fā)現整個(gè)文件總共有兩萬(wàn)六千多行,包括了幾乎所有的Win32 API參數中的常量和數據結構定義。
標號、變量和數據結構
當程序中要跳轉到另一位置時(shí),需要有一個(gè)標識來(lái)指示新的位置,這就是標號,通過(guò)在目的地址的前面放上一個(gè)標號,可以在指令中使用標號來(lái)代替直接使用地址。
使用變量是任何編程語(yǔ)言都要遇到的工作,Win32匯編也不例外,在MASM中使用變量也有需要注意的幾個(gè)問(wèn)題,錯誤地使用變量定義或用錯誤的方法初始化變量會(huì )帶來(lái)難以定位的錯誤。
變量是計算機內存中已命名的存儲位置,在C語(yǔ)言中有很多種類(lèi)的變量,如整數型、浮點(diǎn)型和字符型等,不同的變量有不同的用途和尺寸,比如說(shuō)雖然長(cháng)整數和單精度浮點(diǎn)數都是32位長(cháng),但它們的用途不同。
顧名思義,變量的值在程序運行中是需要改變的,所以它必須定義在可寫(xiě)的段內,如.data和.data?,或者在堆棧內。按照定義的位置不同,MASM中的變量也分為全局變量和局部變量?jì)煞N。
在MASM中標號和變量的命名規范是相同的,它們是:
1)可以用字母、數字、下劃級及符號@、$和?。
2)第一個(gè)符號不能是數字。
3)長(cháng)度不能超過(guò)240個(gè)字符。
4)不能使用指令名等關(guān)鍵字。
5)在作用域內必須是唯一的。
標號
標號的定義
當在程序中使用一條跳轉指令的時(shí)候,可以用標號來(lái)表示跳轉的目的地,編譯器在編譯的時(shí)候會(huì )把它替換成地址,標號既可以定義在目的指令同一行的頭部,也可以在目的指令前一行單獨用一行定義,標號定義的格式是:
標號名: 目的指令
標號的作用域是當前的子程序,在單個(gè)子程序中的標號不能同名,否則編譯器不知該用哪個(gè)地址,但在不同的子程序中可以有相同名稱(chēng)的標號,這意味著(zhù)不能從一個(gè)子程序中用跳轉指令跳到另一個(gè)子程序中。
在低版本的MASM中,標號在整個(gè)程序中是唯一的,子程序中的標號也可以從整個(gè)程序的任何地方轉入。但Win32匯編使用的高版本MASM中不允許這樣,這是為了提供對局部變量和參數的支持,由于在子程序入口有對堆棧的初始化指令,所以一個(gè)子程序不允許有多個(gè)入口,其結果主是標號的作用域變成了單個(gè)子程序范圍。
MASM中的@@
在DOS時(shí)代,為標號起名是個(gè)麻煩的事情,因為匯編指令用到跳轉指令特別多,任何比較和測試等都要涉及跳轉,所以在程序中會(huì )有很多標號,在整個(gè)程序范圍內起個(gè)不重名的標號要費一番功夫,結果常常用addr1和addr2之類(lèi)的標號一直延續下去,如果后來(lái)要在中間插一個(gè)標號,那么就常常出現addr1_1和loop10_5之類(lèi)奇怪的標號。
實(shí)際上,很多標號會(huì )使用一到兩次,而且不一定非要起個(gè)有意義的名稱(chēng),如匯編程序中下列代碼結構很多:
mov cx,1234h
cmp flag,1
je loc1
mov cx,1000h
loc1:
loop loc1
loc1在別的地方就再也用不到了,對于這種情況,高版本的MASM用@@標號去代替它:
mov cx,1234h
cmp flag,1
je @F
mov cx,1000h
@@:
loop @B
當用@@做標號時(shí),可以用@F和@B來(lái)引用它,@F表示本條指令后的第一個(gè)@@標號,@B表示本條指令前的第一個(gè)@@標號,程序中可以有多個(gè)@@標號,@B和@F只尋找匹配最近的一個(gè)。
不要在間隔太遠的代碼中使用@@標號,因為在以后的修改中@@和@B,@F中間可能會(huì )被無(wú)意中插入一個(gè)新的@@,這樣一來(lái),@B或@F就會(huì )引用到錯誤的地方去,源程序中@@標號和跳轉指令之間的距離最好限制在編輯器能夠顯示的同一屏幕的范圍內。
全局變量
全局變量的定義
全局變量的作用域是整個(gè)程序,Win32匯編的全局變量定義在.data或.data?段內,可以同時(shí)定義變量的類(lèi)型和長(cháng)度,格式是:
變量名 類(lèi)型 初始值1, 初始值2,…
變量名 類(lèi)型 重復數量 dup (初始值1,初始值2,…)
MASM中可以定義的變量類(lèi)型相當多。
名稱(chēng)
表示方式
縮寫(xiě)
長(cháng)度(字節)
字節
byte
db
1
字
word
dw
2
雙字(double word)
dword
dd
4
三字(far word)
fword
df
6
四字(quad word)
qword
dq
8
十字節BCD碼(ten byte)
tbyte
dt
10
有符號字節(sign byte)
sbyte
1
有符號字(sign word)
sword
2
有符號雙字(sign dword)
sdword
4
單精度浮點(diǎn)數
real4
4
雙精度浮點(diǎn)數
real8
8
10字節浮點(diǎn)數
real10
10
所有使用到變量類(lèi)型的情況中,只有定義全局變量的時(shí)候類(lèi)型才可以用縮寫(xiě),現在先來(lái)看全局變量定義的幾個(gè)例子:
.data
wHour dw ? ;例1
wMinute dw 10 ;例2
_hWnd dd ? ;例3
word_Buffer dw 100 dup (1,2) ;例4
szBuffer byte 1024 dup (?) ;例5
szText db ‘Hello,world!’ ;例6
例1定義了一個(gè)未初始化的word類(lèi)型變量,名稱(chēng)為wHour。
例2定義了一個(gè)名為wMinute的word類(lèi)型變量。
例3定義了一個(gè)雙字類(lèi)型的變量_hWnd。
例4定義了一組字,以0001,0002,0001,0002,的順序在內存中重復100遍,一共是200個(gè)字節。
例5定義了一個(gè)1024字節的緩沖區。
例6定義了一個(gè)字符串,總共占用了12個(gè)字節。兩頭的單引號是定界的符號,并不屬于字符串中真正的內容。
在byte類(lèi)型變量的定義中,可以用引號定義字符串和數值定義的方法混用,假設要定義兩個(gè)字符串Hello,World!和Hello again,每個(gè)字符串后面中回車(chē)和換行符,最后以一個(gè)0字符結尾,可以定義如下:
szText db ‘Hello,World!’,0dh,0ah,’Hello again’,0dh,0ah,0
全局變量的初始化值
全局變量在定義中既可以指定初值,也可以只用問(wèn)題預留究竟,在.data?段中,只能用問(wèn)號預留究竟,因為.data?段中不能指定初始值,這里就有一個(gè)問(wèn)題:既然可以用問(wèn)號預留空間,那么在實(shí)際運行的時(shí)候,這個(gè)未初始化的值是隨機的還是確定的呢?在全局變量中,這個(gè)值就是0,所以用問(wèn)號指定的全局變量如果要以0為初始值的話(huà),在程序中可以不必為它賦值。
局部變量
局部變量這個(gè)名稱(chēng)最早源于高級語(yǔ)言,主要是為了定義一些僅在單個(gè)函數里面有用的變量而提出的,使用局部變量能帶來(lái)一些額外的好處,它使程序的模塊化封裝變得可能,試想一下,如果要用到的變量必須定義在程序的數據段里面,假設在一個(gè)子程序中要用到一些變量,當把這個(gè)子程序移植到別的程序時(shí),除了把代碼移過(guò)去以外,還必須把變量定義移過(guò)去。而即使把變量定義移過(guò)去了,由于這些變量定義在大家都可以用的數據段中,就無(wú)法對別的代碼保持透明,別的代碼有可能有意無(wú)意地修改它們。還有,在一個(gè)大的工程項目中,存在很多的子程序,所有的子程序要用到的變量全部定義在數據段中,會(huì )使數據段變得很大,混在一起的變量也使維護變得非常不方便。
局部變量這個(gè)概念出現以后,兩個(gè)以上子程序都要用到的數據才被定義為全局變量統一放在數據段中,僅在子程序內部使用的變量則放在堆棧中,這樣子程序可以編成黑匣子的模樣,使程序的模塊結構更加分明。
局部變量的作用域是單個(gè)子程序,在進(jìn)入子程序的時(shí)候,通過(guò)修改堆棧指針esp來(lái)預留出需要的空間,在用ret指令返回主程序之前,同樣通過(guò)恢復esp丟棄這些空間,這些變量就隨之無(wú)效了。它的缺點(diǎn)就是因為空間是臨時(shí)分配的,所以無(wú)法定義含有初始化值的變量,對局部變量的初始化一般在子程序中由指令完成。
在DOS時(shí)代,低版本的宏匯編本來(lái)無(wú)所謂全局變量和局部變量,所有的變量都是定義在數據段里面的,能讓被所有的子程序或主程序存取,就相當于現在所說(shuō)的全局變量,用匯編語(yǔ)言在堆棧中定義局部變量是很麻煩的一件事情。要和高級語(yǔ)言做混合編程的時(shí)候,程序員往往很痛苦地在邊上準備一張表,表上的內容是局部變量名和ebp指針的位置關(guān)系。
局部變量的定義
MASM用local偽指令提供了對局部變量的支持。定義的格式是:
local 變量名1 [[重復數量]] [:類(lèi)型], 變量名2 [[重復數量]] [:類(lèi)型] ……
local偽指令必須緊接在子程序定義的偽指令proc后、其他指令開(kāi)始前,這是因為局部變量的數目必須在子程序開(kāi)始的時(shí)候就確定下來(lái),在一個(gè)local語(yǔ)句定義不下的時(shí)候,可以有多個(gè)local語(yǔ)句,語(yǔ)法中的數據類(lèi)型不能用縮寫(xiě),如果要定義數據結構,可以用數據結構的名稱(chēng)當做類(lèi)型。Win32匯編默認的類(lèi)型是dword,如果定義dword類(lèi)型的局部變量,則類(lèi)型可以省略。當定義數組的時(shí)候,可以[]括號起來(lái)。不能使用定義全局變量的dup偽指令。局部變量不能和已定義的全局變量同名。局部變量的作用域是當前子程序,所以在不同的子程序中可以有同名的局部變量。
定義局部變量的例子:
local local[1024]:byte ;例1
local loc2 ;例2
local loc3:WNDCLASS ;例3
例1定義了一個(gè)1024字節長(cháng)的局部變量loc1。
例2定義了一個(gè)名為loc2的局部變量,類(lèi)型是默認值dword。
例3定義了一個(gè)WNDCLASS數據結構,名為loc3。
下面是局部變量使用的一個(gè)典型的例子:
TestProc proc
local @loc1:dword, @loc2:word
local @loc3:byte
mov eax,@loc1
mov ax,@loc2
mov al,@loc3
ret
TestProc endp
這是一個(gè)名為T(mén)estProc的子程序,用local語(yǔ)句定義了3個(gè)變量,@loc1是dword類(lèi)型,@loc2是word類(lèi)型,@loc3是byte類(lèi)型,在程序中分別有3句存取3個(gè)局部變量的指令,然后就返回了,編譯成可執行文件后,再把它反匯編就得到以下指令:
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
:00401003 83C4F8 add esp, FFFFFFF8
:00401006 8B45FC mov eax, dword ptr [ebp-04]
:00401009 668B45FA mov ax, word ptr [ebp-06]
:0040100D 8A45F9 mov al, byte ptr [ebp-07]
:00401010 C9 leave
:00401011 C3 ret
可以看到,反匯編后的指令比源程序多了前后兩段指令,它們是:
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
:00401003 83C4F8 add esp, FFFFFFF8
:00401010 C9 leave
這些就是使用局部變量所必需的指令,分別用于局部變量的準備工作和掃尾工作。執行了call指令后,CPU把返回的地址壓入堆棧,再轉移到子程序執行,esp在程序的執行過(guò)程中可能隨時(shí)用到,不可能用esp來(lái)隨時(shí)存取局部變量,ebp寄存器是以堆棧段為默認數據段的,所以,可以用ebp做指針,于是,在初始化前,先用一句push ebp指令把原來(lái)的dbp保存起來(lái),然后把esp的值放到ebp中,供存取局部變量做指針用,再后面就是堆棧中預留空間了,由于堆棧是向下增長(cháng)的,所以要在esp中加一個(gè)負值,FFFFFFF8就是-8,慢著(zhù)!一個(gè)dword加一個(gè)word加一個(gè)字節不是7嗎,為什么是8呢?這是因為在80386處理器中,以dword為界對齊時(shí)存取內存速度最快,所以MASM寧可浪費一個(gè)字節,執行了這3句指令后,初始化完成,就可以進(jìn)行正常的操作了,從指令中可以看出局部變量在堆棧中的位置排列。
在程序退出的時(shí)候,必須把正確的esp設置回去,否則,ret指令會(huì )從堆棧中取出錯誤的地址返回,看程序可以發(fā)現,ebp就是正確的esp值,因為子程序開(kāi)始的時(shí)候已經(jīng)有一句mov ebp,esp,所以要返回的時(shí)候只要先mov esp,ebp,然后再pop ebp,堆棧就是正確的了。
在80386指令集中有一條指令可以在一句中實(shí)現這些功能,就是leave指令,所以,編譯器在ret指令之前只使用了一句leave指令。
明白了局部變量使用的原理,就很容易理解使用時(shí)的注意點(diǎn):ebp寄存器是關(guān)鍵,它起到保存原始esp的作用,并隨時(shí)用做存取局部變量的指針基址,所以在任何時(shí)刻,不要嘗試把ebp用于別的用途,否則會(huì )帶來(lái)意想不到的后果。
Win32匯編中局部變量的使用方法可以解釋一個(gè)很有趣的現象:在DOS匯編的時(shí)候,如果在子程序中的push指令和pop指令不配對,那么返回的時(shí)候ret指令從堆棧里得到的肯定是錯誤的返回地址,程序也就死掉了。但在Win32匯編中,push指令和pop指令不配對可能在邏輯上產(chǎn)生錯誤,卻不會(huì )影響子程序正常返回,原因就是在返回的時(shí)候esp不是靠相同數量的push和pop指令來(lái)保持一致的,而是靠leave指令從保存在ebp中的原始值中取回來(lái)的,也就是說(shuō),即使把esp改得一塌糊涂也不會(huì )影響到子程序的返回,當然,竅門(mén)就在ebp,把ebp改掉,程序就玩完了!
局部變量的初始化值
顯然,局部變量是無(wú)法在定義的時(shí)候指定初始化值的,因為local偽指令只是簡(jiǎn)單地把空間給留出來(lái),那么開(kāi)始使用時(shí)它里面是什么值呢?和全局變量不一樣,局部變量的初始值是隨機的,是其他子程序執行后在堆棧里留下的垃圾,所以,對局部變量的值一定要初始化,特別是定義為結構后當參數傳遞給API函數的時(shí)候。
在A(yíng)PI函數使用的大量數據結構中,往往用0做默認值,如果用局部變量定義數據結構,初始化時(shí)只定義了其中的一些字段,那么其余字段的當前值可以是編程者預想不到的數值,傳給API函數后,執行的結果可能是意想不到的,這是初學(xué)者很容易忽略的一個(gè)問(wèn)題。所以最好的辦法是:在賦值前首先將整個(gè)數據結構填0,然后再初始化要用的字段,這樣其余的字段就不必一個(gè)個(gè)地去填0了,RtlZeroMemory這個(gè)API函數就是實(shí)現填0的功能的。
數據結構
數據結構實(shí)際上是由多個(gè)字段組成的數據樣板,相當于一種自定義的數據類(lèi)型,數據結構中間的每一個(gè)字段可以是字節、字、雙字、字符串或所有可能的數據類(lèi)型。
比如在A(yíng)PI函數RegisterClass中要使用到一個(gè)叫做WNDCLASS的數據結構,Microsoft的手冊上是如下定義的
typeof struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
Int cbClsExtra;
Int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
}WNDCLASS, *PWNDCLASS;
注意,這是C語(yǔ)言格式的,這個(gè)數據結構包含了10個(gè)字段,字段的名稱(chēng)是style,lpfnWndProc和cbClsExtra等,前面的UINT和WNDPROC等是這些字段的類(lèi)型,在匯編中,數據結構的寫(xiě)法如下:
結構名 struct
字段1 類(lèi)型 ?
字段2 類(lèi)型 ?
……
結構名 ends
上面的WNDCLASS結構定義用匯編的格式來(lái)表示就是:
WNDCLASS struct
Style DWORD ?
LpfnWndProc DWORD ?
cbClsExtra DWORD ?
cbWndExtra DWORD ?
hInstance DWORD ?
hIcon DWORD ?
hCursor DWORD ?
hbrBackground DWORD ?
lpszMenuName DWORD ?
lpszClassName DWORD ?
WNDCLASS ends
和大部分的常量一樣,幾乎所有API所涉及的數據結構在Windows.inc文件中都已經(jīng)有定義了。要注意的是,定義了數據結構實(shí)際上只是定義了一個(gè)樣板,上面的定義語(yǔ)句并不會(huì )在哪個(gè)段中產(chǎn)生數據,和Word中使用各種信紙與文書(shū)等模板類(lèi)似,定義了數據結構以后就可以多次在源程序中用這個(gè)樣板當做數據類(lèi)型來(lái)定義數據,使用數據結構在數據段中定義數據的方法如下:
.data?
stWndClass WNDCLASS <>
……
.data
stWndClass WNDCLASS <1,1,1,1,1,1,1,1,1,1,>
……
這個(gè)例子定義了一個(gè)以WNDCLASS為結構的變量stWndClass,第一段的定義方法是未初始化的定義方法,第二段是在定義的同時(shí)指定結構中各字段的初始化值,各字段的初始值用逗號隔開(kāi),在這個(gè)例子中10個(gè)字段的初始值都指定為1。
在匯編中,數據結構的引用方法有好幾種,以上面的定義為例,如果要使用stWndClass中的lpfnWndProc字段,最直接的辦法是:
mov eax,stWndClass.lpfnWndProc
它表示把lpfnWndProc字段的值放入eax中去,假設stWndClass在內存中的地址是403000h,這句指令會(huì )被編譯成mov eax,[403004h],因為lpfnWndProc是stWndClass中的第二個(gè)字段,第一個(gè)字段是dword,已經(jīng)占用了4字節的空間。
在實(shí)際使用中,常常有使用指令存取數據結構的情況,如果使用esi寄存器做指針尋址,可以使用下列語(yǔ)句完成同樣的功能:
mov esi,offset stWndClass
move ax,[esi + WNDCLASS.lpfnWndProc]
注意:第二句是[esi + WNDCLASS.lpfnWndProc]而不是[esi + stWndClass.lpfnWndProc],因為前者被編譯成mov eax,[esi + 4],而后者被編譯成mov eax,[esi + 403004h],后者的結果顯然是錯誤的!如果要對一個(gè)數據結構中的大量字段進(jìn)行了操作,這種寫(xiě)法顯然比較煩瑣,MASM還有一個(gè)用法,可以用assume偽指令把寄存器預先定義為結構指針,再進(jìn)行操作:
mov esi,offset stWndClass
assume esi:ptr WNDCLASS
move ax,[esi].lpfnWndProc
……
assume esi:nothing
這樣,使用寄存器也可以用逗號引用字段名,程序的可讀性比較好。這樣的寫(xiě)法在最后編譯成可執行程序的時(shí)候產(chǎn)生同樣的代碼。注意:在不再使用esi寄存器做指針的時(shí)候要用assume esi:nothing取消定義。
結構的定義也可以嵌套,如果要定義一個(gè)新的NEW_WNDCLASS結構,里面包含一個(gè)老的WNDCLASS結構和一個(gè)新的dwOption字段,那么可以如下定義:
NEW_WNDCLASS struct
DwOption dword ?
OldWndClass WNDCLASS <>
NEW_WNDCLASS ends
假設現在esi是指向一個(gè)NEW_WNDCLASS的指針,那么引用里面嵌套的oldWndClass中的lpfnWndProc字段時(shí),就可以用下面的語(yǔ)句:
move ax,[esi].oldWndClass.lpfnWndProc
結構的嵌套在Windows的數據定義中也常有,熟練掌握數據結構的使用對Win32匯編編程是很重要的!
變量的使用
以不同的類(lèi)型訪(fǎng)問(wèn)變量
這個(gè)話(huà)題有點(diǎn)像C語(yǔ)言中的數據類(lèi)型強制轉換,C語(yǔ)言中的類(lèi)型轉換指的是把一個(gè)變量的內容轉換成另外一種類(lèi)型,轉換過(guò)程中,數據的內容已經(jīng)發(fā)生了變化,如把浮點(diǎn)數轉換成整數后,小數點(diǎn)后的內容就丟失了。在MASM中以不同的類(lèi)型訪(fǎng)問(wèn)不會(huì )對變量造成影響。
例如,以db方式定義一個(gè)緩沖區:
szBuffer db 1024 dup (?)
然后從其他地方取得了數據,但數據的格式是字方式組織的,要處理數據,最有效的方法是兩個(gè)字節兩個(gè)字節處理,但如果在程序中把szBuffer的值放入ax:
mov ax,szBuffer
編譯器會(huì )報一個(gè)錯:
error A2070: invalid instruction operands
意思是無(wú)效的指令操作,為什么呢?因為szBuffer是用db定義的,而ax的尺寸是一個(gè)word,等于兩個(gè)字節,尺寸不符合。MASM中,如果要用指定類(lèi)型之外的長(cháng)度訪(fǎng)問(wèn)變量,必須顯式地指出要訪(fǎng)問(wèn)的長(cháng)度,這樣,編譯器忽略語(yǔ)法上的長(cháng)度檢驗,僅使用變量的地址。使用的方法是:
類(lèi)型 ptr 變量名
類(lèi)型可以是byte, word, dword, fword, qword, real8和real10。如:
mov ax,word ptr szBuffer
mov eax,dword ptr szBuffer
DOS匯編中也有這種用法。
上述語(yǔ)句能通過(guò)編譯,當然,類(lèi)型必須和操作的寄存器長(cháng)度匹配。在這里要注意的是,指定類(lèi)型的參數訪(fǎng)問(wèn)并不會(huì )去檢測長(cháng)度是否溢出,看下面一段代碼:
.data
bTest1 db 12h
wTest2 dw 1234h
dwTest3 dd 12345678h
……
.code
mov al,bTest1
mov ax,word ptr bTest1
mov eax,dword ptr bTest1
……
上面的程序片斷,每一句執行后寄存器中的值是什么呢,mov al,bTest1這一句很顯然使al等12h,下面的兩句呢,ax和eax難道等于0012h和00000012h嗎?實(shí)際運行結果是3412h和78123412h,為什么呢?(DOS匯編基礎不錯的同學(xué),應該能理解)先來(lái)看反匯編的內容:
: .data段中的變量
:00403000 12 34 12 78 56 34 12 …
: .code段中的代碼
:00401000 A000304000 mov al, byte ptr [00403000]
:00401005 66A100304000 mov ax, word ptr [00403000]
:0040100B A100304000 mov eax, dword ptr [00403000]
.data段中的變量是按順序從低地址往高地址排列的,對于超過(guò)一個(gè)字節的數據,80386處理器的數據排列方式是低位數據在低地址,所以wTest2的1234h在內存中的排列是34h 12h,因為34h是低位。同樣,dwTest3在內存中以78h 56h 34h 12h從低地址往高地址存放,在執行指令mov ax,word ptr bTest1的時(shí)候,是從bTest1的地址403000h處取一個(gè)字,其長(cháng)度已經(jīng)超過(guò)了bTest1的范圍并落到了wTest2中,從內存中看,是取了bTest1的數據12h和wTest2的低位34h,在這兩個(gè)字節中,12h位于低地址,所以ax中的數值是3412h。同理,看另一條指令:
move ax,dword ptr bTest1
這條指令取了bTest1,wTest2的全部和dwTest3的最低位78h,在內存中的排列是12h 34h 12h 78h,所以eax等于78123412h。
這個(gè)例子說(shuō)明了匯編中用ptr強制覆蓋變量長(cháng)度的時(shí)候,實(shí)質(zhì)上是只用了變量的地址而禁止編譯器進(jìn)行檢驗,編譯器并不會(huì )考慮定界的問(wèn)題,程序員在使用的時(shí)候必須對內存中的數據排列有個(gè)全局概念,以免越界存取到意料之外的數據。
如果程序員的本意是類(lèi)似于C語(yǔ)言的強制類(lèi)型轉換,想把bTest1的一個(gè)字節擴展到一個(gè)字或一個(gè)雙字再放到ax或eax中,高位保持0而不是越界存取到其他的變量,可以用80386的擴展指令來(lái)實(shí)現。80386處理器提供的movzx指令可以實(shí)現這個(gè)功能,例如:
movzx ax,bTest1 ;例1
movzx eax,bTest1 ;例2
movzx eax,cl ;例3
movzx eax,ax ;例4
例1把單字節變量bTest1的值擴展到16位放入ax中。
例2把單字節變量bTest1的值擴展到32位放入eax中。
例3把cl中的8位值擴展到32位放入eax中。
例4把ax中的16位值擴展到32位放入eax中。
用movzx指令進(jìn)行數據長(cháng)度擴展是Win32匯編中經(jīng)常用到的技巧。
變量的尺寸和數量
在源程序中用到變量的尺寸和數量的時(shí)候,可以用sizeof和lengthof偽指令來(lái)實(shí)現,格式是:
sizeof 變量名、數據類(lèi)型或數據結構名
lengthof 變量名
sizeof偽指令可以取得變量、數據類(lèi)型或數據結構以字節為單位的長(cháng)度,lengthof可以取得變量中數據的項數。例如定義了以下數據:
stWndClass WNDCLASS <>
szHello db ‘Hello,world!’,0
dwTest dd 1,2,3,4
……
.code
……
mov eax, sizeof stWndClass
mov ebx, sizeof WNDCLASS
mov ecx, sizeof szHello
mov edx, sizeof dword
mov esi, sizeof dwTest
執行后eax的值是stWndClass結構的長(cháng)度40,ebx同樣是40,ecx的值是13,就是Hello,world!字符串的長(cháng)度加上一個(gè)字節的0結束符,edx的值是一個(gè)雙字的長(cháng)度:4,而esi則等于4個(gè)雙字的長(cháng)度16。
如果把所有的sizeof換成lengthof,那么eax會(huì )等于1,因為只定義了1項WNDCLASS,而ecx同樣等于13,esi則等于4,而lenghof WNDCLASST和lengthof dword是非法的用法,編譯程序會(huì )報錯。
要注意的是,sizeof和lengthof的數值是編譯時(shí)產(chǎn)生的,由編譯器傳遞到指令中去,上邊的指令最后產(chǎn)生的代碼就是:
mov eax,40
mov ebx,40
mov ecx,13
mov edx,4
mov esi,16
如果為了把Hello和World分兩行定義,szHello是這樣定義的:
szHello db ‘Hello’,odh,oah
db ‘World’,0
那么sizeof szHello是多少呢?注意!是7而不是13,MASM中的變量定義只認一行,后一行db ‘World’,0實(shí)際上是另一個(gè)沒(méi)有名稱(chēng)的數據定義,編譯器認為sizeof szHello是第一行字符的數量。雖然把szHello的地址當參數傳給MessageBox等函數顯示時(shí)會(huì )把兩行都顯示出來(lái),但嚴格地說(shuō)這是越界使用變量。雖然在實(shí)際的應用中這樣定義長(cháng)字符串的用法很普遍,因為如果要顯示一屏幕幫助,一行是不夠的,但要注意的是:要用到這種字符串的長(cháng)度時(shí),千萬(wàn)不要用sizeof去表示,最好是在程序中用lstrlen函數去計算。
獲取變量地址
獲取變量地址的操作對于全局變量和局部變量是不同的。
對于全局變量,它的地址在編譯的時(shí)候已經(jīng)由編譯器確定了,它的用法大家都不陌生:
mov 寄存器, offset 變量名
其中offset是取變量地址的偽操作符,和sizeof偽操作符一樣,它僅把變量的地址帶到指令中去,這個(gè)操作是在編譯時(shí)而不是在運行時(shí)完成的。
對于局部變量,它是用ebp來(lái)做指針操作的,假設ebp的值是40100h,那么局部變量l的地址是ebp-4即400FCh,由于ebp的值隨著(zhù)程序的執行環(huán)境不同可能是不同的,所以局部變量的地址值在編譯的時(shí)候也是不確定的,不可能用offset偽操作符來(lái)獲取它的地址。
80386處理器中有一條指令用來(lái)取指針的地址,就是lea指令,如:
lea eax,[ebp-4]
該指令可以在運行時(shí)按照ebp的值實(shí)際計算出地址放到eax中。
如果要在invoke偽指令的參數中用到一個(gè)局部變量的地址,該怎么辦呢?參數中是不可能寫(xiě)入lea指令的,用offset又是不對的。MASM對此有一個(gè)專(zhuān)用的偽操作符addr,其格式為:
addr 局部變量名和全局變量名
當addr后跟全局變量名的時(shí)候,用法和offset是相同的;當addr后面跟局部變量名的時(shí)候,編譯器自動(dòng)用lea指令先把地址取到eax中,然后用eax來(lái)代替變量地址使用。注意addr偽操作符只能在invoke的參數中使用,不能用在類(lèi)似于下列的場(chǎng)合:
move ax, addr 局部變量名 ;注意:錯誤用法
假設在一個(gè)子程序中有如下invoke指令:
invoke Test,eax, addr szHello
其中Test是一個(gè)需要兩個(gè)參數的子程序,szHello是一個(gè)局部變量,會(huì )發(fā)生什么結果呢?編譯器會(huì )把invoke偽指令和addr翻譯成下面這個(gè)模樣:
lea eax,[ebp-4]
push eax ;參數2:addr szHello
push eax ;參數1:eax
call Test
發(fā)現了什么?到push第一個(gè)參數eax之前,eax的值已經(jīng)被lea eax,[ebp-4]指令覆蓋了!也就是說(shuō),要用到的eax的值不再有效,所以,當在invoke中使用addr偽操作符時(shí),注意在它的前面不能用eax,否則eax的值會(huì )被覆蓋掉,當然eax在addr的后面的參數中用是可以的。幸虧M(mǎn)ASM編譯器對這種情況有如下錯誤提示:
error A2133:register value overwritten by INVOKE
否則,不知道又會(huì )引出多少莫名其妙的錯誤!
使用子程序
當程序中相同功能的一段代碼用得比較頻繁時(shí),可以將它分離出來(lái)寫(xiě)成一個(gè)子程序,在主程序中用call指令來(lái)調用它。這樣可以不用重復寫(xiě)相同的代碼,而用call指令就可以完成多次同樣的工作了。Win32匯編中的子程序也采用堆棧來(lái)傳遞參數,這樣就可以用invoke偽指令來(lái)進(jìn)行調用和語(yǔ)法檢查工作。
子程序的定義
子程序的定義方式如下所示:
子程序名 proc [距離] [語(yǔ)言類(lèi)型] [可視區域] [USES寄存器列表] [,參數:類(lèi)型]…[VARARG]
local 局部變量列表
指令
子程序名 endp
proc和endp偽指令定義了子程序開(kāi)始和結束的位置,proc后面跟的參數是子程序的屬性和輸入參數。子程序的屬性有:
距離??梢允荖EAR,FAR,NEAR16,NEAR32,FAR16或FAR32,Win32中只有一個(gè)平坦的段,無(wú)所謂距離,所以對距離的定義往往忽略。
語(yǔ)言類(lèi)型表示參數的使用方式和堆棧平衡的方式,可以是StdCall,C,SysCall,BASIC,FORTRAN和PASCAL,如果忽略,則使用程序頭部.model定義的值。
可視區域,可以是PRIVATE,PUBLIC和EXPORT。PRIVATE表示子程序只對本模塊可見(jiàn);PUBLIC表示對所有的模塊可見(jiàn)(在最后編譯鏈接完成的.exe文件中);EXPORT表示是導出的函數,當編寫(xiě)DLL的時(shí)候要將某個(gè)函數導出的時(shí)候可以這樣使用。默認的設置是PUBLIC。
USES寄存器列表,表示由編譯器在子程序指令開(kāi)始前自動(dòng)安排push這些寄存器的指令,并且在ret前自動(dòng)安排pop指令,用于保存執行環(huán)境,但筆者認為不如自己在開(kāi)頭和結尾用pushad和popad指令一次保存和恢復所有寄存器來(lái)得方便。
參數和類(lèi)型。參數指參數的名稱(chēng),在定義參數名的時(shí)候不能跟全局變量和子程序中的局部變量重名。對于類(lèi)型,由于Win32中的參數類(lèi)型只有32位(dword)一種類(lèi)型,所以可以省略。在參數定義的最后還可以跟VARARG,表示在已確定的參數后還可以跟多個(gè)數量不確定的參數,在Win32匯編中唯一使用VARARG的API就是wsprintf,類(lèi)似于C語(yǔ)言中的printf,其參數的個(gè)數取決于要顯示的字符串中指定的變量個(gè)數。
完成了定義之后,可以用invoke偽指令來(lái)調用子程序,當invoke偽指令位于子程序代碼之前的時(shí)候,處理到invoke語(yǔ)句的時(shí)候編譯器還沒(méi)有掃描到子程序定義信息的記錄,所以會(huì )有以下錯誤的信息:
error A2006: undefined symbol: _ProcWinMain
這并不是說(shuō)子程序的編寫(xiě)有錯誤,而是invoke偽指令無(wú)法得知子程序的定義情況,所以無(wú)法進(jìn)行參數的檢測。在這種情況下,為了讓invoke指令能正常使用,必須在程序的頭部用proto偽操作定義子程序的信息,提前告訴invoke語(yǔ)句關(guān)于子程序的信息,當然,如果子程序定義在前的話(huà),用proto的定義就可以省略了。
由于程序的調試過(guò)程中可能常常對一些子程序的參數個(gè)數進(jìn)行調整,為了使它們保持一致,就需要同時(shí)修改proc語(yǔ)句和proto語(yǔ)句。在寫(xiě)源程序的時(shí)候有意識地把子程序的位置提到invoke語(yǔ)句的前面,省略掉proto語(yǔ)句,可以簡(jiǎn)化程序和避免出錯。
參數傳遞和堆棧平衡
了解了子程序的定義方法后,讓我們繼續深入了解了程序的使用細節。在調用子程序時(shí),參數的傳遞是通過(guò)堆棧進(jìn)行的,也就是說(shuō),調用者把要傳遞給子程序的參數壓入堆棧,子程序在堆棧中取出相應的值再使用,比如,如果要調用:
SubRouting(Var1, Var2, Var3)
經(jīng)過(guò)編譯后的最終代碼可能是(注意只是可能):
push Var3
push Var2
push Var1
call SubRouting
add esp,12
也就是說(shuō),調用者首先把參數壓入堆棧,然后調用子程序,在完成后,由于堆棧中先前壓入的數不再有用,調用者或者被調用者必須有一方把堆棧指針修正到調用前的狀態(tài),即堆棧的平衡。參數是最右邊的先入堆棧還是最左邊的先入堆棧、還有由調用者還是被調用者來(lái)修正堆棧都必須有個(gè)約定,不然就會(huì )產(chǎn)生錯誤的結果,這就是在上述文字中使用“可能”這兩個(gè)字的原因。各種語(yǔ)言中調用子程序的約定是不同的,所以在proc以及proto語(yǔ)句的語(yǔ)言屬性中確定語(yǔ)言類(lèi)型后,編譯器才可能將invoke偽指令翻譯成正確的樣子,不同語(yǔ)言的不同點(diǎn)如下:
C
SysCall
StdCall
BASIC
FORTRAN
PASCAL
最先入棧參數
右
右
右
左
左
左
清除堆棧者
調用者
子程序
子程序
子程序
子程序
子程序
允許使用VARARG
是
是
是
否
否
否
注:VARARG表示參數的個(gè)數可以是不確定的,如wsprintf函數,本表中特殊的地方是StdCall的堆棧清除平時(shí)是由子程序完成的,但使用VARARG時(shí)是由調用者清除的。
為了了解編譯器對不同類(lèi)型子程序的處理方式,先來(lái)看一段源程序:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sub1 proc C _Var1,_Var2
mov eax, _Var1
mov ebx,_Var2
ret
Sub1 endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sub2 proc PASCAL _Var1, _Var2
mov eax, _Var1
mov ebx, _Var2
ret
Sub2 endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sub3 proc _Var1, _Var2
mov eax,_Var1
mov ebx,_Var2
ret
Sub3 endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
……
invoke Sub1,1,2
invoke Sub2,1,2
invoke Sub3,1,2
編譯后再進(jìn)行反匯編,看編譯器是如何轉換處理不同類(lèi)型的子程序的:
;這里是Sub1 – C類(lèi)型
:00401000 55 push ebp
:00401001 8BEC mov ebp,esp
:00401003 8B4508 mov eax, dword ptr [ebp+08]
:00401006 8B5D0C mov ebx, dword ptr [ebp+0C]
:00401009 C9 leave
:0040100A C3 ret
;這里是Sub2 – PASCAL類(lèi)型
:0040100B 55 push ebp
:0040100C 8BEC mov ebp,esp
:0040100E 8B450C move ax, dword ptr [ebp+0C]
:00401011 8B5D08 mov ebx, dword ptr [ebp+08]
:00401014 C9 leave
:00401015 C20800 ret 0008
;這里是Sub3 – StdCall類(lèi)型
:00401018 55 push ebp
:00401019 8BEC mov ebp,esp
:0040101B 8B4508 mov eax, dword ptr [ebp+08]
:0040101E 8B5D0C mov ebx, dword ptr [ebp+0C]
:00401021 C9 leave
:00401022 C20800 ret 0008
……
;這里是invoke Sub1,1,2 – C類(lèi)型
:00401025 6A02 push 00000002
:00401027 6A01 push 00000001
:00401029 E8D2FFFFFF call 00401000
:0040102E 83C408 add esp,00000008
;這里是invoke Sub2,1,2 -- PASCAL類(lèi)型
:00401031 6A01 push 00000001
:00401033 6A02 push 00000002
:00401035 E8D1FFFFFF call 0040100B
;這里是invoke Sub3,1,2 – StdCall類(lèi)型
:0040103A 6A02 push 00000002
:0040103C 6A01 push 00000001
:0040103E E8D5FFFFFF call 00401018
可以清楚地看到,在參數入棧順序上,C類(lèi)型和StdCall類(lèi)型是先把右邊的參數先壓入堆棧,而PASCAL類(lèi)型是先把左邊的參數壓入堆棧。在堆棧平衡上,C類(lèi)型是在調用者在使用call指令完成后,自行用add esp,8指令把8個(gè)字節的參數空間清除,而PASCAL和StdCall的調用者則不管這個(gè)事情,堆棧平衡的事情是由子程序用ret 8來(lái)實(shí)現的,ret指令后面加一個(gè)操作數表示在ret后把堆棧指針esp加上操作數,完成的是同樣的功能。
Win32約定的類(lèi)型是StdCall,所以在程序中調用子程序或系統API后,不必自己來(lái)平衡堆棧,免去了很多麻煩。
存取參數和局部變量都是通過(guò)堆棧來(lái)定義的,所以參數的存取也是通過(guò)ebp做指針來(lái)完成的。在探討局部變量的時(shí)候,已經(jīng)就沒(méi)有參數的情況下ebp指針和局部變量的對應關(guān)系做了分析,現在來(lái)分析一下ebp指針和參數之間的對應關(guān)系,注意,這里是以Win32中的StdCall為例,不同的語(yǔ)言類(lèi)型,指針的順序可能是不同的。
假定在一個(gè)子程序中有兩個(gè)參數,主程序調用時(shí)在push第一個(gè)參數前的堆棧指針esp為X,那么壓入兩個(gè)參數后的esp為X-8,程序開(kāi)始執行call指令,call指令把返回地址壓入堆棧,這時(shí)候esp為X-C,接下去是子程序中用push ebp來(lái)保存ebp的值,esp變?yōu)閄-10,再執行一句mov ebp,esp,就可以開(kāi)始用ebp存取參數和局部變量了。
在源程序中,由于參數、局部變量和ebp的關(guān)系是由編譯器自動(dòng)維護的,所以讀者不必關(guān)心它們的具體關(guān)系,但到了用Soft-ICE等工具來(lái)分析其他軟件的時(shí)候,遇到調用子程序的時(shí)候一定要先看清楚它們之間的類(lèi)型差別。
在子程序中使用參數,可以使用與存取局部變量同樣的方法,因為這兩者的構造原理幾乎一模一樣,所以,在子程序中有invoke語(yǔ)句時(shí),如果要用到輸入參數的地址當做invoke的參數,同樣要遵循局部變量的使用方式,不能用offset偽操作符,只能用addr來(lái)完成。同樣,所有對局部變量使用的限制幾乎都可以適用于參數。
高級語(yǔ)法
以前高級語(yǔ)言和匯編的最大差別就是條件測試、分支和循環(huán)等高級語(yǔ)法。
高級語(yǔ)言中,程序員可以方便地用類(lèi)似于if,case,loop和while等語(yǔ)句來(lái)構成程序的結構流程,不僅條理清楚、一目了然,而且維護性相當好。而匯編程序員呢?只能在cmp指令后面絞盡腦汁地想究竟用幾十種跳轉語(yǔ)句中的哪一種,這里就能列出近三十個(gè)條件跳轉指令來(lái):ja,jae,jb,jeb,jc,je,jg,jge,jl,jle.jna,jnb,jnbe,jnc,jng,jnge,jnl,jno,jnp,jns,jnz,jo,jp,jpe,jpo以及jz等。雖然其中的很多指令我們一輩子也不會(huì )用到,但就是這些指令和一些loop,loopnz以及被loop涉及的ecx等寄存器糾纏在一起,使在匯編中書(shū)寫(xiě)結構清晰、可讀性好的代碼變得相當困難,這也是很多人視匯編為畏途的一個(gè)原因。
現在好了,MASM中新引入了一系列的偽指令,涉及條件測試、分支和循環(huán)語(yǔ)句,利用它們,匯編語(yǔ)言有了和高級語(yǔ)言一樣的結構,配合對局部變量和調用參數等高級語(yǔ)言中覺(jué)元素的支持,為使用Win32匯編編寫(xiě)大規模的應用程序奠定了基礎。
條件測試語(yǔ)句
在高級語(yǔ)言中,所有的分支和循環(huán)語(yǔ)句首先要涉及條件測試,也就是涉及一個(gè)表達式的結果是真還是假的問(wèn)題,表達式中往往有用來(lái)做比較和計算的操作符,MASM也不例外,這就是條件測試語(yǔ)句。
MASM條件測試的基本表達式是:
寄存器或變量 操作符 操作數
兩個(gè)以上的表達式可以用邏輯運算符連接:
(表達式1) 邏輯運算符 (表達式2) 邏輯運算符 (表達式3) …
允許的操作符和邏輯運算符如下所示:
條件溑或的操作符
操作符和邏輯運算符
操作
用途
==
等于
變量和操作數之間的比較
!=
不等于
變量和操作數之間的比較
>
大于
變量和操作數之間的比較
>=
大于等于
變量和操作數之間的比較
<
小于
變量和操作數之間的比較
<=
小于等于
變量和操作數之間的比較
&
位測試
將變量和操作數做與操作
!
邏輯取反
對變量取反或對表達式的結果取反
&&
邏輯與
對兩個(gè)表達式的結果進(jìn)行邏輯與操作
||
邏輯或
對兩個(gè)表達式的結果進(jìn)行邏輯或操作
舉例,左邊為表達式,右邊是表達式為真的條件:
x == 3 ;x等于3
eax != 3 ;eax不等于3
(y>=3) && ebx ;y大于等于3且ebx為非零值
(z&1) ||!eax ;z和1進(jìn)行“與”操作后非零或eax取反后非零
;也就是說(shuō)z的位0等于1或eax為零
細心的讀者一定會(huì )發(fā)現,MASM的條件測試采用的是和C語(yǔ)言相同的語(yǔ)法。如!和&是對變量的操作符(取反和與操作),||和&&是表達式結果之間的邏輯與和邏輯或,而==、!=、>、<等是比較符。同樣,對于不含比較符的單個(gè)變量或寄存器,MASM也是將所有非零認為是真,零值認為是假。
MASM的條件測試語(yǔ)句有幾個(gè)限制,首先是表達式的左邊只能是變量或寄存器,不能為常數;其次表達的兩邊不能同時(shí)為變量,但可以同時(shí)是寄存器。這些限制來(lái)自于80x86的指令,因為條件測試偽操作符只是簡(jiǎn)單地把每個(gè)表達式翻譯成cmp或test指令,80x86的指令集中沒(méi)有cmp 0,eax之類(lèi)的指令,同時(shí)也不允許直接操作兩個(gè)內存中的數,所以對這兩個(gè)限制是很好理解的。
除了這些和高級語(yǔ)言類(lèi)似的條件測試偽操作,匯編語(yǔ)言還有特殊的要求,就是程序中常常要根據系統標志寄存器中的各種標志位來(lái)做條件跳轉,這些在高級語(yǔ)言中是用不到的,所以又增加了以下一些標志位的狀態(tài)指示,它們本身相當于一個(gè)表達式:
CARRY? 表示Carry位是否置位
OVERFLOW? 表示Overflow位是否置位
PARITY? 表示Parity位是否置位
SIGN? 表示Sign位是否置位
ZERO? 表示Zero位是否置位
要測試eax等于ebx同時(shí)Zero位置位,條件表達式可以寫(xiě)為:
(eax == ebx) && ZERO?
要測試eax等ebx同時(shí)Zero位清零,條件表達式可以寫(xiě)為:
(eax == ebx) && !ZERO?
和C語(yǔ)言的條件測試同樣,MASM的條件測試偽指令并不會(huì )改變被測試的變量或寄存器的值,只是進(jìn)行測試而已,到最后它會(huì )被編譯器翻譯成類(lèi)似于cmp或test之類(lèi)的比較或位測試指令。
分支語(yǔ)句
分支語(yǔ)句用來(lái)根據條件表達式測試的真假執行不同的代碼模塊,MASM中的分支語(yǔ)句的語(yǔ)法如下:
.if 條件表達式1
表達式1為“真”時(shí)執行的指令
[.elseif 條件表達式2]
表達式2為“真”時(shí)執行的指令
[.elseif 條件表達式3]
表達式3為“真”時(shí)執行的指令
……
[.else]
所有表達式為“否”時(shí)執行的指令
.endif
注意:關(guān)鍵字if/elseif/else/endif的前面有個(gè)小數點(diǎn),如果不加小數點(diǎn),就變成宏匯編中的條件匯編偽操作了,結果可是天差地別。
這些偽指令把匯編程序的可讀性基本上提高到了高級語(yǔ)言的水平。
注意:使用.if/.else/.endif構成分支偽指令的時(shí)候,不要漏寫(xiě)前面的小數點(diǎn),if/else/endif是宏匯編中條件匯編宏操作的偽操作指令,作用是根據條件決定在最后的可執行文件中包不包括某一段代碼。這和.if/.else/.endif構成分支的偽指令完全是兩回事情。
循環(huán)語(yǔ)句
循環(huán)是重復執行的一組指令,MASM的循環(huán)偽指令可以根據條件表達式的真假來(lái)控制循環(huán)是否繼續,也可以在循環(huán)體中直接退出,使用循環(huán)的語(yǔ)法是:
.while 條件測試表達式
指令
[.break [.if 退出條件]]
[.continue]
.endw
或
.repeat
指令
[.break [.if 退出條件]]
[.continue]
.until 條件測試表達式 (或.untilcxz [條件測試表達式])
.while/.endw循環(huán)首先判斷條件測試表達式,如果結果是真,則執行循環(huán)體內的指令,結束后再回到.while處判斷表達式,如此往復,一直到表達式結果為假為止。.while/.endw指令有可能一遍也不會(huì )執行到循環(huán)體內的指令,因為如果第一次判斷表達式時(shí)就遇到結果為假的情況,那么就直接退出循環(huán)。
.repeat/.until循環(huán)首先執行一遍循環(huán)體內的指令,然后再判斷條件測試表達式,如果結果為真的話(huà),就退出循環(huán),如果為假,則返回.repeat處繼續循環(huán),可以看出,.repeat/.until不管表達式的值如何,至少會(huì )執行一遍循環(huán)體內的指令。
也可中以把條件表達式直接設置為固定值,這樣就可以構建一個(gè)無(wú)限循環(huán),對于.while/.end直接使用TRUE,對于.repeat/until直接使用FALSE來(lái)當表達式就是如此,這種情況下,可以使用.break偽指令強制退出循環(huán),如果.break偽指令后面跟一個(gè).if測試偽指令的話(huà),那么當退出條件為真時(shí)才執行.break偽指令。
在循環(huán)體中也可以用.continue偽指令忽略以后的指令,遇到.continue偽指令時(shí),不管下面還有沒(méi)有其他循環(huán)體中的指令,都會(huì )直接回到循環(huán)頭部開(kāi)始執行。
代碼風(fēng)格
隨著(zhù)程序功能的增加和版本的提高,程序越來(lái)越復雜,源文件也越來(lái)越多,風(fēng)格規范的源程序會(huì )對軟件的升級、修改和維護帶來(lái)極大的方便,要想開(kāi)發(fā)一個(gè)成熟的軟件產(chǎn)品,必須在編寫(xiě)源程序的時(shí)候就有條不紊,細致嚴謹。
在編程中,在程序排版、注釋、命名和可讀性等問(wèn)題上都有一定的規范,雖然編寫(xiě)可讀性良好的代碼并不是必然的要求,但好的代碼風(fēng)格實(shí)際上是為自己將來(lái)維護和使用這些代碼節省時(shí)間。
下面是對匯編語(yǔ)言代碼風(fēng)格的建議。
變量和函數的命名
匈牙利表示法
匈牙利表示法主要用在變量和子程序的命名,這是現在大部分程序員都在使用的命名約
定。匈牙利表示法這個(gè)奇怪的名字是為了紀念匈牙利籍的Microsoft 程序員Charles
Simonyi,他首先使用了這種命名方法。
匈牙利表示法用連在一起的幾個(gè)部分來(lái)命名一個(gè)變量,格式是類(lèi)型前綴加上變量說(shuō)明,類(lèi)型用小寫(xiě)字母表示,如用h表示句柄,用dw表示double word,用sz表示以0結尾的字符串等,說(shuō)明則用首字母大寫(xiě)的幾個(gè)英文單詞組成,如TimeCounter,NextPoint等,可以令人一眼看出變量的含義來(lái),在匯編語(yǔ)言中常用字的類(lèi)型前綴有:
b 表示byte
w 表示word
dw 表示dword
h 表示句柄
lp 表示指針
sz 表示以0結尾的字符串
lpsz 表示指向以0結尾的字符串的指針
f 表示浮點(diǎn)數
st 表示一個(gè)數據結構
這樣一來(lái),變量的意思就很好理解:
hWinMain 主窗口的句柄
dwTimeCount 時(shí)間計數器,以雙字定義
szWelcome 歡迎信息字符串,以0結尾
lpBuffer 指向緩沖區的指針
很明顯,這些變量名比count1,abc,commandlinebuffer和FILEFLAG之類(lèi)的命名要易于理解。由于匈牙利表示法既描述了變量的類(lèi)型,又描述了變量的作用,所以能幫助程序員及早發(fā)現變量的使用錯誤,如把一個(gè)數值當指針來(lái)使用引發(fā)的內存頁(yè)錯誤等。
對于函數名,由于不會(huì )返回多種類(lèi)型的數值,所以命名時(shí)一般不再用類(lèi)型開(kāi)頭,但名稱(chēng)還是用表示用途的單詞組成,每個(gè)單詞的首字母大寫(xiě)。Windows API是這種命名方式的絕好例子,當人們看到ShowWindow,GetWindowText,DeleteFile和GetCommandLine之類(lèi)的API函數名稱(chēng)時(shí),恐怕不用查手冊,就能知道它們是做什么用的。比起int 21h/09h和int 13h/02h之類(lèi)的中斷調用,好處是不必多講的。
對匈牙利表示法的補充
使用匈牙利表示法已經(jīng)基本上解決了命名的可讀性問(wèn)題,但相對于其他高級語(yǔ)言,匯編語(yǔ)言有語(yǔ)法上的特殊性,考慮下面這些匯編語(yǔ)言特有的問(wèn)題:
·對局部變量的地址引用要用lea指令或用addr偽操作,全局變量要用offset;對局部變量的使用要特別注意初始化問(wèn)題。如何在定義中區分全局變量、局部變量和參數?
·匯編的源代碼占用的行數比較多,代碼行數很容易膨脹,程序規模大了如何分清一個(gè)函數是系統的API還是本程序內部的子程序?
實(shí)際上上面的這些問(wèn)題可以歸納為區分作用域的問(wèn)題。為了分清變量的作用域,命名中對全局變量、局部變量和參數應該有所區別,所以我們需要對匈牙利表示法做一些補充,以適應Win32匯編的特殊情況,下面的補充方法僅供參考:
·全局變量的定義使用標準的匈牙利表示法,在參數的前面加下劃線(xiàn),在局部變量的前面加@符號,這樣引用的時(shí)候就能隨時(shí)注意到變量的作用域。
·在內部子程序的名稱(chēng)前面加下劃線(xiàn),以便和系統API區別。
如下面是一個(gè)求復數模的子程序,子程序名前面加下劃線(xiàn)表示這是本程序內部模塊,兩個(gè)參數——復數的實(shí)部和虛部用_dwX和_dwY表示,中間用到的局部變量@dwResult則用@號開(kāi)頭:
_Calc proc _dwX, _dwY
local @dwResult
finit
fild _dwX
fld st(0)
fmul ;i * i
fild _dwY
fld st(0)
fmul ; j * j
fadd ; i * I + j * j
fsqrt ;sqrt(i * i + j * j)
fistp @dwResult ;put result
mov eax,@dwResult
ret
_Calc endp
(說(shuō)實(shí)話(huà),上面這段Win32匯編子程序,我只能看懂20%??戳艘粋€(gè)月的匯編了,痛哉!痛哉?。?div style="height:15px;">
首先是大小寫(xiě)的問(wèn)題,匯編程序中對于指令和寄存器的書(shū)寫(xiě)是不分大小寫(xiě)的,但小寫(xiě)代碼比大寫(xiě)代碼便于閱讀,所以程序中的指令和寄存器等要采用小寫(xiě)字母,而用equ偽操作符定義的常量則使用大寫(xiě),變量和標號使用匈牙利表示法,大小寫(xiě)混合。
其次是使用Tab的問(wèn)題。匯編源程序中Tab的寬度一般設置為8個(gè)字符。在語(yǔ)法上,指令和操作數之間至少有一個(gè)空格就可以了,但指令的助記符長(cháng)度是不等長(cháng)的,用Tab隔開(kāi)指令和操作數可以使格式對齊,便于閱讀。如:
還有就是縮進(jìn)格式的問(wèn)題。程序中的各部分采用不同的縮進(jìn),一般變量和標號的定義不縮進(jìn),指令用兩個(gè)Tab縮進(jìn),遇到分支或循環(huán)偽指令再縮進(jìn)一格,如:
合適的縮進(jìn)格式可以明顯地表現出程序的流程結構,也很容易發(fā)現嵌套錯誤,當縮進(jìn)過(guò)多的時(shí)候,可以意識到嵌套過(guò)深,該改進(jìn)程序結構了。
由于匯編語(yǔ)言是以一條指令為一行的,實(shí)現一個(gè)小功能就需要好幾行,沒(méi)有分段的程序很難看出功能模塊來(lái),所以要合理利用空行來(lái)隔開(kāi)不同的功能塊,一般以在高級語(yǔ)言中可以用一句語(yǔ)句來(lái)完成的一段匯編指令為單位插入一個(gè)空行。
在MASM的宏功能中最好只使用條件匯編,用來(lái)選擇編譯不同的代碼塊來(lái)構建不同的版本,其他如宏定義和宏調用只會(huì )破壞程序的可讀性,能夠不用就盡量不用,雖然展開(kāi)后只有一兩句的宏定義不在此列,但既然展開(kāi)后也只有一兩句,那么和直接使用指令也就沒(méi)有什么區別了。
在匯編中避免使用宏定義的理由是:匯編中隨時(shí)要用到各個(gè)寄存器,宏定義不同于子程序,可以有選擇地保護現場(chǎng),在使用中很容易忽略里面用了哪個(gè)寄存器,從而對程序結構構成威脅。高級語(yǔ)言的宏定義則不會(huì )有這個(gè)問(wèn)題。
最極端的使用宏定義的程序是MicroMedia的Director SDK,100行左右的例子中幾乎有90%都是宏定義,雖然例子很容易改成其他功能的程序,但要在里面加新的功能則幾乎是不可能的,因為程序中連C語(yǔ)言函數開(kāi)始和結束的花括號都被改成了宏定義,這樣一來(lái),如果要真正使用這個(gè)開(kāi)發(fā)包,則必須把宏定義“翻譯”回原來(lái)的樣子才能真正理解程序的流程。
把僅在子程序中使用的變量設置為局部變量可以使子程序更容易封裝成一個(gè)黑匣子,如果無(wú)法把全部變量設置為局部變量,則盡量把這些數據改為參數輸入輸出,如果無(wú)法改為參數,那么意味著(zhù)這個(gè)子程序不能不經(jīng)修改地直接放到別的程序中使用。
在主程序中使用比較頻繁的部分,以及便于封裝成黑匣子在別的程序上用的代碼,都應該寫(xiě)上子程序,但一個(gè)子程序的規模不應該太大,行數盡量限制在幾百行之內,功能則限于完成單個(gè)功能。對于子程序,定義參數的時(shí)候要盡可能精簡(jiǎn),對可能引起程序崩潰的參數,如指針等,要進(jìn)行合法性檢測。
對于程序員來(lái)說(shuō),開(kāi)發(fā)每一個(gè)軟件都是要從頭做起是很浪費時(shí)間的,一般的做是從自己以前做的程序中拷貝相似的代碼,但修改還是要花一定時(shí)間,最好的辦法就是盡量把子程序做成一個(gè)黑匣子,可以不經(jīng)修改地直接拿過(guò)來(lái)用,這樣,每次編程相當于只是編寫(xiě)新增的部分,隨著(zhù)代碼的積累,開(kāi)發(fā)任何程序都將是很快的事情。