使用MASM
Win32匯編源程序的結(jié)構(gòu)
任何種類(lèi)的語(yǔ)言,總是有基本的源程序結(jié)構(gòu)規(guī)范。
下面以經(jīng)典的Hello World程序?yàn)槔?,展示一個(gè)C語(yǔ)言、DOS匯編、Win32匯編三種寫(xiě)法。同學(xué)位好好體會(huì)一下。
如果沒(méi)有匯編基礎(chǔ),建議看一下王爽老師的《匯編語(yǔ)言》這本書(shū)。
C語(yǔ)言中的HelloWorld程序:
#include <stdio.h>
main()
{
printf(“Hello, world\n”);
}
像這樣的一個(gè)程序,就說(shuō)明了C語(yǔ)言中最基本的格式,main()中的括號(hào)和下面的花括號(hào)說(shuō)明了一個(gè)函數(shù)的定義方法,printf語(yǔ)句說(shuō)明了一個(gè)函數(shù)的調(diào)用方法,調(diào)用函數(shù)語(yǔ)句后面的分號(hào)也是基本的格式。C是一種高級(jí)語(yǔ)言,在C源程序中,不必為堆棧段、數(shù)據(jù)段和代碼段的定義而擔(dān)心,編譯器會(huì)把程序中的字符串和語(yǔ)句代碼分別放到它們?cè)撊サ牡胤?,程序開(kāi)始執(zhí)行的時(shí)候也會(huì)自己找到main()函數(shù)。而匯編是低級(jí)語(yǔ)言,必須為所有的東西找到它們?cè)撊サ牡胤?,所以?/span>DOS的匯編中,Hello World又長(zhǎng)成了這樣一副模板:
;分號(hào)后面是注釋
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 堆棧段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
stack segment
db 100 dup (?) ;定義100個(gè)字節(jié)的內(nèi)存存儲(chǔ)單元空間,默認(rèn)值為?
stack ends
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 數(shù)據(jù)段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
data segment
szHello db ‘Hello, world’,0dh,0ah,’$’
;szHello為數(shù)據(jù)標(biāo)號(hào),它標(biāo)記了存儲(chǔ)數(shù)據(jù)的單元的地址和長(zhǎng)度。
;天哪,這太像高級(jí)語(yǔ)言中的變量了?。。?/span>
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字符串則跑到數(shù)據(jù)段中去了,代碼則放在代碼段中,程序的開(kāi)始語(yǔ)句必須由最后一句end start來(lái)說(shuō)明應(yīng)該從start這個(gè)標(biāo)號(hào)開(kāi)始執(zhí)行,整個(gè)程序在使用過(guò)DOS匯編的程序員眼里是非常的熟悉。(一個(gè)月前我不熟悉,現(xiàn)在我熟悉了。感謝王爽老師。)
到了Win32匯編的時(shí)候,程序的基本結(jié)構(gòu)還是如此,先來(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
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 數(shù)據(jù)段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.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應(yīng)該就是包含別的文件,.data想必是數(shù)據(jù)段,.code應(yīng)該就是代碼段了吧!接下來(lái)通過(guò)這個(gè)例子程序逐段介紹Win32匯編程序的結(jié)構(gòu)。
模式定義
程序的第一部分是模式和源程序格式的定義語(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的匯編中默認(rèn)使用的是8086指令集,那時(shí)候如果在源程序中寫(xiě)入80386所特有的指令或使用32位的寄存器就會(huì)報(bào)錯(cuò),為了在DOS環(huán)境下進(jìn)行保護(hù)模式編程或僅為了使用32位寄存器,常在DOS的匯編中使用.386來(lái)定義。Win32環(huán)境工作在80386及以上的處理器中,所以這一句.386是必不可少的。
后面帶p的偽指令則表示程序中可以使用特權(quán)指令,如:
mov cr0,eax
這一類(lèi)指令必須在特權(quán)級(jí)0上運(yùn)行,如果只指定.386,那么使用普通的指令是可以的,編譯時(shí)到這一句就會(huì)報(bào)錯(cuò),如果我們要寫(xiě)的程序是VxD等驅(qū)動(dòng)程序,中間要用到特權(quán)指令,那么必須定義.386p,在應(yīng)用程序級(jí)別的Win32編程中,程序都是運(yùn)行在優(yōu)先級(jí)3上,不會(huì)用到特權(quán)指令,只需定義.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 內(nèi)存模式 [,語(yǔ)言模式] [,其他模式]
內(nèi)存模式的定義影響最后生成的可執(zhí)行文件,可執(zhí)行文件的規(guī)模從小到大,可以有很多種類(lèi)型,在DOS的可執(zhí)行程序中,有只用到64KB的.com文件,也有大大小小的.exe文件。到了Win32環(huán)境下,又有了可以用4GB內(nèi)存的PE格式可執(zhí)行文件,編寫(xiě)不同類(lèi)型的可執(zhí)行文件要用.model語(yǔ)句定義不同的參數(shù),具體如下 表所示。
內(nèi)存模式
模式
|
內(nèi)存使用方式
|
tiny
|
用來(lái)建立.com文件,所有的代碼、數(shù)據(jù)和堆棧都在同一個(gè)64KB段內(nèi)
|
small
|
建立代碼和數(shù)據(jù)分別用一個(gè)64KB段的.exe文件
|
medium
|
代碼段可以有多個(gè)64KB段,數(shù)據(jù)段只有一個(gè)64KB段
|
compact
|
代碼段只有一個(gè)64KB,數(shù)據(jù)段可以有多個(gè)64KB段
|
large
|
代碼段和數(shù)據(jù)段都可以有多個(gè)64KB段
|
huge
|
同large,并且數(shù)據(jù)段中的一個(gè)數(shù)組也可以超過(guò)64KB
|
float
|
Win32程序使用的模式,代碼和數(shù)據(jù)使用同一個(gè)4GB段
|
Windows 程序運(yùn)行在保護(hù)模式下,系統(tǒng)把每一個(gè)Win32應(yīng)用程序都放到分開(kāi)的虛擬地址空間中去運(yùn)行,也就是說(shuō),每一個(gè)應(yīng)用程序都擁有其相互獨(dú)立的4GB地址空間,對(duì)Win32程序來(lái)說(shuō),只有一種內(nèi)存模式,即flat(平坦)模式,意思是內(nèi)存是很平坦地從0延伸到4GB,再?zèng)]有64KB段大小限制。對(duì)比一下DOS的Hello World和Win32的Hello World開(kāi)始部分的不同,DOS程序中有這樣語(yǔ)句:
mov ax,data
mov ds,ax
意思是把數(shù)據(jù)段寄存器DS指向data數(shù)據(jù)段,data數(shù)據(jù)段在前面已經(jīng)用data segment語(yǔ)句定義,只要DS不重新設(shè)置,那么從此以后指令中涉及的數(shù)據(jù)默認(rèn)將從data數(shù)據(jù)段中取得,所以下面的語(yǔ)句是從data數(shù)據(jù)段取出szHello字符串的地址后再顯示:
mov ah,9
mov dx,offset szHello
int 21h
縱觀Win32匯編的源程序,沒(méi)有一處可以找到ds或es等段寄存器的使用,因?yàn)樗械?/span>4GB空間用32位的寄存器全部都能訪問(wèn)到了,不必在頭腦中隨時(shí)記著當(dāng)前使用的是哪個(gè)數(shù)據(jù)段,這就是平坦內(nèi)存模式帶來(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寄存默認(rèn)不使用,這時(shí)若在源程序中使用FS或GS,在編譯時(shí)會(huì)報(bào)錯(cuò)。如果有必要使用它們,只需在使用前用下面的語(yǔ)句聲明一下就可以了:
assume fs:nothing,gs:nothing 或者 assume fs:flat,gs:flat
在Win32匯編中,.model語(yǔ)句中還應(yīng)該指定語(yǔ)言模式,即子程序和調(diào)用方式,例子中用的是stdcall,它指出了調(diào)用子程序或Win32 API時(shí)參數(shù)傳遞的次序和堆棧平衡的方法,相對(duì)于stdcall,不同的語(yǔ)言類(lèi)型還有C,SysCall,BASIC,FORTRAN和PASCALL,雖然各種高級(jí)語(yǔ)言在調(diào)用子程序時(shí)都是使用堆棧來(lái)傳遞參數(shù)。Windows的API調(diào)用使用是的stdcall格式,所以在Win32匯編中沒(méi)有選擇,必須在.model中加上stdcall參數(shù)。
3)option語(yǔ)句
option casemap:none
用option語(yǔ)句定義的選項(xiàng)有很多,如option language定義和option segment定義等,在Win32匯編程序中,需要的只是定義option casemap:none,這個(gè)語(yǔ)句定義了程序中的變量和子程序名是否對(duì)大小寫(xiě)每感,由于Win32 API中的API名稱(chēng)是區(qū)分大小寫(xiě)的,所以必須指定這個(gè)選項(xiàng),否則在調(diào)用API的時(shí)候會(huì)有問(wèn)題。
段的定義
段的概念
把上面的Win32的Hello World源程序中的語(yǔ)句歸納精簡(jiǎn)一下,再列在下面:
.386
.model flat,stdcall
option casemap:none
<一些include語(yǔ)句>
.data
<一些字符串、變量定義>
.code
<代碼>
<開(kāi)始標(biāo)號(hào)>
<其他語(yǔ)句>
end 開(kāi)始標(biāo)號(hào)
模式定義中的模式、選項(xiàng)等定義并不會(huì)在編譯好的可執(zhí)行程序中產(chǎn)生什么東西,它們只是說(shuō)明,而真正的數(shù)據(jù)和代碼是定義在各個(gè)段中的,如上面的.data段和.code段,考慮到不同的數(shù)據(jù)類(lèi)型,還可以有其他種類(lèi)的數(shù)據(jù)段,下面是包含全部段的源程序結(jié)構(gòu):
.386
.model flat,stdcall
option casemap:none
<一些include語(yǔ)句>
.stack [堆棧段的大小]
.data
<一些初始化過(guò)的變量定義>
.data?
<一些沒(méi)有初始化過(guò)的變量定義>
.const
<一些常量定義>
.code
<代碼>
<開(kāi)始標(biāo)號(hào)>
<其他語(yǔ)句>
end 開(kāi)始標(biāo)號(hào)
.stack、.data、.data?、.const和.code是分段偽指令,Win32中實(shí)際上只有代碼和數(shù)據(jù)之分,.data,.data?和.const是數(shù)據(jù)段,.code是代碼段,和DOS匯編不同,Win32匯編不必考慮堆棧,系統(tǒng)會(huì)為程序分配一個(gè)向下擴(kuò)展的、足夠大的段作為堆棧段,所以.stack段定義常常被忽略。
注意,前面不是說(shuō)過(guò)Win32環(huán)境下不用段了嗎?是的,這些“段”,實(shí)際上并不是DOS匯編中那種意義的段,而是內(nèi)存的“分段”。上一個(gè)段的結(jié)束就是下一個(gè)段的開(kāi)始,所有的分段,合起來(lái),包括系統(tǒng)使用的地址空間,就組成了整個(gè)可以尋址的4GB空間。Win32匯編的內(nèi)存管理使用了80386處理器的分頁(yè)機(jī)制,每個(gè)頁(yè)(4KB大?。┛梢宰杂芍付▽傩裕陨弦粋€(gè)4KB可能是代碼,屬性是可執(zhí)行但不可寫(xiě),下一個(gè)4KB就有可能是既可讀也可寫(xiě)但不可執(zhí)行的數(shù)據(jù),再下面呢?有可能是可讀不可寫(xiě)也不可執(zhí)行的數(shù)據(jù)。Win32匯編源程序中“分段”的概念實(shí)際上是把不同類(lèi)型的數(shù)據(jù)或代碼歸類(lèi),再放到不同屬性的內(nèi)存頁(yè)(也就是不同的“分段”)中,這中間不涉及使用不同的段選擇器。雖然使用和DOS匯編同樣的.code和.data語(yǔ)句來(lái)定義,意思可是完全不同了!
數(shù)據(jù)段
.data、.data?和.const定義的是數(shù)據(jù)段,分別對(duì)應(yīng)不同方式的數(shù)據(jù)定義,在最后生成的可執(zhí)行文件中也分別放在不同的節(jié)區(qū)(Section)中。程序中的數(shù)據(jù)定義一段可以歸納為3類(lèi):
1)第一類(lèi)是可讀可寫(xiě)的已定義變量。這些數(shù)據(jù)在源程序中已經(jīng)被定義了初始值,而且在程序的執(zhí)行中有可能被更改,如一些標(biāo)志等,這些數(shù)據(jù)必須定義在.data段中,.data段是已初始化數(shù)據(jù)段,其中定義的數(shù)據(jù)是可讀可寫(xiě)的,在程序裝入完成的時(shí)候,這些值就已經(jīng)在內(nèi)存中了,.data段存放在可執(zhí)行文件的_DATA節(jié)區(qū)內(nèi)。
2)第二類(lèi)是可讀可寫(xiě)的未定義變量。這些變量一般是當(dāng)做緩沖區(qū)或者在程序執(zhí)行后才開(kāi)始使用的,這些數(shù)據(jù)可以定義在.data段中,也可以定義在.data?段中,但一般把它放到.data?段中。雖然定義在這兩種段中都可以正常使用,但定義在.data?段中不會(huì)增大.exe文件的大小。舉例說(shuō)明,如果要用到一個(gè)100KB的緩沖區(qū),可以在數(shù)據(jù)段中定義:
szBuffer db 100 * 1024 dup (?)
如果放在.data段中,編譯器認(rèn)為這些數(shù)據(jù)在程序裝入時(shí)就必須有效,所以它在生成可執(zhí)行文件的時(shí)候保留了所有的100KB的內(nèi)容,即使它們是全零!如果程序其他部分的大小是50KB,那么最后的.exe文件就會(huì)是150KB大小,如果緩沖區(qū)定義為1MB,那么.exe文件會(huì)增大到1050KB。.data?段則不同,其中的內(nèi)容編譯器會(huì)認(rèn)為程序在開(kāi)始執(zhí)行后才會(huì)用到,所以在生成可執(zhí)行文件的時(shí)候只保留了大小信息,不會(huì)為它浪費(fèi)磁盤(pán)空間。和上面同樣的情況下,即使緩沖區(qū)定義為1MB,可執(zhí)行文件同樣只有50KB!總之,.data?段是未初始化數(shù)據(jù)段,其中的數(shù)據(jù)也是可讀可寫(xiě)的,但在可執(zhí)行文件中不占空間,.data?段在可執(zhí)行文件中存放在_BSS節(jié)區(qū)中。
3)第三類(lèi)數(shù)據(jù)是一些常量。如一些要顯示的字符串信息,它們?cè)诔绦蜓b入的時(shí)候也已經(jīng)有效,但在整個(gè)執(zhí)行過(guò)程中不需要修改,這些數(shù)據(jù)可以放在.const段中,.const段是常量段,它是可讀不可寫(xiě)的。一般為了方便起見(jiàn),在小程序中常常把常量一起定義到.data段中,而不另外定義一個(gè).const段。在程序中如果不小心寫(xiě)了對(duì).const段中的數(shù)據(jù)做寫(xiě)操作的指令,會(huì)引起保護(hù)錯(cuò)誤,Windows會(huì)顯示一個(gè)提示框并結(jié)束程序。
Hello.exe – 應(yīng)用程序錯(cuò)誤
“0x00401000”指令引用 的”0x00402010”內(nèi)存。該內(nèi)存不能為”written”。
要終止程序,請(qǐng)單擊”確定”。
要調(diào)試程序,請(qǐng)單擊”取消”。
如果不怕程序可讀性不佳的話,把.const段中定義的東西混到.code段中去也可以正常使用,因?yàn)?/span>.code段也是可以讀的。
代碼段
.code段是代碼段,所有的指令都必須寫(xiě)在代碼段中,在可執(zhí)行文件中,代碼段是放在_TEXT節(jié)區(qū)中的。Win32環(huán)境中的數(shù)據(jù)段是不可執(zhí)行的,只有代碼段有可執(zhí)行的屬性。對(duì)于工作在特權(quán)級(jí)3的應(yīng)用程序來(lái)說(shuō),.code段是不可寫(xiě)的,在編寫(xiě)DOS匯編程序的時(shí)候,好事的程序員往往有個(gè)習(xí)慣,就是靠改動(dòng)代碼段中的代碼來(lái)做一些反跟蹤的事情,如果企圖在Win32匯編下做同樣的事情,結(jié)果就是和上面同樣 “非法操作”。
當(dāng)然事物總有兩面性,在Windows95下,在特權(quán)級(jí)0下運(yùn)行的程序?qū)λ械亩味加凶x寫(xiě)的權(quán)利,包括代碼段。另外,在優(yōu)先級(jí)3下運(yùn)行的程序也不是一定不能寫(xiě)代碼段,代碼段的屬性是由可執(zhí)行文件PE頭部中的屬性位決定的,通過(guò)編輯磁盤(pán)上的.exe文件,把代碼段屬性位改成可寫(xiě),那么在程序中就允許修改自己的代碼段。一個(gè)典型的應(yīng)用就是一些針對(duì)可執(zhí)行文件的壓縮軟件和加殼軟件,如Upx和PeCompact等,這些軟件靠把代碼段進(jìn)行變換來(lái)達(dá)到解壓縮和解密的目的,被處理過(guò)的可執(zhí)行文件在執(zhí)行時(shí)需要由解壓代碼來(lái)將代碼段解壓縮,這就需要寫(xiě)代碼段,所以這些軟件對(duì)可執(zhí)行文件代碼段的屬性預(yù)先做修改。
程序結(jié)束和程序入口
在C語(yǔ)言源程序中,程序不必顯式地指定程序由哪里開(kāi)始執(zhí)行,編譯器已經(jīng)約定好從main()函數(shù)開(kāi)始執(zhí)行了。而在匯編程序中,并沒(méi)有一個(gè)main函數(shù),程序員可以指定從代碼段的任何一個(gè)地方開(kāi)始執(zhí)行,這個(gè)地方由程序最后一句的end語(yǔ)句來(lái)指定:
end [開(kāi)始地址]
這句語(yǔ)句同時(shí)表示源程序結(jié)束,所有的代碼必須在end語(yǔ)句之前。
end start
上述語(yǔ)句指定程序從start這個(gè)標(biāo)號(hào)開(kāi)始執(zhí)行。當(dāng)然,start標(biāo)號(hào)必須在程序的代碼段中有所定義。
但是,一個(gè)源程序不必非要指定入口標(biāo)號(hào),這時(shí)候可以把開(kāi)始地址忽略不寫(xiě),這種情況發(fā)生在編寫(xiě)多模塊程序的單個(gè)模塊的時(shí)候。當(dāng)分開(kāi)寫(xiě)多個(gè)程序模塊時(shí),每個(gè)模塊的源程序中也可以包括.data、.data?、.const和.code段,結(jié)構(gòu)就和上面的Win32 Hello World一樣,只是其他模塊最后的end語(yǔ)句必須不帶開(kāi)始地址。當(dāng)最后把多個(gè)模塊鏈接在一起的時(shí)候,只能有一個(gè)主模塊指定入口地址,在多個(gè)模塊中指定入口地址或者沒(méi)有一個(gè)模塊指定了入口地址,鏈接程序都會(huì)報(bào)錯(cuò)。
注釋和換行
注釋是源程序中不可忽略的一部分,匯編源程序的注釋以分號(hào)(;)開(kāi)始,注釋既可以在一行的頭部,也可以在一行的中間,一行中所有在分號(hào)之后的字符全部當(dāng)做注釋處理,但在字符串的字義中包含的引號(hào)內(nèi)的分號(hào)不當(dāng)做是注釋的開(kāi)始。
;這里是注釋
call _PrintChar ;這里是注釋
szChar db ‘Hello, world; ’,0dh,0ah ;world后面的分號(hào)不是注釋?zhuān)竺娴牟攀?/span>
當(dāng)源程序的某一行過(guò)長(zhǎng),不利于閱讀的時(shí)候,可以分行書(shū)寫(xiě),分行的辦法是在一行的最后用反斜杠(\)做換行符,如:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
可以寫(xiě)為:
invoke MessageBox, \
NULL, \ ;父窗口句柄
offset szText, \ ;消息框中的文字
offset szCaption, \ ;標(biāo)題文字
MB_OK
一行的最后,指的是最后一個(gè)有用的字符,反斜杠后面多幾個(gè)空格或加上注釋并不影響換行符的使用,如上例所示,這一點(diǎn)和makefile文件中換行符的規(guī)定有所不同。
調(diào)用API
API是什么?
Win32程序是構(gòu)筑在Win32 API基礎(chǔ)上的。在Win32 API中,包括了大量的函數(shù)、結(jié)構(gòu)和消息等,它不僅為應(yīng)用程序所調(diào)用,也是Windows自身的一部分,Windows自身的運(yùn)行也調(diào)用這些API函數(shù)。
在DOS下,操作系統(tǒng)的功能是通過(guò)各種軟中斷來(lái)實(shí)現(xiàn)的,如大家都知道int 21h是DOS中斷,int 13h和int 10h是BIOS中的磁盤(pán)中斷和視頻中斷。當(dāng)應(yīng)用程序要引用系統(tǒng)功能時(shí),要把相應(yīng)的參數(shù)放在各個(gè)寄存器中再調(diào)用相應(yīng)的中斷,程序控制權(quán)轉(zhuǎn)到中斷中去執(zhí)行,完成以后會(huì)通過(guò)iret中斷返回指令回到應(yīng)用程序中。如DOS匯編下的Hello World程序中有下列語(yǔ)句:
mov ah,9
mov dx,offset szHello
int 21h
這3條語(yǔ)句調(diào)用DOS系統(tǒng)模塊中的屏幕顯示功能,功能號(hào)放在ah中,9號(hào)功能表示屏幕顯示,要輸出到屏幕上的內(nèi)容的地址放在dx中,然后去調(diào)用int 21h,字符串就會(huì)顯示到屏幕上。
這個(gè)例子說(shuō)明了應(yīng)用程序調(diào)用系統(tǒng)功能的一般過(guò)程。首先,系統(tǒng)提供功能模塊并約定參數(shù)的定義方法,同時(shí)約定調(diào)用的方式,同時(shí)約定調(diào)用的方式,應(yīng)用程序按照這個(gè)約定來(lái)調(diào)用系統(tǒng)功能。在這里,ah中放功能號(hào)9,dx中放字符串地址就是約定的參數(shù),int 21h是約定的調(diào)用方式。
下面來(lái)看看這種方法的不便這處。首先,所有的功能號(hào)定義是冷冰冰的數(shù)字,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號(hào)功能看使用方法:
Print string (Func 09)
AH = 09h
DS:DX -> string terminated by “$”
這就是DOS時(shí)代匯編程序員都有一厚本《中斷大全》的原因,因?yàn)樗械墓δ芫幪?hào)包括使用的參數(shù)定義僅從字面上看,是看不出一點(diǎn)頭緒來(lái)的。
另外,80x86系列處理器能處理的中斷最多只能有256個(gè),不同的系統(tǒng)服務(wù)程序使用了不同的中斷號(hào),這少得可憐的中斷數(shù)量就顯得太少了,結(jié)果到最后是中斷掛中斷,大家搶來(lái)?yè)屓サ?,把好好的一個(gè)系統(tǒng)搞得像接力賽跑一樣。
對(duì)于這些弱點(diǎn),程序員們都有個(gè)愿望:系統(tǒng)功能如果能以功能名作為子程序名直接調(diào)用就好了,參數(shù)也最好定義的有意義一點(diǎn),這樣一來(lái)寫(xiě)程序就會(huì)方便得多,編系統(tǒng)擴(kuò)展模塊也就不必老是擔(dān)心往哪個(gè)中斷上面掛了,最好能把上面int 21h/ah=9的調(diào)用寫(xiě)成下面這副樣子:
call PrintString, addr szHello
終于,好消息出來(lái)了,Win32環(huán)境中的編程接口就是這個(gè)樣子,這就是API,它實(shí)際上是以一種新的方法代替了DOS中用軟中斷的方式。和DOS的結(jié)構(gòu)相比,Win32的系統(tǒng)功能模塊放在Windows的動(dòng)態(tài)鏈接庫(kù)(DLL)中,DLL是一種Windows的可執(zhí)行文件,采用的是和.exe文件同樣的PE格式,在PE格式文件頭的導(dǎo)出表中,以字符串形式指出了這個(gè)DLL能提供的函數(shù)列表。應(yīng)用程序使用字符串類(lèi)型的函數(shù)名指定要調(diào)用的函數(shù)。
應(yīng)用程序在使用的時(shí)候由Windows自動(dòng)載入DLL程序并調(diào)用相應(yīng)的函數(shù)。
實(shí)際上,Win32的基礎(chǔ)就是由DLL組成的。Win32 API的核心由3個(gè)DLL提供,它們是:
KERNEL32.DLL——系統(tǒng)服務(wù)功能。包括內(nèi)存管理、任務(wù)管理和動(dòng)態(tài)鏈接等。
GDI32.DLL——圖形設(shè)備接口。利用VGA與DRV之類(lèi)的顯示設(shè)備驅(qū)動(dòng)程序完成顯示文本和矩形等功能。
USER32.DLL——用戶接口服務(wù)。建立窗口和傳送消息等。
當(dāng)然,Win32 API還包括其他很多函數(shù),這些也是由DLL提供的,不同的DLL提供了不同的系統(tǒng)功能。如使用TCP/IP協(xié)議進(jìn)行網(wǎng)絡(luò)通信的DLL是Wsock32.dll,它所提供的API稱(chēng)為Socket API;專(zhuān)用于電話服務(wù)方面的API稱(chēng)為TAPI(Telephony API),包含在Tapi32.dll中,所有的這些DLL提供的函數(shù)組成了現(xiàn)在使用的Win32編程環(huán)境。
調(diào)用API
和在DOS中用中斷方式調(diào)用系統(tǒng)功能一樣,用API方式調(diào)用存放在DLL中的函數(shù)必須同樣約定一個(gè)規(guī)范,用來(lái)定義函數(shù)的調(diào)用方法、參數(shù)的傳遞方法和參數(shù)的定義,洋洋灑灑幾百MB的Windows系統(tǒng)比起才幾百KB規(guī)模的DOS,其系統(tǒng)函數(shù)的規(guī)模和復(fù)雜程度都上了一個(gè)數(shù)量級(jí),所在使用一個(gè)API時(shí),帶的參數(shù)數(shù)量多達(dá)十幾個(gè)是常有的事,在DOS下用寄存來(lái)傳遞參數(shù)的方法顯然已經(jīng)不能勝任了。
Win32 API是用堆棧來(lái)傳遞參數(shù)的,調(diào)用者把參數(shù)一個(gè)個(gè)壓入堆棧,DLL中的函數(shù)程序再?gòu)亩褩V腥〕鰠?shù)處理,并在返回之前將堆棧中已經(jīng)無(wú)用的參數(shù)丟棄。在Microsoft發(fā)布的《Microsoft Win32 Programmer’s Reference》中定義了常用API的參數(shù)和函數(shù)聲明,先來(lái)看消息框函數(shù)的聲明:
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。
上述函數(shù)聲明說(shuō)明了MessageBox有4個(gè)參數(shù),它們分別是HWND類(lèi)型的窗口句柄(hWnd),LPCTSTR類(lèi)型的要顯示的字符串地址(lpText)和標(biāo)題字符串地址(lpCaption),還有UINT類(lèi)型的消息框類(lèi)型(uType)。這些數(shù)據(jù)類(lèi)型看起來(lái)很復(fù)雜,但有一點(diǎn)是很重要的,對(duì)于匯編語(yǔ)言來(lái)說(shuō),Win32環(huán)境中的參數(shù)實(shí)際上只有一種類(lèi)型,那就是一個(gè)32位的整數(shù),所以這些HWND,LPCTSTR和UINT實(shí)際上就是匯編中的dword(double word,雙字型,4個(gè)字節(jié),兩個(gè)字,32位),之所以定義為不同的模樣,是用來(lái)說(shuō)明了用途。由于Windows是用C寫(xiě)成的,世界上的程序員好像也是用C語(yǔ)言的最多,所以Windows所有編程資料發(fā)布的格式也是C格式。
上面的聲明用匯編的格式來(lái)表達(dá)就是:
MessageBox Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword
上面最后一句Library:Use User32.lib則說(shuō)明了這個(gè)函數(shù)包括在User32.dll中。
有了函數(shù)原型的定義后,就是調(diào)用的問(wèn)題了,Win32 API調(diào)用中要把參數(shù)放入堆棧,順序是最后一個(gè)參數(shù)最先進(jìn)棧,在匯編中調(diào)用MessageBox函數(shù)的方法是:
push uType
push lpCaption
push lpText
push hWnd
call MessageBox
在源程序編譯鏈接成可執(zhí)行文件后,call MessageBox語(yǔ)句中的MessageBox會(huì)被換成一個(gè)地址,指向可執(zhí)行文件中的導(dǎo)入表,導(dǎo)入表中指向MessageBox函數(shù)的實(shí)際地址會(huì)在程序裝入內(nèi)存的時(shí)候,根據(jù)User32.dll在內(nèi)存中的位置由Windows系統(tǒng)動(dòng)態(tài)填入。
使用invoke語(yǔ)句
API是可以調(diào)用了,另一個(gè)煩人的問(wèn)題又出現(xiàn)了,Win32的API動(dòng)輒就是十幾個(gè)參數(shù),整個(gè)源程序一眼看上去基本上都是把參數(shù)壓堆棧的push指令,參數(shù)的個(gè)數(shù)和順序很容易搞錯(cuò),由此引起的莫名其妙的錯(cuò)誤源源不斷,源程序的可讀性看上去也很差。如果寫(xiě)的時(shí)候少寫(xiě)了一句push指令,程序在編譯和鏈接的時(shí)候都不會(huì)報(bào)錯(cuò),但在執(zhí)行的時(shí)候必定會(huì)崩潰,原因是堆棧對(duì)不齊了。
有不有解決的辦法呢?最好是像C語(yǔ)言一樣,能在同一句中打入所有的參數(shù),并在參數(shù)使用錯(cuò)誤的時(shí)候能夠提示。
好消息又來(lái)了,Microsoft終于做了一件好事,在MASM中提供了一個(gè)偽指令實(shí)現(xiàn)了這個(gè)功能,那就是invoke偽指令,它的格式是:
invoke 函數(shù)名 [,參數(shù)1][,參數(shù)2]…[,參數(shù)n]
對(duì)MessageBox的調(diào)用在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ù)數(shù)量的檢查工作,如果帶的參數(shù)數(shù)量和聲明時(shí)的數(shù)量不符,編譯器報(bào)錯(cuò):
error A2137: too few arguments to INVOKE
編譯時(shí)看到這樣的錯(cuò)誤報(bào)告,首先要檢查的是有沒(méi)有少寫(xiě)一個(gè)參數(shù)。對(duì)于不帶參數(shù)的API調(diào)用,invoke偽指令的參數(shù)檢查功能可有可無(wú),所以既可以用call API_Name這樣的語(yǔ)法,也可以用invoke API_Name這樣的語(yǔ)法。
API函數(shù)的返回值
有的API函數(shù)有返回值,如MessageBox定義的返回值是int類(lèi)型的數(shù),返回值的類(lèi)型對(duì)匯編程序來(lái)說(shuō)也只有dword一種類(lèi)型,它永遠(yuǎn)放在eax中。如果要返回的內(nèi)容不是一個(gè)eax所能容納的,Win32 API采用的方法一般是返回一個(gè)指針,或者在調(diào)用參數(shù)中提供一個(gè)緩沖區(qū)地址,干脆把數(shù)據(jù)直接返回到緩沖區(qū)中去。
函數(shù)的聲明
在調(diào)用API函數(shù)的時(shí)候,函數(shù)原型也必須預(yù)先聲明,否則,編譯器會(huì)不認(rèn)這個(gè)函數(shù)。invoke偽指令也無(wú)法檢查參數(shù)個(gè)數(shù)。聲明函數(shù)的格式是:
函數(shù)名 proto [距離] [語(yǔ)言] [參數(shù)1]:數(shù)據(jù)類(lèi)型, [參數(shù)2]:數(shù)據(jù)類(lèi)型,
句中的proto是函數(shù)聲明的偽指令,距離可以是NEAR,FAR,NEAR16,NEAR32,FAR16或FAR32,Win32中只有一個(gè)平坦的段,無(wú)所謂距離,所以在定義時(shí)是忽略的;語(yǔ)言類(lèi)型就是.model那些類(lèi)型,如果忽略,則使用.model定義的默認(rèn)值。
后面就是參數(shù)的列表了,對(duì)Win32匯編來(lái)說(shuō)只存在dword類(lèi)型的參數(shù),所以所有參數(shù)的數(shù)據(jù)類(lèi)型永遠(yuǎn)是dword,另外對(duì)于編譯器來(lái)說(shuō),它只關(guān)心參數(shù)的數(shù)量,參數(shù)的名稱(chēng)在這里是無(wú)用的,僅是為了可讀性而設(shè)置的,可以省略掉,所以下面兩句消息框函數(shù)的定義實(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),分別對(duì)應(yīng)兩個(gè)字符集:一類(lèi)是處理ANSI字符集的,另一類(lèi)是處理Unicode字符集的。前一類(lèi)函數(shù)名字的尾部帶一個(gè)A字符,處理Unicode的則帶一個(gè)W字符。
我們比較熟悉的ANSI字符串是以NULL結(jié)尾的一串字符數(shù)組,每一個(gè)ANSI字符占一個(gè)字節(jié)寬。對(duì)于歐洲語(yǔ)言體系,ANSI字符集已足夠了,但對(duì)于有成千上萬(wàn)個(gè)不同字符的幾種東方語(yǔ)言體系來(lái)說(shuō),Unicode字符集更有用。每一個(gè)Unicode字符占兩個(gè)字節(jié)的寬度,這樣一來(lái)就可以在一個(gè)字符串中使用65536個(gè)不同的字符了。
MessageBox和顯示字符串有關(guān),同樣它有兩個(gè)版本,嚴(yán)格地說(shuō),系統(tǒng)中有兩個(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。在源程序中繼續(xù)沿用MessageBox是為了程序的可讀性以及保持和手冊(cè)的一致性,但對(duì)于編譯器來(lái)說(shuō),實(shí)際是在使用MessageBoxA。
由于并不是每個(gè)Win32系統(tǒng)都支持W系統(tǒng)的API,在Windows 9x系列中,對(duì)Unicode是不支持的,很多的API只有ANSI版本,只有Windows NT系列才對(duì)Unicode完全支持。為了編寫(xiě)在幾個(gè)平臺(tái)中通用的程序,一般應(yīng)用程序都使用ANSI版本的API函數(shù)集。
為了使程序更有移植性,在源程序中一般不直接指明使用Unicode還是ANSI版本,而是使用宏匯編中的條件匯編功能來(lái)統(tǒng)一替換,如在源程序中使用MessageBox,但在頭文件中定義:
if UNICODE
MessageBox equ <MessageBoxW>
else
MessageBox equ <MessageBoxA>
endif
所有涉及版本問(wèn)題的API都可以按此方法定義,然后在源程序的頭指定UNICODE=1或UNICODE=0,重新編譯后就能產(chǎn)生不同的版本。
include語(yǔ)句
對(duì)于所有要用到的API函數(shù),在程序的開(kāi)始部分都必須預(yù)先聲明,但這一個(gè)步驟顯然是比較麻煩的,為了簡(jiǎn)化操作,可以采用各種語(yǔ)言通用的解決辦法,就是把所有的聲明預(yù)先放在一個(gè)文件中,在用到的時(shí)候再用include語(yǔ)句包含進(jìn)來(lái)。現(xiàn)在回到Win32 Hello World程序,這個(gè)程序用到了兩個(gè)API函數(shù):MessageBox和ExitProcess,它們分別在User32.dll和Kernel32.dll中,在MASM32工具包中已經(jīng)包括了所有DLL的API函數(shù)聲明列表,每個(gè)DLL對(duì)應(yīng)<DLL名.inc>文件,在源程序中只要使用include語(yǔ)句包含進(jìn)來(lái)就可以了:
include user32.inc
include kernel32.inc
當(dāng)用到其他的API函數(shù)時(shí),只需相應(yīng)增加對(duì)應(yīng)的include語(yǔ)句。
include語(yǔ)句還用來(lái)在源程序中包含別的文件,當(dāng)多個(gè)源程序用到相同的函數(shù)定義、常量定義、甚至源代碼時(shí),可以把相同的部分寫(xiě)成一個(gè)文件,然后在不同的源程序中用include語(yǔ)句包含進(jìn)來(lái)。
編譯器對(duì)include語(yǔ)句的處理僅是簡(jiǎn)單地把這一行用指定的文件內(nèi)容替換掉而而已。
include語(yǔ)句的語(yǔ)法是:
include 文件名
或 include <文件名>
當(dāng)遇到要包括的文件名和MASM的關(guān)鍵字同名等可能會(huì)引起編譯器混淆的情況時(shí),可以用<>將文件名括起來(lái)。
includelib語(yǔ)句
在DOS匯編中,使用中斷調(diào)用系統(tǒng)功能是不必聲明的,處理器自己知道到中斷向量表中去取中斷地址。在Win32匯編中使用API函數(shù),程序必須知道調(diào)用的API函數(shù)存在于哪個(gè)DLL中,否則,操作系統(tǒng)必須搜索系統(tǒng)中存在的所有DLL,并且無(wú)法處理不同DLL中的同名函數(shù),這顯然是不現(xiàn)實(shí)的,所以,必須有個(gè)文件包括DLL庫(kù)正確的定位信息,這個(gè)任務(wù)是由導(dǎo)入庫(kù)來(lái)實(shí)現(xiàn)的。
在使用外部函數(shù)的時(shí)候,DOS下有函數(shù)庫(kù)的概念,那時(shí)的函數(shù)庫(kù)實(shí)際上是靜態(tài)庫(kù),靜態(tài)庫(kù)是一組已經(jīng)編寫(xiě)好的代碼模塊,在程序中可以自由引用,在源程序編譯成目標(biāo)文件,最后要鏈接可執(zhí)行文件的時(shí)候,由link程序從庫(kù)中找出相應(yīng)的函數(shù)代碼,一起鏈接到最后的可執(zhí)行文件中。DOS下C語(yǔ)言的函數(shù)庫(kù)就是典型的靜態(tài)庫(kù)。庫(kù)的出現(xiàn)為程序員節(jié)省了大量的開(kāi)發(fā)時(shí)間,缺點(diǎn)就是每個(gè)可執(zhí)行文件中都包括了要用到的相同函數(shù)的代碼,占用了大量的磁盤(pán)空間,在執(zhí)行的時(shí)候,這些代碼同樣重復(fù)占用了寶貴的內(nèi)存。
Win32環(huán)境中,程序鏈接的時(shí)候仍然要使用函數(shù)庫(kù)來(lái)定位函數(shù)信息,只不過(guò)由于函數(shù)代碼放在DLL文件中,庫(kù)文件中只留有函數(shù)的定位信息和參數(shù)數(shù)目等簡(jiǎn)單信息,這種庫(kù)文件叫做導(dǎo)入庫(kù),一個(gè)DLL文件對(duì)應(yīng)一個(gè)導(dǎo)入庫(kù),如User32.dll文件用于編程的導(dǎo)入庫(kù)是User32.lib,MASM32工具包中包含了所有DLL的導(dǎo)入庫(kù)。
為了告訴鏈接程序使用哪個(gè)導(dǎo)入庫(kù),使用的語(yǔ)句是:
includelib 庫(kù)文件名
或 includelib <庫(kù)文件名>
和include的用法一樣,在要包括讓編譯器混淆的文件名時(shí)加括號(hào)。
Win32 Hello World程序用到的兩個(gè)API函數(shù)MessageBox和ExitProcess分別在User32.dll和Kernel32.dll中,那么在源程序使用的相應(yīng)語(yǔ)句為:
includelib user32.lib
includelib kernel32.lib
和include語(yǔ)句的處理不同,includelib不會(huì)把.lib文件插入到源程序中,它只是告訴鏈接器在鏈接的時(shí)候到指定的庫(kù)文件中去找而已。
API參數(shù)中的等值定義
再回過(guò)頭來(lái)看顯示消息框的語(yǔ)句:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
在uType這個(gè)參數(shù)中使用了MB_OK,這個(gè)MB_OK是什么意思?
在《Microsoft Win32 Programmer’s Reference》中的說(shuō)明:
uType——定義對(duì)話框的類(lèi)型,這個(gè)參數(shù)可以是以下標(biāo)志的合集:
要定義消息框上顯示按鈕,用下面的某一個(gè)標(biāo)志:
MB_ABORTRETRYIGNORE——消息框有三個(gè)按鈕:終止,重試和忽略
MB_HELP——消息框上顯示一個(gè)幫助按鈕,按下后發(fā)送WM_HELP消息
MB_OK——消息框上顯示一個(gè)確定按鈕,這是默認(rèn)值
……
要在消息框中顯示圖標(biāo),用下面的某一個(gè)標(biāo)志:
MB_ICONWARNING——顯示驚嘆號(hào)圖標(biāo)
MB_ICONINFORMATION——顯示消息圖標(biāo)
……
這些是uType參數(shù)說(shuō)明中的一小半,可以看出,參數(shù)可以用的值有很多種。
MB_ICONWARING和MB_YESNO等參數(shù)究竟是什么意思呢?
在Visual C++的目錄下中,可以找到頭文件WinUser.h,里面定義了如下一段內(nèi)容:
/* 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,默認(rèn)的MB_OK就是0,Win32 API的參數(shù)使用這樣的定義方法顯然是為了免除程序員死記數(shù)值定義的麻煩。在編寫(xiě)Win32匯編程序的時(shí)候,MASM32工具包中的Windows.inc也包括了所有這些參數(shù)的定義,只要在程序的開(kāi)頭包含這個(gè)定義文件:
include windows.inc
就可以方便地完全按照API手冊(cè)來(lái)使用Win32函數(shù)。
打開(kāi)\masm32\include 目錄下的Windows.inc查看一下,可以發(fā)現(xiàn)整個(gè)文件總共有兩萬(wàn)六千多行,包括了幾乎所有的Win32 API參數(shù)中的常量和數(shù)據(jù)結(jié)構(gòu)定義。
標(biāo)號(hào)、變量和數(shù)據(jù)結(jié)構(gòu)
當(dāng)程序中要跳轉(zhuǎn)到另一位置時(shí),需要有一個(gè)標(biāo)識(shí)來(lái)指示新的位置,這就是標(biāo)號(hào),通過(guò)在目的地址的前面放上一個(gè)標(biāo)號(hào),可以在指令中使用標(biāo)號(hào)來(lái)代替直接使用地址。
使用變量是任何編程語(yǔ)言都要遇到的工作,Win32匯編也不例外,在MASM中使用變量也有需要注意的幾個(gè)問(wèn)題,錯(cuò)誤地使用變量定義或用錯(cuò)誤的方法初始化變量會(huì)帶來(lái)難以定位的錯(cuò)誤。
變量是計(jì)算機(jī)內(nèi)存中已命名的存儲(chǔ)位置,在C語(yǔ)言中有很多種類(lèi)的變量,如整數(shù)型、浮點(diǎn)型和字符型等,不同的變量有不同的用途和尺寸,比如說(shuō)雖然長(zhǎng)整數(shù)和單精度浮點(diǎn)數(shù)都是32位長(zhǎng),但它們的用途不同。
顧名思義,變量的值在程序運(yùn)行中是需要改變的,所以它必須定義在可寫(xiě)的段內(nèi),如.data和.data?,或者在堆棧內(nèi)。按照定義的位置不同,MASM中的變量也分為全局變量和局部變量?jī)煞N。
在MASM中標(biāo)號(hào)和變量的命名規(guī)范是相同的,它們是:
1)可以用字母、數(shù)字、下劃級(jí)及符號(hào)@、$和?。
2)第一個(gè)符號(hào)不能是數(shù)字。
3)長(zhǎng)度不能超過(guò)240個(gè)字符。
4)不能使用指令名等關(guān)鍵字。
5)在作用域內(nèi)必須是唯一的。
標(biāo)號(hào)
標(biāo)號(hào)的定義
當(dāng)在程序中使用一條跳轉(zhuǎn)指令的時(shí)候,可以用標(biāo)號(hào)來(lái)表示跳轉(zhuǎn)的目的地,編譯器在編譯的時(shí)候會(huì)把它替換成地址,標(biāo)號(hào)既可以定義在目的指令同一行的頭部,也可以在目的指令前一行單獨(dú)用一行定義,標(biāo)號(hào)定義的格式是:
標(biāo)號(hào)名: 目的指令
標(biāo)號(hào)的作用域是當(dāng)前的子程序,在單個(gè)子程序中的標(biāo)號(hào)不能同名,否則編譯器不知該用哪個(gè)地址,但在不同的子程序中可以有相同名稱(chēng)的標(biāo)號(hào),這意味著不能從一個(gè)子程序中用跳轉(zhuǎn)指令跳到另一個(gè)子程序中。
在低版本的MASM中,標(biāo)號(hào)在整個(gè)程序中是唯一的,子程序中的標(biāo)號(hào)也可以從整個(gè)程序的任何地方轉(zhuǎn)入。但Win32匯編使用的高版本MASM中不允許這樣,這是為了提供對(duì)局部變量和參數(shù)的支持,由于在子程序入口有對(duì)堆棧的初始化指令,所以一個(gè)子程序不允許有多個(gè)入口,其結(jié)果主是標(biāo)號(hào)的作用域變成了單個(gè)子程序范圍。
MASM中的@@
在DOS時(shí)代,為標(biāo)號(hào)起名是個(gè)麻煩的事情,因?yàn)閰R編指令用到跳轉(zhuǎn)指令特別多,任何比較和測(cè)試等都要涉及跳轉(zhuǎn),所以在程序中會(huì)有很多標(biāo)號(hào),在整個(gè)程序范圍內(nèi)起個(gè)不重名的標(biāo)號(hào)要費(fèi)一番功夫,結(jié)果常常用addr1和addr2之類(lèi)的標(biāo)號(hào)一直延續(xù)下去,如果后來(lái)要在中間插一個(gè)標(biāo)號(hào),那么就常常出現(xiàn)addr1_1和loop10_5之類(lèi)奇怪的標(biāo)號(hào)。
實(shí)際上,很多標(biāo)號(hào)會(huì)使用一到兩次,而且不一定非要起個(gè)有意義的名稱(chēng),如匯編程序中下列代碼結(jié)構(gòu)很多:
mov cx,1234h
cmp flag,1
je loc1
mov cx,1000h
loc1:
loop loc1
loc1在別的地方就再也用不到了,對(duì)于這種情況,高版本的MASM用@@標(biāo)號(hào)去代替它:
mov cx,1234h
cmp flag,1
je @F
mov cx,1000h
@@:
loop @B
當(dāng)用@@做標(biāo)號(hào)時(shí),可以用@F和@B來(lái)引用它,@F表示本條指令后的第一個(gè)@@標(biāo)號(hào),@B表示本條指令前的第一個(gè)@@標(biāo)號(hào),程序中可以有多個(gè)@@標(biāo)號(hào),@B和@F只尋找匹配最近的一個(gè)。
不要在間隔太遠(yuǎn)的代碼中使用@@標(biāo)號(hào),因?yàn)樵谝院蟮男薷闹?/span>@@和@B,@F中間可能會(huì)被無(wú)意中插入一個(gè)新的@@,這樣一來(lái),@B或@F就會(huì)引用到錯(cuò)誤的地方去,源程序中@@標(biāo)號(hào)和跳轉(zhuǎn)指令之間的距離最好限制在編輯器能夠顯示的同一屏幕的范圍內(nèi)。
全局變量
全局變量的定義
全局變量的作用域是整個(gè)程序,Win32匯編的全局變量定義在.data或.data?段內(nèi),可以同時(shí)定義變量的類(lèi)型和長(zhǎng)度,格式是:
變量名 類(lèi)型 初始值1, 初始值2,…
變量名 類(lèi)型 重復(fù)數(shù)量 dup (初始值1,初始值2,…)
MASM中可以定義的變量類(lèi)型相當(dāng)多。
名稱(chēng)
|
表示方式
|
縮寫(xiě)
|
長(zhǎng)度(字節(jié))
|
字節(jié)
|
byte
|
db
|
1
|
字
|
word
|
dw
|
2
|
雙字(double word)
|
dword
|
dd
|
4
|
三字(far word)
|
fword
|
df
|
6
|
四字(quad word)
|
qword
|
dq
|
8
|
十字節(jié)BCD碼(ten byte)
|
tbyte
|
dt
|
10
|
有符號(hào)字節(jié)(sign byte)
|
sbyte
|
|
1
|
有符號(hào)字(sign word)
|
sword
|
|
2
|
有符號(hào)雙字(sign dword)
|
sdword
|
|
4
|
單精度浮點(diǎn)數(shù)
|
real4
|
|
4
|
雙精度浮點(diǎn)數(shù)
|
real8
|
|
8
|
10字節(jié)浮點(diǎn)數(shù)
|
real10
|
|
10
|
所有使用到變量類(lèi)型的情況中,只有定義全局變量的時(shí)候類(lèi)型才可以用縮寫(xiě),現(xiàn)在先來(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,的順序在內(nèi)存中重復(fù)100遍,一共是200個(gè)字節(jié)。
例5定義了一個(gè)1024字節(jié)的緩沖區(qū)。
例6定義了一個(gè)字符串,總共占用了12個(gè)字節(jié)。兩頭的單引號(hào)是定界的符號(hào),并不屬于字符串中真正的內(nèi)容。
在byte類(lèi)型變量的定義中,可以用引號(hào)定義字符串和數(shù)值定義的方法混用,假設(shè)要定義兩個(gè)字符串Hello,World!和Hello again,每個(gè)字符串后面中回車(chē)和換行符,最后以一個(gè)0字符結(jié)尾,可以定義如下:
szText db ‘Hello,World!’,0dh,0ah,’Hello again’,0dh,0ah,0
全局變量的初始化值
全局變量在定義中既可以指定初值,也可以只用問(wèn)題預(yù)留究竟,在.data?段中,只能用問(wèn)號(hào)預(yù)留究竟,因?yàn)?/span>.data?段中不能指定初始值,這里就有一個(gè)問(wèn)題:既然可以用問(wèn)號(hào)預(yù)留空間,那么在實(shí)際運(yùn)行的時(shí)候,這個(gè)未初始化的值是隨機(jī)的還是確定的呢?在全局變量中,這個(gè)值就是0,所以用問(wèn)號(hào)指定的全局變量如果要以0為初始值的話,在程序中可以不必為它賦值。
局部變量
局部變量這個(gè)名稱(chēng)最早源于高級(jí)語(yǔ)言,主要是為了定義一些僅在單個(gè)函數(shù)里面有用的變量而提出的,使用局部變量能帶來(lái)一些額外的好處,它使程序的模塊化封裝變得可能,試想一下,如果要用到的變量必須定義在程序的數(shù)據(jù)段里面,假設(shè)在一個(gè)子程序中要用到一些變量,當(dāng)把這個(gè)子程序移植到別的程序時(shí),除了把代碼移過(guò)去以外,還必須把變量定義移過(guò)去。而即使把變量定義移過(guò)去了,由于這些變量定義在大家都可以用的數(shù)據(jù)段中,就無(wú)法對(duì)別的代碼保持透明,別的代碼有可能有意無(wú)意地修改它們。還有,在一個(gè)大的工程項(xiàng)目中,存在很多的子程序,所有的子程序要用到的變量全部定義在數(shù)據(jù)段中,會(huì)使數(shù)據(jù)段變得很大,混在一起的變量也使維護(hù)變得非常不方便。
局部變量這個(gè)概念出現(xiàn)以后,兩個(gè)以上子程序都要用到的數(shù)據(jù)才被定義為全局變量統(tǒng)一放在數(shù)據(jù)段中,僅在子程序內(nèi)部使用的變量則放在堆棧中,這樣子程序可以編成黑匣子的模樣,使程序的模塊結(jié)構(gòu)更加分明。
局部變量的作用域是單個(gè)子程序,在進(jìn)入子程序的時(shí)候,通過(guò)修改堆棧指針esp來(lái)預(yù)留出需要的空間,在用ret指令返回主程序之前,同樣通過(guò)恢復(fù)esp丟棄這些空間,這些變量就隨之無(wú)效了。它的缺點(diǎn)就是因?yàn)榭臻g是臨時(shí)分配的,所以無(wú)法定義含有初始化值的變量,對(duì)局部變量的初始化一般在子程序中由指令完成。
在DOS時(shí)代,低版本的宏匯編本來(lái)無(wú)所謂全局變量和局部變量,所有的變量都是定義在數(shù)據(jù)段里面的,能讓被所有的子程序或主程序存取,就相當(dāng)于現(xiàn)在所說(shuō)的全局變量,用匯編語(yǔ)言在堆棧中定義局部變量是很麻煩的一件事情。要和高級(jí)語(yǔ)言做混合編程的時(shí)候,程序員往往很痛苦地在邊上準(zhǔn)備一張表,表上的內(nèi)容是局部變量名和ebp指針的位置關(guān)系。
局部變量的定義
MASM用local偽指令提供了對(duì)局部變量的支持。定義的格式是:
local 變量名1 [[重復(fù)數(shù)量]] [:類(lèi)型], 變量名2 [[重復(fù)數(shù)量]] [:類(lèi)型] ……
local偽指令必須緊接在子程序定義的偽指令proc后、其他指令開(kāi)始前,這是因?yàn)榫植孔兞康臄?shù)目必須在子程序開(kāi)始的時(shí)候就確定下來(lái),在一個(gè)local語(yǔ)句定義不下的時(shí)候,可以有多個(gè)local語(yǔ)句,語(yǔ)法中的數(shù)據(jù)類(lèi)型不能用縮寫(xiě),如果要定義數(shù)據(jù)結(jié)構(gòu),可以用數(shù)據(jù)結(jié)構(gòu)的名稱(chēng)當(dāng)做類(lèi)型。Win32匯編默認(rèn)的類(lèi)型是dword,如果定義dword類(lèi)型的局部變量,則類(lèi)型可以省略。當(dāng)定義數(shù)組的時(shí)候,可以[]括號(hào)起來(lái)。不能使用定義全局變量的dup偽指令。局部變量不能和已定義的全局變量同名。局部變量的作用域是當(dāng)前子程序,所以在不同的子程序中可以有同名的局部變量。
定義局部變量的例子:
local local[1024]:byte ;例1
local loc2 ;例2
local loc3:WNDCLASS ;例3
例1定義了一個(gè)1024字節(jié)長(zhǎng)的局部變量loc1。
例2定義了一個(gè)名為loc2的局部變量,類(lèi)型是默認(rèn)值dword。
例3定義了一個(gè)WNDCLASS數(shù)據(jù)結(jié)構(gòu),名為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è)名為TestProc的子程序,用local語(yǔ)句定義了3個(gè)變量,@loc1是dword類(lèi)型,@loc2是word類(lèi)型,@loc3是byte類(lèi)型,在程序中分別有3句存取3個(gè)局部變量的指令,然后就返回了,編譯成可執(zhí)行文件后,再把它反匯編就得到以下指令:
: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
這些就是使用局部變量所必需的指令,分別用于局部變量的準(zhǔn)備工作和掃尾工作。執(zhí)行了call指令后,CPU把返回的地址壓入堆棧,再轉(zhuǎn)移到子程序執(zhí)行,esp在程序的執(zhí)行過(guò)程中可能隨時(shí)用到,不可能用esp來(lái)隨時(shí)存取局部變量,ebp寄存器是以堆棧段為默認(rèn)數(shù)據(jù)段的,所以,可以用ebp做指針,于是,在初始化前,先用一句push ebp指令把原來(lái)的dbp保存起來(lái),然后把esp的值放到ebp中,供存取局部變量做指針用,再后面就是堆棧中預(yù)留空間了,由于堆棧是向下增長(zhǎng)的,所以要在esp中加一個(gè)負(fù)值,FFFFFFF8就是-8,慢著!一個(gè)dword加一個(gè)word加一個(gè)字節(jié)不是7嗎,為什么是8呢?這是因?yàn)樵?/span>80386處理器中,以dword為界對(duì)齊時(shí)存取內(nèi)存速度最快,所以MASM寧可浪費(fèi)一個(gè)字節(jié),執(zhí)行了這3句指令后,初始化完成,就可以進(jìn)行正常的操作了,從指令中可以看出局部變量在堆棧中的位置排列。
在程序退出的時(shí)候,必須把正確的esp設(shè)置回去,否則,ret指令會(huì)從堆棧中取出錯(cuò)誤的地址返回,看程序可以發(fā)現(xiàn),ebp就是正確的esp值,因?yàn)樽映绦蜷_(kāi)始的時(shí)候已經(jīng)有一句mov ebp,esp,所以要返回的時(shí)候只要先mov esp,ebp,然后再pop ebp,堆棧就是正確的了。
在80386指令集中有一條指令可以在一句中實(shí)現(xiàn)這些功能,就是leave指令,所以,編譯器在ret指令之前只使用了一句leave指令。
明白了局部變量使用的原理,就很容易理解使用時(shí)的注意點(diǎn):ebp寄存器是關(guān)鍵,它起到保存原始esp的作用,并隨時(shí)用做存取局部變量的指針基址,所以在任何時(shí)刻,不要嘗試把ebp用于別的用途,否則會(huì)帶來(lái)意想不到的后果。
Win32匯編中局部變量的使用方法可以解釋一個(gè)很有趣的現(xiàn)象:在DOS匯編的時(shí)候,如果在子程序中的push指令和pop指令不配對(duì),那么返回的時(shí)候ret指令從堆棧里得到的肯定是錯(cuò)誤的返回地址,程序也就死掉了。但在Win32匯編中,push指令和pop指令不配對(duì)可能在邏輯上產(chǎn)生錯(cuò)誤,卻不會(huì)影響子程序正常返回,原因就是在返回的時(shí)候esp不是靠相同數(shù)量的push和pop指令來(lái)保持一致的,而是靠leave指令從保存在ebp中的原始值中取回來(lái)的,也就是說(shuō),即使把esp改得一塌糊涂也不會(huì)影響到子程序的返回,當(dāng)然,竅門(mén)就在ebp,把ebp改掉,程序就玩完了!
局部變量的初始化值
顯然,局部變量是無(wú)法在定義的時(shí)候指定初始化值的,因?yàn)?/span>local偽指令只是簡(jiǎn)單地把空間給留出來(lái),那么開(kāi)始使用時(shí)它里面是什么值呢?和全局變量不一樣,局部變量的初始值是隨機(jī)的,是其他子程序執(zhí)行后在堆棧里留下的垃圾,所以,對(duì)局部變量的值一定要初始化,特別是定義為結(jié)構(gòu)后當(dāng)參數(shù)傳遞給API函數(shù)的時(shí)候。
在API函數(shù)使用的大量數(shù)據(jù)結(jié)構(gòu)中,往往用0做默認(rèn)值,如果用局部變量定義數(shù)據(jù)結(jié)構(gòu),初始化時(shí)只定義了其中的一些字段,那么其余字段的當(dāng)前值可以是編程者預(yù)想不到的數(shù)值,傳給API函數(shù)后,執(zhí)行的結(jié)果可能是意想不到的,這是初學(xué)者很容易忽略的一個(gè)問(wèn)題。所以最好的辦法是:在賦值前首先將整個(gè)數(shù)據(jù)結(jié)構(gòu)填0,然后再初始化要用的字段,這樣其余的字段就不必一個(gè)個(gè)地去填0了,RtlZeroMemory這個(gè)API函數(shù)就是實(shí)現(xiàn)填0的功能的。
數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)實(shí)際上是由多個(gè)字段組成的數(shù)據(jù)樣板,相當(dāng)于一種自定義的數(shù)據(jù)類(lèi)型,數(shù)據(jù)結(jié)構(gòu)中間的每一個(gè)字段可以是字節(jié)、字、雙字、字符串或所有可能的數(shù)據(jù)類(lèi)型。
比如在API函數(shù)RegisterClass中要使用到一個(gè)叫做WNDCLASS的數(shù)據(jù)結(jié)構(gòu),Microsoft的手冊(cè)上是如下定義的
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è)數(shù)據(jù)結(jié)構(gòu)包含了10個(gè)字段,字段的名稱(chēng)是style,lpfnWndProc和cbClsExtra等,前面的UINT和WNDPROC等是這些字段的類(lèi)型,在匯編中,數(shù)據(jù)結(jié)構(gòu)的寫(xiě)法如下:
結(jié)構(gòu)名 struct
字段1 類(lèi)型 ?
字段2 類(lèi)型 ?
……
結(jié)構(gòu)名 ends
上面的WNDCLASS結(jié)構(gòu)定義用匯編的格式來(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所涉及的數(shù)據(jù)結(jié)構(gòu)在Windows.inc文件中都已經(jīng)有定義了。要注意的是,定義了數(shù)據(jù)結(jié)構(gòu)實(shí)際上只是定義了一個(gè)樣板,上面的定義語(yǔ)句并不會(huì)在哪個(gè)段中產(chǎn)生數(shù)據(jù),和Word中使用各種信紙與文書(shū)等模板類(lèi)似,定義了數(shù)據(jù)結(jié)構(gòu)以后就可以多次在源程序中用這個(gè)樣板當(dāng)做數(shù)據(jù)類(lèi)型來(lái)定義數(shù)據(jù),使用數(shù)據(jù)結(jié)構(gòu)在數(shù)據(jù)段中定義數(shù)據(jù)的方法如下:
.data?
stWndClass WNDCLASS <>
……
.data
stWndClass WNDCLASS <1,1,1,1,1,1,1,1,1,1,>
……
這個(gè)例子定義了一個(gè)以WNDCLASS為結(jié)構(gòu)的變量stWndClass,第一段的定義方法是未初始化的定義方法,第二段是在定義的同時(shí)指定結(jié)構(gòu)中各字段的初始化值,各字段的初始值用逗號(hào)隔開(kāi),在這個(gè)例子中10個(gè)字段的初始值都指定為1。
在匯編中,數(shù)據(jù)結(jié)構(gòu)的引用方法有好幾種,以上面的定義為例,如果要使用stWndClass中的lpfnWndProc字段,最直接的辦法是:
mov eax,stWndClass.lpfnWndProc
它表示把lpfnWndProc字段的值放入eax中去,假設(shè)stWndClass在內(nèi)存中的地址是403000h,這句指令會(huì)被編譯成mov eax,[403004h],因?yàn)?/span>lpfnWndProc是stWndClass中的第二個(gè)字段,第一個(gè)字段是dword,已經(jīng)占用了4字節(jié)的空間。
在實(shí)際使用中,常常有使用指令存取數(shù)據(jù)結(jié)構(gòu)的情況,如果使用esi寄存器做指針尋址,可以使用下列語(yǔ)句完成同樣的功能:
mov esi,offset stWndClass
move ax,[esi + WNDCLASS.lpfnWndProc]
注意:第二句是[esi + WNDCLASS.lpfnWndProc]而不是[esi + stWndClass.lpfnWndProc],因?yàn)榍罢弑痪幾g成mov eax,[esi + 4],而后者被編譯成mov eax,[esi + 403004h],后者的結(jié)果顯然是錯(cuò)誤的!如果要對(duì)一個(gè)數(shù)據(jù)結(jié)構(gòu)中的大量字段進(jìn)行了操作,這種寫(xiě)法顯然比較煩瑣,MASM還有一個(gè)用法,可以用assume偽指令把寄存器預(yù)先定義為結(jié)構(gòu)指針,再進(jìn)行操作:
mov esi,offset stWndClass
assume esi:ptr WNDCLASS
move ax,[esi].lpfnWndProc
……
assume esi:nothing
這樣,使用寄存器也可以用逗號(hào)引用字段名,程序的可讀性比較好。這樣的寫(xiě)法在最后編譯成可執(zhí)行程序的時(shí)候產(chǎn)生同樣的代碼。注意:在不再使用esi寄存器做指針的時(shí)候要用assume esi:nothing取消定義。
結(jié)構(gòu)的定義也可以嵌套,如果要定義一個(gè)新的NEW_WNDCLASS結(jié)構(gòu),里面包含一個(gè)老的WNDCLASS結(jié)構(gòu)和一個(gè)新的dwOption字段,那么可以如下定義:
NEW_WNDCLASS struct
DwOption dword ?
OldWndClass WNDCLASS <>
NEW_WNDCLASS ends
假設(shè)現(xiàn)在esi是指向一個(gè)NEW_WNDCLASS的指針,那么引用里面嵌套的oldWndClass中的lpfnWndProc字段時(shí),就可以用下面的語(yǔ)句:
move ax,[esi].oldWndClass.lpfnWndProc
結(jié)構(gòu)的嵌套在Windows的數(shù)據(jù)定義中也常有,熟練掌握數(shù)據(jù)結(jié)構(gòu)的使用對(duì)Win32匯編編程是很重要的!
變量的使用
以不同的類(lèi)型訪問(wèn)變量
這個(gè)話題有點(diǎn)像C語(yǔ)言中的數(shù)據(jù)類(lèi)型強(qiáng)制轉(zhuǎn)換,C語(yǔ)言中的類(lèi)型轉(zhuǎn)換指的是把一個(gè)變量的內(nèi)容轉(zhuǎn)換成另外一種類(lèi)型,轉(zhuǎn)換過(guò)程中,數(shù)據(jù)的內(nèi)容已經(jīng)發(fā)生了變化,如把浮點(diǎn)數(shù)轉(zhuǎn)換成整數(shù)后,小數(shù)點(diǎn)后的內(nèi)容就丟失了。在MASM中以不同的類(lèi)型訪問(wèn)不會(huì)對(duì)變量造成影響。
例如,以db方式定義一個(gè)緩沖區(qū):
szBuffer db 1024 dup (?)
然后從其他地方取得了數(shù)據(jù),但數(shù)據(jù)的格式是字方式組織的,要處理數(shù)據(jù),最有效的方法是兩個(gè)字節(jié)兩個(gè)字節(jié)處理,但如果在程序中把szBuffer的值放入ax:
mov ax,szBuffer
編譯器會(huì)報(bào)一個(gè)錯(cuò):
error A2070: invalid instruction operands
意思是無(wú)效的指令操作,為什么呢?因?yàn)?/span>szBuffer是用db定義的,而ax的尺寸是一個(gè)word,等于兩個(gè)字節(jié),尺寸不符合。MASM中,如果要用指定類(lèi)型之外的長(zhǎng)度訪問(wèn)變量,必須顯式地指出要訪問(wèn)的長(zhǎng)度,這樣,編譯器忽略語(yǔ)法上的長(zhǎng)度檢驗(yàn),僅使用變量的地址。使用的方法是:
類(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ò)編譯,當(dāng)然,類(lèi)型必須和操作的寄存器長(zhǎng)度匹配。在這里要注意的是,指定類(lèi)型的參數(shù)訪問(wèn)并不會(huì)去檢測(cè)長(zhǎ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
……
上面的程序片斷,每一句執(zhí)行后寄存器中的值是什么呢,mov al,bTest1這一句很顯然使al等12h,下面的兩句呢,ax和eax難道等于0012h和00000012h嗎?實(shí)際運(yùn)行結(jié)果是3412h和78123412h,為什么呢?(DOS匯編基礎(chǔ)不錯(cuò)的同學(xué),應(yīng)該能理解)先來(lái)看反匯編的內(nè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段中的變量是按順序從低地址往高地址排列的,對(duì)于超過(guò)一個(gè)字節(jié)的數(shù)據(jù),80386處理器的數(shù)據(jù)排列方式是低位數(shù)據(jù)在低地址,所以wTest2的1234h在內(nèi)存中的排列是34h 12h,因?yàn)?/span>34h是低位。同樣,dwTest3在內(nèi)存中以78h 56h 34h 12h從低地址往高地址存放,在執(zhí)行指令mov ax,word ptr bTest1的時(shí)候,是從bTest1的地址403000h處取一個(gè)字,其長(zhǎng)度已經(jīng)超過(guò)了bTest1的范圍并落到了wTest2中,從內(nèi)存中看,是取了bTest1的數(shù)據(jù)12h和wTest2的低位34h,在這兩個(gè)字節(jié)中,12h位于低地址,所以ax中的數(shù)值是3412h。同理,看另一條指令:
move ax,dword ptr bTest1
這條指令取了bTest1,wTest2的全部和dwTest3的最低位78h,在內(nèi)存中的排列是12h 34h 12h 78h,所以eax等于78123412h。
這個(gè)例子說(shuō)明了匯編中用ptr強(qiáng)制覆蓋變量長(zhǎng)度的時(shí)候,實(shí)質(zhì)上是只用了變量的地址而禁止編譯器進(jìn)行檢驗(yàn),編譯器并不會(huì)考慮定界的問(wèn)題,程序員在使用的時(shí)候必須對(duì)內(nèi)存中的數(shù)據(jù)排列有個(gè)全局概念,以免越界存取到意料之外的數(shù)據(jù)。
如果程序員的本意是類(lèi)似于C語(yǔ)言的強(qiáng)制類(lèi)型轉(zhuǎn)換,想把bTest1的一個(gè)字節(jié)擴(kuò)展到一個(gè)字或一個(gè)雙字再放到ax或eax中,高位保持0而不是越界存取到其他的變量,可以用80386的擴(kuò)展指令來(lái)實(shí)現(xiàn)。80386處理器提供的movzx指令可以實(shí)現(xiàn)這個(gè)功能,例如:
movzx ax,bTest1 ;例1
movzx eax,bTest1 ;例2
movzx eax,cl ;例3
movzx eax,ax ;例4
例1把單字節(jié)變量bTest1的值擴(kuò)展到16位放入ax中。
例2把單字節(jié)變量bTest1的值擴(kuò)展到32位放入eax中。
例3把cl中的8位值擴(kuò)展到32位放入eax中。
例4把ax中的16位值擴(kuò)展到32位放入eax中。
用movzx指令進(jìn)行數(shù)據(jù)長(zhǎng)度擴(kuò)展是Win32匯編中經(jīng)常用到的技巧。
變量的尺寸和數(shù)量
在源程序中用到變量的尺寸和數(shù)量的時(shí)候,可以用sizeof和lengthof偽指令來(lái)實(shí)現(xiàn),格式是:
sizeof 變量名、數(shù)據(jù)類(lèi)型或數(shù)據(jù)結(jié)構(gòu)名
lengthof 變量名
sizeof偽指令可以取得變量、數(shù)據(jù)類(lèi)型或數(shù)據(jù)結(jié)構(gòu)以字節(jié)為單位的長(zhǎng)度,lengthof可以取得變量中數(shù)據(jù)的項(xiàng)數(shù)。例如定義了以下數(shù)據(jù):
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
執(zhí)行后eax的值是stWndClass結(jié)構(gòu)的長(zhǎng)度40,ebx同樣是40,ecx的值是13,就是Hello,world!字符串的長(zhǎng)度加上一個(gè)字節(jié)的0結(jié)束符,edx的值是一個(gè)雙字的長(zhǎng)度:4,而esi則等于4個(gè)雙字的長(zhǎng)度16。
如果把所有的sizeof換成lengthof,那么eax會(huì)等于1,因?yàn)橹欢x了1項(xiàng)WNDCLASS,而ecx同樣等于13,esi則等于4,而lenghof WNDCLASST和lengthof dword是非法的用法,編譯程序會(huì)報(bào)錯(cuò)。
要注意的是,sizeof和lengthof的數(shù)值是編譯時(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中的變量定義只認(rèn)一行,后一行db ‘World’,0實(shí)際上是另一個(gè)沒(méi)有名稱(chēng)的數(shù)據(jù)定義,編譯器認(rèn)為sizeof szHello是第一行字符的數(shù)量。雖然把szHello的地址當(dāng)參數(shù)傳給MessageBox等函數(shù)顯示時(shí)會(huì)把兩行都顯示出來(lái),但嚴(yán)格地說(shuō)這是越界使用變量。雖然在實(shí)際的應(yīng)用中這樣定義長(zhǎng)字符串的用法很普遍,因?yàn)槿绻@示一屏幕幫助,一行是不夠的,但要注意的是:要用到這種字符串的長(zhǎng)度時(shí),千萬(wàn)不要用sizeof去表示,最好是在程序中用lstrlen函數(shù)去計(jì)算。
獲取變量地址
獲取變量地址的操作對(duì)于全局變量和局部變量是不同的。
對(duì)于全局變量,它的地址在編譯的時(shí)候已經(jīng)由編譯器確定了,它的用法大家都不陌生:
mov 寄存器, offset 變量名
其中offset是取變量地址的偽操作符,和sizeof偽操作符一樣,它僅把變量的地址帶到指令中去,這個(gè)操作是在編譯時(shí)而不是在運(yùn)行時(shí)完成的。
對(duì)于局部變量,它是用ebp來(lái)做指針操作的,假設(shè)ebp的值是40100h,那么局部變量l的地址是ebp-4即400FCh,由于ebp的值隨著程序的執(zhí)行環(huán)境不同可能是不同的,所以局部變量的地址值在編譯的時(shí)候也是不確定的,不可能用offset偽操作符來(lái)獲取它的地址。
80386處理器中有一條指令用來(lái)取指針的地址,就是lea指令,如:
lea eax,[ebp-4]
該指令可以在運(yùn)行時(shí)按照ebp的值實(shí)際計(jì)算出地址放到eax中。
如果要在invoke偽指令的參數(shù)中用到一個(gè)局部變量的地址,該怎么辦呢?參數(shù)中是不可能寫(xiě)入lea指令的,用offset又是不對(duì)的。MASM對(duì)此有一個(gè)專(zhuān)用的偽操作符addr,其格式為:
addr 局部變量名和全局變量名
當(dāng)addr后跟全局變量名的時(shí)候,用法和offset是相同的;當(dāng)addr后面跟局部變量名的時(shí)候,編譯器自動(dòng)用lea指令先把地址取到eax中,然后用eax來(lái)代替變量地址使用。注意addr偽操作符只能在invoke的參數(shù)中使用,不能用在類(lèi)似于下列的場(chǎng)合:
move ax, addr 局部變量名 ;注意:錯(cuò)誤用法
假設(shè)在一個(gè)子程序中有如下invoke指令:
invoke Test,eax, addr szHello
其中Test是一個(gè)需要兩個(gè)參數(shù)的子程序,szHello是一個(gè)局部變量,會(huì)發(fā)生什么結(jié)果呢?編譯器會(huì)把invoke偽指令和addr翻譯成下面這個(gè)模樣:
lea eax,[ebp-4]
push eax ;參數(shù)2:addr szHello
push eax ;參數(shù)1:eax
call Test
發(fā)現(xiàn)了什么?到push第一個(gè)參數(shù)eax之前,eax的值已經(jīng)被lea eax,[ebp-4]指令覆蓋了!也就是說(shuō),要用到的eax的值不再有效,所以,當(dāng)在invoke中使用addr偽操作符時(shí),注意在它的前面不能用eax,否則eax的值會(huì)被覆蓋掉,當(dāng)然eax在addr的后面的參數(shù)中用是可以的。幸虧MASM編譯器對(duì)這種情況有如下錯(cuò)誤提示:
error A2133:register value overwritten by INVOKE
否則,不知道又會(huì)引出多少莫名其妙的錯(cuò)誤!
使用子程序
當(dāng)程序中相同功能的一段代碼用得比較頻繁時(shí),可以將它分離出來(lái)寫(xiě)成一個(gè)子程序,在主程序中用call指令來(lái)調(diào)用它。這樣可以不用重復(fù)寫(xiě)相同的代碼,而用call指令就可以完成多次同樣的工作了。Win32匯編中的子程序也采用堆棧來(lái)傳遞參數(shù),這樣就可以用invoke偽指令來(lái)進(jìn)行調(diào)用和語(yǔ)法檢查工作。
子程序的定義
子程序的定義方式如下所示:
子程序名 proc [距離] [語(yǔ)言類(lèi)型] [可視區(qū)域] [USES寄存器列表] [,參數(shù):類(lèi)型]…[VARARG]
local 局部變量列表
指令
子程序名 endp
proc和endp偽指令定義了子程序開(kāi)始和結(jié)束的位置,proc后面跟的參數(shù)是子程序的屬性和輸入?yún)?shù)。子程序的屬性有:
距離。可以是NEAR,FAR,NEAR16,NEAR32,FAR16或FAR32,Win32中只有一個(gè)平坦的段,無(wú)所謂距離,所以對(duì)距離的定義往往忽略。
語(yǔ)言類(lèi)型表示參數(shù)的使用方式和堆棧平衡的方式,可以是StdCall,C,SysCall,BASIC,FORTRAN和PASCAL,如果忽略,則使用程序頭部.model定義的值。
可視區(qū)域,可以是PRIVATE,PUBLIC和EXPORT。PRIVATE表示子程序只對(duì)本模塊可見(jiàn);PUBLIC表示對(duì)所有的模塊可見(jiàn)(在最后編譯鏈接完成的.exe文件中);EXPORT表示是導(dǎo)出的函數(shù),當(dāng)編寫(xiě)DLL的時(shí)候要將某個(gè)函數(shù)導(dǎo)出的時(shí)候可以這樣使用。默認(rèn)的設(shè)置是PUBLIC。
USES寄存器列表,表示由編譯器在子程序指令開(kāi)始前自動(dòng)安排push這些寄存器的指令,并且在ret前自動(dòng)安排pop指令,用于保存執(zhí)行環(huán)境,但筆者認(rèn)為不如自己在開(kāi)頭和結(jié)尾用pushad和popad指令一次保存和恢復(fù)所有寄存器來(lái)得方便。
參數(shù)和類(lèi)型。參數(shù)指參數(shù)的名稱(chēng),在定義參數(shù)名的時(shí)候不能跟全局變量和子程序中的局部變量重名。對(duì)于類(lèi)型,由于Win32中的參數(shù)類(lèi)型只有32位(dword)一種類(lèi)型,所以可以省略。在參數(shù)定義的最后還可以跟VARARG,表示在已確定的參數(shù)后還可以跟多個(gè)數(shù)量不確定的參數(shù),在Win32匯編中唯一使用VARARG的API就是wsprintf,類(lèi)似于C語(yǔ)言中的printf,其參數(shù)的個(gè)數(shù)取決于要顯示的字符串中指定的變量個(gè)數(shù)。
完成了定義之后,可以用invoke偽指令來(lái)調(diào)用子程序,當(dāng)invoke偽指令位于子程序代碼之前的時(shí)候,處理到invoke語(yǔ)句的時(shí)候編譯器還沒(méi)有掃描到子程序定義信息的記錄,所以會(huì)有以下錯(cuò)誤的信息:
error A2006: undefined symbol: _ProcWinMain
這并不是說(shuō)子程序的編寫(xiě)有錯(cuò)誤,而是invoke偽指令無(wú)法得知子程序的定義情況,所以無(wú)法進(jìn)行參數(shù)的檢測(cè)。在這種情況下,為了讓invoke指令能正常使用,必須在程序的頭部用proto偽操作定義子程序的信息,提前告訴invoke語(yǔ)句關(guān)于子程序的信息,當(dāng)然,如果子程序定義在前的話,用proto的定義就可以省略了。
由于程序的調(diào)試過(guò)程中可能常常對(duì)一些子程序的參數(shù)個(gè)數(shù)進(jìn)行調(diào)整,為了使它們保持一致,就需要同時(shí)修改proc語(yǔ)句和proto語(yǔ)句。在寫(xiě)源程序的時(shí)候有意識(shí)地把子程序的位置提到invoke語(yǔ)句的前面,省略掉proto語(yǔ)句,可以簡(jiǎn)化程序和避免出錯(cuò)。
參數(shù)傳遞和堆棧平衡
了解了子程序的定義方法后,讓我們繼續(xù)深入了解了程序的使用細(xì)節(jié)。在調(diào)用子程序時(shí),參數(shù)的傳遞是通過(guò)堆棧進(jìn)行的,也就是說(shuō),調(diào)用者把要傳遞給子程序的參數(shù)壓入堆棧,子程序在堆棧中取出相應(yīng)的值再使用,比如,如果要調(diào)用:
SubRouting(Var1, Var2, Var3)
經(jīng)過(guò)編譯后的最終代碼可能是(注意只是可能):
push Var3
push Var2
push Var1
call SubRouting
add esp,12
也就是說(shuō),調(diào)用者首先把參數(shù)壓入堆棧,然后調(diào)用子程序,在完成后,由于堆棧中先前壓入的數(shù)不再有用,調(diào)用者或者被調(diào)用者必須有一方把堆棧指針修正到調(diào)用前的狀態(tài),即堆棧的平衡。參數(shù)是最右邊的先入堆棧還是最左邊的先入堆棧、還有由調(diào)用者還是被調(diào)用者來(lái)修正堆棧都必須有個(gè)約定,不然就會(huì)產(chǎn)生錯(cuò)誤的結(jié)果,這就是在上述文字中使用“可能”這兩個(gè)字的原因。各種語(yǔ)言中調(diào)用子程序的約定是不同的,所以在proc以及proto語(yǔ)句的語(yǔ)言屬性中確定語(yǔ)言類(lèi)型后,編譯器才可能將invoke偽指令翻譯成正確的樣子,不同語(yǔ)言的不同點(diǎn)如下:
|
C
|
SysCall
|
StdCall
|
BASIC
|
FORTRAN
|
PASCAL
|
最先入棧參數(shù)
|
右
|
右
|
右
|
左
|
左
|
左
|
清除堆棧者
|
調(diào)用者
|
子程序
|
子程序
|
子程序
|
子程序
|
子程序
|
允許使用VARARG
|
是
|
是
|
是
|
否
|
否
|
否
|
注:VARARG表示參數(shù)的個(gè)數(shù)可以是不確定的,如wsprintf函數(shù),本表中特殊的地方是StdCall的堆棧清除平時(shí)是由子程序完成的,但使用VARARG時(shí)是由調(diào)用者清除的。
為了了解編譯器對(duì)不同類(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)行反匯編,看編譯器是如何轉(zhuǎ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
可以清楚地看到,在參數(shù)入棧順序上,C類(lèi)型和StdCall類(lèi)型是先把右邊的參數(shù)先壓入堆棧,而PASCAL類(lèi)型是先把左邊的參數(shù)壓入堆棧。在堆棧平衡上,C類(lèi)型是在調(diào)用者在使用call指令完成后,自行用add esp,8指令把8個(gè)字節(jié)的參數(shù)空間清除,而PASCAL和StdCall的調(diào)用者則不管這個(gè)事情,堆棧平衡的事情是由子程序用ret 8來(lái)實(shí)現(xiàn)的,ret指令后面加一個(gè)操作數(shù)表示在ret后把堆棧指針esp加上操作數(shù),完成的是同樣的功能。
Win32約定的類(lèi)型是StdCall,所以在程序中調(diào)用子程序或系統(tǒng)API后,不必自己來(lái)平衡堆棧,免去了很多麻煩。
存取參數(shù)和局部變量都是通過(guò)堆棧來(lái)定義的,所以參數(shù)的存取也是通過(guò)ebp做指針來(lái)完成的。在探討局部變量的時(shí)候,已經(jīng)就沒(méi)有參數(shù)的情況下ebp指針和局部變量的對(duì)應(yīng)關(guān)系做了分析,現(xiàn)在來(lái)分析一下ebp指針和參數(shù)之間的對(duì)應(yīng)關(guān)系,注意,這里是以Win32中的StdCall為例,不同的語(yǔ)言類(lèi)型,指針的順序可能是不同的。
假定在一個(gè)子程序中有兩個(gè)參數(shù),主程序調(diào)用時(shí)在push第一個(gè)參數(shù)前的堆棧指針esp為X,那么壓入兩個(gè)參數(shù)后的esp為X-8,程序開(kāi)始執(zhí)行call指令,call指令把返回地址壓入堆棧,這時(shí)候esp為X-C,接下去是子程序中用push ebp來(lái)保存ebp的值,esp變?yōu)?/span>X-10,再執(zhí)行一句mov ebp,esp,就可以開(kāi)始用ebp存取參數(shù)和局部變量了。
在源程序中,由于參數(shù)、局部變量和ebp的關(guān)系是由編譯器自動(dòng)維護(hù)的,所以讀者不必關(guān)心它們的具體關(guān)系,但到了用Soft-ICE等工具來(lái)分析其他軟件的時(shí)候,遇到調(diào)用子程序的時(shí)候一定要先看清楚它們之間的類(lèi)型差別。
在子程序中使用參數(shù),可以使用與存取局部變量同樣的方法,因?yàn)檫@兩者的構(gòu)造原理幾乎一模一樣,所以,在子程序中有invoke語(yǔ)句時(shí),如果要用到輸入?yún)?shù)的地址當(dāng)做invoke的參數(shù),同樣要遵循局部變量的使用方式,不能用offset偽操作符,只能用addr來(lái)完成。同樣,所有對(duì)局部變量使用的限制幾乎都可以適用于參數(shù)。
高級(jí)語(yǔ)法
以前高級(jí)語(yǔ)言和匯編的最大差別就是條件測(cè)試、分支和循環(huán)等高級(jí)語(yǔ)法。
高級(jí)語(yǔ)言中,程序員可以方便地用類(lèi)似于if,case,loop和while等語(yǔ)句來(lái)構(gòu)成程序的結(jié)構(gòu)流程,不僅條理清楚、一目了然,而且維護(hù)性相當(dāng)好。而匯編程序員呢?只能在cmp指令后面絞盡腦汁地想究竟用幾十種跳轉(zhuǎn)語(yǔ)句中的哪一種,這里就能列出近三十個(gè)條件跳轉(zhuǎn)指令來(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ě)結(jié)構(gòu)清晰、可讀性好的代碼變得相當(dāng)困難,這也是很多人視匯編為畏途的一個(gè)原因。
現(xiàn)在好了,MASM中新引入了一系列的偽指令,涉及條件測(cè)試、分支和循環(huán)語(yǔ)句,利用它們,匯編語(yǔ)言有了和高級(jí)語(yǔ)言一樣的結(jié)構(gòu),配合對(duì)局部變量和調(diào)用參數(shù)等高級(jí)語(yǔ)言中覺(jué)元素的支持,為使用Win32匯編編寫(xiě)大規(guī)模的應(yīng)用程序奠定了基礎(chǔ)。
條件測(cè)試語(yǔ)句
在高級(jí)語(yǔ)言中,所有的分支和循環(huán)語(yǔ)句首先要涉及條件測(cè)試,也就是涉及一個(gè)表達(dá)式的結(jié)果是真還是假的問(wèn)題,表達(dá)式中往往有用來(lái)做比較和計(jì)算的操作符,MASM也不例外,這就是條件測(cè)試語(yǔ)句。
MASM條件測(cè)試的基本表達(dá)式是:
寄存器或變量 操作符 操作數(shù)
兩個(gè)以上的表達(dá)式可以用邏輯運(yùn)算符連接:
(表達(dá)式1) 邏輯運(yùn)算符 (表達(dá)式2) 邏輯運(yùn)算符 (表達(dá)式3) …
允許的操作符和邏輯運(yùn)算符如下所示:
條件溑或的操作符
操作符和邏輯運(yùn)算符
|
操作
|
用途
|
==
|
等于
|
變量和操作數(shù)之間的比較
|
!=
|
不等于
|
變量和操作數(shù)之間的比較
|
>
|
大于
|
變量和操作數(shù)之間的比較
|
>=
|
大于等于
|
變量和操作數(shù)之間的比較
|
<
|
小于
|
變量和操作數(shù)之間的比較
|
<=
|
小于等于
|
變量和操作數(shù)之間的比較
|
&
|
位測(cè)試
|
將變量和操作數(shù)做與操作
|
!
|
邏輯取反
|
對(duì)變量取反或?qū)Ρ磉_(dá)式的結(jié)果取反
|
&&
|
邏輯與
|
對(duì)兩個(gè)表達(dá)式的結(jié)果進(jìn)行邏輯與操作
|
||
|
邏輯或
|
對(duì)兩個(gè)表達(dá)式的結(jié)果進(jìn)行邏輯或操作
|
舉例,左邊為表達(dá)式,右邊是表達(dá)式為真的條件:
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為零
細(xì)心的讀者一定會(huì)發(fā)現(xiàn),MASM的條件測(cè)試采用的是和C語(yǔ)言相同的語(yǔ)法。如!和&是對(duì)變量的操作符(取反和與操作),||和&&是表達(dá)式結(jié)果之間的邏輯與和邏輯或,而==、!=、>、<等是比較符。同樣,對(duì)于不含比較符的單個(gè)變量或寄存器,MASM也是將所有非零認(rèn)為是真,零值認(rèn)為是假。
MASM的條件測(cè)試語(yǔ)句有幾個(gè)限制,首先是表達(dá)式的左邊只能是變量或寄存器,不能為常數(shù);其次表達(dá)的兩邊不能同時(shí)為變量,但可以同時(shí)是寄存器。這些限制來(lái)自于80x86的指令,因?yàn)闂l件測(cè)試偽操作符只是簡(jiǎn)單地把每個(gè)表達(dá)式翻譯成cmp或test指令,80x86的指令集中沒(méi)有cmp 0,eax之類(lèi)的指令,同時(shí)也不允許直接操作兩個(gè)內(nèi)存中的數(shù),所以對(duì)這兩個(gè)限制是很好理解的。
除了這些和高級(jí)語(yǔ)言類(lèi)似的條件測(cè)試偽操作,匯編語(yǔ)言還有特殊的要求,就是程序中常常要根據(jù)系統(tǒng)標(biāo)志寄存器中的各種標(biāo)志位來(lái)做條件跳轉(zhuǎn),這些在高級(jí)語(yǔ)言中是用不到的,所以又增加了以下一些標(biāo)志位的狀態(tài)指示,它們本身相當(dāng)于一個(gè)表達(dá)式:
CARRY? 表示Carry位是否置位
OVERFLOW? 表示Overflow位是否置位
PARITY? 表示Parity位是否置位
SIGN? 表示Sign位是否置位
ZERO? 表示Zero位是否置位
要測(cè)試eax等于ebx同時(shí)Zero位置位,條件表達(dá)式可以寫(xiě)為:
(eax == ebx) && ZERO?
要測(cè)試eax等ebx同時(shí)Zero位清零,條件表達(dá)式可以寫(xiě)為:
(eax == ebx) && !ZERO?
和C語(yǔ)言的條件測(cè)試同樣,MASM的條件測(cè)試偽指令并不會(huì)改變被測(cè)試的變量或寄存器的值,只是進(jìn)行測(cè)試而已,到最后它會(huì)被編譯器翻譯成類(lèi)似于cmp或test之類(lèi)的比較或位測(cè)試指令。
分支語(yǔ)句
分支語(yǔ)句用來(lái)根據(jù)條件表達(dá)式測(cè)試的真假執(zhí)行不同的代碼模塊,MASM中的分支語(yǔ)句的語(yǔ)法如下:
.if 條件表達(dá)式1
表達(dá)式1為“真”時(shí)執(zhí)行的指令
[.elseif 條件表達(dá)式2]
表達(dá)式2為“真”時(shí)執(zhí)行的指令
[.elseif 條件表達(dá)式3]
表達(dá)式3為“真”時(shí)執(zhí)行的指令
……
[.else]
所有表達(dá)式為“否”時(shí)執(zhí)行的指令
.endif
注意:關(guān)鍵字if/elseif/else/endif的前面有個(gè)小數(shù)點(diǎn),如果不加小數(shù)點(diǎn),就變成宏匯編中的條件匯編偽操作了,結(jié)果可是天差地別。
這些偽指令把匯編程序的可讀性基本上提高到了高級(jí)語(yǔ)言的水平。
注意:使用.if/.else/.endif構(gòu)成分支偽指令的時(shí)候,不要漏寫(xiě)前面的小數(shù)點(diǎn),if/else/endif是宏匯編中條件匯編宏操作的偽操作指令,作用是根據(jù)條件決定在最后的可執(zhí)行文件中包不包括某一段代碼。這和.if/.else/.endif構(gòu)成分支的偽指令完全是兩回事情。
循環(huán)語(yǔ)句
循環(huán)是重復(fù)執(zhí)行的一組指令,MASM的循環(huán)偽指令可以根據(jù)條件表達(dá)式的真假來(lái)控制循環(huán)是否繼續(xù),也可以在循環(huán)體中直接退出,使用循環(huán)的語(yǔ)法是:
.while 條件測(cè)試表達(dá)式
指令
[.break [.if 退出條件]]
[.continue]
.endw
或
.repeat
指令
[.break [.if 退出條件]]
[.continue]
.until 條件測(cè)試表達(dá)式 (或.untilcxz [條件測(cè)試表達(dá)式])
.while/.endw循環(huán)首先判斷條件測(cè)試表達(dá)式,如果結(jié)果是真,則執(zhí)行循環(huán)體內(nèi)的指令,結(jié)束后再回到.while處判斷表達(dá)式,如此往復(fù),一直到表達(dá)式結(jié)果為假為止。.while/.endw指令有可能一遍也不會(huì)執(zhí)行到循環(huán)體內(nèi)的指令,因?yàn)槿绻谝淮闻袛啾磉_(dá)式時(shí)就遇到結(jié)果為假的情況,那么就直接退出循環(huán)。
.repeat/.until循環(huán)首先執(zhí)行一遍循環(huán)體內(nèi)的指令,然后再判斷條件測(cè)試表達(dá)式,如果結(jié)果為真的話,就退出循環(huán),如果為假,則返回.repeat處繼續(xù)循環(huán),可以看出,.repeat/.until不管表達(dá)式的值如何,至少會(huì)執(zhí)行一遍循環(huán)體內(nèi)的指令。
也可中以把條件表達(dá)式直接設(shè)置為固定值,這樣就可以構(gòu)建一個(gè)無(wú)限循環(huán),對(duì)于.while/.end直接使用TRUE,對(duì)于.repeat/until直接使用FALSE來(lái)當(dāng)表達(dá)式就是如此,這種情況下,可以使用.break偽指令強(qiáng)制退出循環(huán),如果.break偽指令后面跟一個(gè).if測(cè)試偽指令的話,那么當(dāng)退出條件為真時(shí)才執(zhí)行.break偽指令。
在循環(huán)體中也可以用.continue偽指令忽略以后的指令,遇到.continue偽指令時(shí),不管下面還有沒(méi)有其他循環(huán)體中的指令,都會(huì)直接回到循環(huán)頭部開(kāi)始執(zhí)行。
代碼風(fēng)格
隨著程序功能的增加和版本的提高,程序越來(lái)越復(fù)雜,源文件也越來(lái)越多,風(fēng)格規(guī)范的源程序會(huì)對(duì)軟件的升級(jí)、修改和維護(hù)帶來(lái)極大的方便,要想開(kāi)發(fā)一個(gè)成熟的軟件產(chǎn)品,必須在編寫(xiě)源程序的時(shí)候就有條不紊,細(xì)致嚴(yán)謹(jǐn)。
在編程中,在程序排版、注釋、命名和可讀性等問(wèn)題上都有一定的規(guī)范,雖然編寫(xiě)可讀性良好的代碼并不是必然的要求,但好的代碼風(fēng)格實(shí)際上是為自己將來(lái)維護(hù)和使用這些代碼節(jié)省時(shí)間。
下面是對(duì)匯編語(yǔ)言代碼風(fēng)格的建議。
變量和函數(shù)的命名
匈牙利表示法
匈牙利表示法主要用在變量和子程序的命名,這是現(xiàn)在大部分程序員都在使用的命名約
定。匈牙利表示法這個(gè)奇怪的名字是為了紀(jì)念匈牙利籍的Microsoft 程序員Charles
Simonyi,他首先使用了這種命名方法。
匈牙利表示法用連在一起的幾個(gè)部分來(lái)命名一個(gè)變量,格式是類(lèi)型前綴加上變量說(shuō)明,類(lèi)型用小寫(xiě)字母表示,如用h表示句柄,用dw表示double word,用sz表示以0結(jié)尾的字符串等,說(shuō)明則用首字母大寫(xiě)的幾個(gè)英文單詞組成,如TimeCounter,NextPoint等,可以令人一眼看出變量的含義來(lái),在匯編語(yǔ)言中常用字的類(lèi)型前綴有:
b 表示byte
w 表示word
dw 表示dword
h 表示句柄
lp 表示指針
sz 表示以0結(jié)尾的字符串
lpsz 表示指向以0結(jié)尾的字符串的指針
f 表示浮點(diǎn)數(shù)
st 表示一個(gè)數(shù)據(jù)結(jié)構(gòu)
這樣一來(lái),變量的意思就很好理解:
hWinMain 主窗口的句柄
dwTimeCount 時(shí)間計(jì)數(shù)器,以雙字定義
szWelcome 歡迎信息字符串,以0結(jié)尾
lpBuffer 指向緩沖區(qū)的指針
很明顯,這些變量名比count1,abc,commandlinebuffer和FILEFLAG之類(lèi)的命名要易于理解。由于匈牙利表示法既描述了變量的類(lèi)型,又描述了變量的作用,所以能幫助程序員及早發(fā)現(xiàn)變量的使用錯(cuò)誤,如把一個(gè)數(shù)值當(dāng)指針來(lái)使用引發(fā)的內(nèi)存頁(yè)錯(cuò)誤等。
對(duì)于函數(shù)名,由于不會(huì)返回多種類(lèi)型的數(shù)值,所以命名時(shí)一般不再用類(lèi)型開(kāi)頭,但名稱(chēng)還是用表示用途的單詞組成,每個(gè)單詞的首字母大寫(xiě)。Windows API是這種命名方式的絕好例子,當(dāng)人們看到ShowWindow,GetWindowText,DeleteFile和GetCommandLine之類(lèi)的API函數(shù)名稱(chēng)時(shí),恐怕不用查手冊(cè),就能知道它們是做什么用的。比起int 21h/09h和int 13h/02h之類(lèi)的中斷調(diào)用,好處是不必多講的。
對(duì)匈牙利表示法的補(bǔ)充
使用匈牙利表示法已經(jīng)基本上解決了命名的可讀性問(wèn)題,但相對(duì)于其他高級(jí)語(yǔ)言,匯編語(yǔ)言有語(yǔ)法上的特殊性,考慮下面這些匯編語(yǔ)言特有的問(wèn)題:
·對(duì)局部變量的地址引用要用lea指令或用addr偽操作,全局變量要用offset;對(duì)局部變量的使用要特別注意初始化問(wèn)題。如何在定義中區(qū)分全局變量、局部變量和參數(shù)?
·匯編的源代碼占用的行數(shù)比較多,代碼行數(shù)很容易膨脹,程序規(guī)模大了如何分清一個(gè)函數(shù)是系統(tǒng)的API還是本程序內(nèi)部的子程序?
實(shí)際上上面的這些問(wèn)題可以歸納為區(qū)分作用域的問(wèn)題。為了分清變量的作用域,命名中對(duì)全局變量、局部變量和參數(shù)應(yīng)該有所區(qū)別,所以我們需要對(duì)匈牙利表示法做一些補(bǔ)充,以適應(yīng)Win32匯編的特殊情況,下面的補(bǔ)充方法僅供參考:
·全局變量的定義使用標(biāo)準(zhǔn)的匈牙利表示法,在參數(shù)的前面加下劃線,在局部變量的前面加@符號(hào),這樣引用的時(shí)候就能隨時(shí)注意到變量的作用域。
·在內(nèi)部子程序的名稱(chēng)前面加下劃線,以便和系統(tǒng)API區(qū)別。
如下面是一個(gè)求復(fù)數(shù)模的子程序,子程序名前面加下劃線表示這是本程序內(nèi)部模塊,兩個(gè)參數(shù)——復(fù)數(shù)的實(shí)部和虛部用_dwX和_dwY表示,中間用到的局部變量@dwResult則用@號(hào)開(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í)話,上面這段Win32匯編子程序,我只能看懂20%。看了一個(gè)月的匯編了,痛哉!痛哉!)
代碼的書(shū)寫(xiě)格式
排版方式
程序的排版風(fēng)格應(yīng)該遵循以下規(guī)則。
首先是大小寫(xiě)的問(wèn)題,匯編程序中對(duì)于指令和寄存器的書(shū)寫(xiě)是不分大小寫(xiě)的,但小寫(xiě)代碼比大寫(xiě)代碼便于閱讀,所以程序中的指令和寄存器等要采用小寫(xiě)字母,而用equ偽操作符定義的常量則使用大寫(xiě),變量和標(biāo)號(hào)使用匈牙利表示法,大小寫(xiě)混合。
其次是使用Tab的問(wèn)題。匯編源程序中Tab的寬度一般設(shè)置為8個(gè)字符。在語(yǔ)法上,指令和操作數(shù)之間至少有一個(gè)空格就可以了,但指令的助記符長(zhǎng)度是不等長(zhǎng)的,用Tab隔開(kāi)指令和操作數(shù)可以使格式對(duì)齊,便于閱讀。如:
xor eax,eax
fistp dwNumber
xchg eax,ebx
上述代碼的寫(xiě)法就不如下面的寫(xiě)法整齊:
xor eax,eax
fistp dwNumber
xchg eax,ebx
還有就是縮進(jìn)格式的問(wèn)題。程序中的各部分采用不同的縮進(jìn),一般變量和標(biāo)號(hào)的定義不縮進(jìn),指令用兩個(gè)Tab縮進(jìn),遇到分支或循環(huán)偽指令再縮進(jìn)一格,如:
.data
dwFlag dd ?
.code
start:
mov eax,dwFlag
.if dwFlag == 1
call _Function1
.else
call _Function2
.endif
合適的縮進(jìn)格式可以明顯地表現(xiàn)出程序的流程結(jié)構(gòu),也很容易發(fā)現(xiàn)嵌套錯(cuò)誤,當(dāng)縮進(jìn)過(guò)多的時(shí)候,可以意識(shí)到嵌套過(guò)深,該改進(jìn)程序結(jié)構(gòu)了。
注釋和空行
沒(méi)有注釋的程序是很難維護(hù)的,但注釋的方法也很有講究,寫(xiě)注釋要遵循以下的規(guī)則:
·不要寫(xiě)無(wú)意義的注釋?zhuān)纾簩?/span>1放到eax中,跳轉(zhuǎn)到 exit標(biāo)號(hào)處。
·修改代碼同時(shí)修改相應(yīng)的注釋?zhuān)员WC注釋與代碼的一致性。
·注釋以描寫(xiě)一組指令實(shí)現(xiàn)的功能為主,不要解釋單個(gè)指令的用法,那是應(yīng)該由指令手冊(cè)來(lái)完成的,不要假設(shè)看程序的人連指令都不熟悉。
·對(duì)于子程序,要在頭部加注釋說(shuō)明參數(shù)和返回值,子程序可以實(shí)現(xiàn)的功能,以及調(diào)用時(shí)應(yīng)該注意的事項(xiàng)。
由于匯編語(yǔ)言是以一條指令為一行的,實(shí)現(xiàn)一個(gè)小功能就需要好幾行,沒(méi)有分段的程序很難看出功能模塊來(lái),所以要合理利用空行來(lái)隔開(kāi)不同的功能塊,一般以在高級(jí)語(yǔ)言中可以用一句語(yǔ)句來(lái)完成的一段匯編指令為單位插入一個(gè)空行。
避免使用宏
在MASM的宏功能中最好只使用條件匯編,用來(lái)選擇編譯不同的代碼塊來(lái)構(gòu)建不同的版本,其他如宏定義和宏調(diào)用只會(huì)破壞程序的可讀性,能夠不用就盡量不用,雖然展開(kāi)后只有一兩句的宏定義不在此列,但既然展開(kāi)后也只有一兩句,那么和直接使用指令也就沒(méi)有什么區(qū)別了。
在匯編中避免使用宏定義的理由是:匯編中隨時(shí)要用到各個(gè)寄存器,宏定義不同于子程序,可以有選擇地保護(hù)現(xiàn)場(chǎng),在使用中很容易忽略里面用了哪個(gè)寄存器,從而對(duì)程序結(jié)構(gòu)構(gòu)成威脅。高級(jí)語(yǔ)言的宏定義則不會(huì)有這個(gè)問(wèn)題。
最極端的使用宏定義的程序是MicroMedia的Director SDK,100行左右的例子中幾乎有90%都是宏定義,雖然例子很容易改成其他功能的程序,但要在里面加新的功能則幾乎是不可能的,因?yàn)槌绦蛑羞BC語(yǔ)言函數(shù)開(kāi)始和結(jié)束的花括號(hào)都被改成了宏定義,這樣一來(lái),如果要真正使用這個(gè)開(kāi)發(fā)包,則必須把宏定義“翻譯”回原來(lái)的樣子才能真正理解程序的流程。
代碼的組織
程序中要注意變量的組織和模塊的組織方式。
過(guò)多的全局變量會(huì)影響程序的模塊化結(jié)構(gòu),所以不要設(shè)置沒(méi)必要的全局變量,盡量把變量定義成局部變量。
把僅在子程序中使用的變量設(shè)置為局部變量可以使子程序更容易封裝成一個(gè)黑匣子,如果無(wú)法把全部變量設(shè)置為局部變量,則盡量把這些數(shù)據(jù)改為參數(shù)輸入輸出,如果無(wú)法改為參數(shù),那么意味著這個(gè)子程序不能不經(jīng)修改地直接放到別的程序中使用。
在主程序中使用比較頻繁的部分,以及便于封裝成黑匣子在別的程序上用的代碼,都應(yīng)該寫(xiě)上子程序,但一個(gè)子程序的規(guī)模不應(yīng)該太大,行數(shù)盡量限制在幾百行之內(nèi),功能則限于完成單個(gè)功能。對(duì)于子程序,定義參數(shù)的時(shí)候要盡可能精簡(jiǎn),對(duì)可能引起程序崩潰的參數(shù),如指針等,要進(jìn)行合法性檢測(cè)。
子程序中在使用完申請(qǐng)的資源的時(shí)候,注意在退出前要釋放所用資源,包括申請(qǐng)的內(nèi)存和其他句柄等,對(duì)于打開(kāi)的文件則要關(guān)閉。
對(duì)于程序員來(lái)說(shuō),開(kāi)發(fā)每一個(gè)軟件都是要從頭做起是很浪費(fèi)時(shí)間的,一般的做是從自己以前做的程序中拷貝相似的代碼,但修改還是要花一定時(shí)間,最好的辦法就是盡量把子程序做成一個(gè)黑匣子,可以不經(jīng)修改地直接拿過(guò)來(lái)用,這樣,每次編程相當(dāng)于只是編寫(xiě)新增的部分,隨著代碼的積累,開(kāi)發(fā)任何程序都將是很快的事情。