青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

luqingfei@C++

為中華之崛起而崛起!
兼聽則明,偏聽則暗。

Win32匯編--使用MASM

 

使用MASM

 

Win32匯編源程序的結(jié)構(gòu)

任何種類的語言,總是有基本的源程序結(jié)構(gòu)規(guī)范。

下面以經(jīng)典的Hello World程序為例,展示一個C語言、DOS匯編、Win32匯編三種寫法。同學(xué)位好好體會一下。

如果沒有匯編基礎(chǔ),建議看一下王爽老師的《匯編語言》這本書。

 

C語言中的HelloWorld程序:

#include <stdio.h>

main()

{

    printf(“Hello, world\n”);

}

像這樣的一個程序,就說明了C語言中最基本的格式,main()中的括號和下面的花括號說明了一個函數(shù)的定義方法,printf語句說明了一個函數(shù)的調(diào)用方法,調(diào)用函數(shù)語句后面的分號也是基本的格式。C是一種高級語言,在C源程序中,不必為堆棧段、數(shù)據(jù)段和代碼段的定義而擔(dān)心,編譯器會把程序中的字符串和語句代碼分別放到它們該去的地方,程序開始執(zhí)行的時候也會自己找到main()函數(shù)。而匯編是低級語言,必須為所有的東西找到它們該去的地方,所以在DOS的匯編中,Hello World又長成了這樣一副模板:

;分號后面是注釋

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

; 堆棧段

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

stack segment

       db 100 dup (?) ;定義100個字節(jié)的內(nèi)存存儲單元空間,默認(rèn)值為?

stack ends

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

; 數(shù)據(jù)段

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

data segment

       szHello db ‘Hello, world’,0dh,0ah,’$’ 

       ;szHello為數(shù)據(jù)標(biāo)號,它標(biāo)記了存儲數(shù)據(jù)的單元的地址和長度。

       ;天哪,這太像高級語言中的變量了!!!

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

在這個源程序中,stack段為堆棧找了個家,hello world字符串則跑到數(shù)據(jù)段中去了,代碼則放在代碼段中,程序的開始語句必須由最后一句end start來說明應(yīng)該從start這個標(biāo)號開始執(zhí)行,整個程序在使用過DOS匯編的程序員眼里是非常的熟悉。(一個月前我不熟悉,現(xiàn)在我熟悉了。感謝王爽老師。)

 

到了Win32匯編的時候,程序的基本結(jié)構(gòu)還是如此,先來看一看這個看起來很新鮮的Win32Hello 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

怎么樣,看來和上面的C以及DOS匯編又不同了吧!但從include, .data.code等語句,顧名思義,也能看出一點苗頭來,include應(yīng)該就是包含別的文件,.data想必是數(shù)據(jù)段,.code應(yīng)該就是代碼段了吧!接下來通過這個例子程序逐段介紹Win32匯編程序的結(jié)構(gòu)。

 

模式定義

程序的第一部分是模式和源程序格式的定義語句:

                            .386

                            .model flat,stdcall

                            option casemap:none

這些指令定義了程序使用的指令集、工作模式和格式。

1)指定使用的指令集

.386語句是匯編語句的偽指令,它在低版本的宏匯編中就已經(jīng)存在,類似的指令還有:.8086.186.286.386/.386p.486/..486p.586/.586p等,用于告訴編譯器在本程序中使用的指令集。在DOS的匯編中默認(rèn)使用的是8086指令集,那時候如果在源程序中寫入80386所特有的指令或使用32位的寄存器就會報錯,為了在DOS環(huán)境下進(jìn)行保護(hù)模式編程或僅為了使用32位寄存器,常在DOS的匯編中使用.386來定義。Win32環(huán)境工作在80386及以上的處理器中,所以這一句.386是必不可少的。

后面帶p的偽指令則表示程序中可以使用特權(quán)指令,如:

mov cr0,eax

這一類指令必須在特權(quán)級0上運行,如果只指定.386,那么使用普通的指令是可以的,編譯時到這一句就會報錯,如果我們要寫的程序是VxD等驅(qū)動程序,中間要用到特權(quán)指令,那么必須定義.386p,在應(yīng)用程序級別的Win32編程中,程序都是運行在優(yōu)先級3上,不會用到特權(quán)指令,只需定義.386就夠了。80486Pentium處理器指令是80386處理器指令的超集,同樣道理,如果程序中要用80486處理器或Pentium處理器的指令,則必須定義.486.586

 

另外,Intel公司的80x86系列處理器從Pentium MMX開始增加了MMX指令集,為了使用MMX指令,除了定義.586之外,還要加上一句.mmx偽指令:

                            .386

                            .mmx

 

2model語句

.model語句在低版本的宏匯編中已經(jīng)存在,用來定義程序工作的模式,它的使用方法是:

                            .model 內(nèi)存模式 [,語言模式] [,其他模式]

內(nèi)存模式的定義影響最后生成的可執(zhí)行文件,可執(zhí)行文件的規(guī)模從小到大,可以有很多種類型,在DOS的可執(zhí)行程序中,有只用到64KB.com文件,也有大大小小的.exe文件。到了Win32環(huán)境下,又有了可以用4GB內(nèi)存的PE格式可執(zhí)行文件,編寫不同類型的可執(zhí)行文件要用.model語句定義不同的參數(shù),具體如下 表所示。

                                   內(nèi)存模式

模式

內(nèi)存使用方式

tiny

用來建立.com文件,所有的代碼、數(shù)據(jù)和堆棧都在同一個64KB段內(nèi)

small

建立代碼和數(shù)據(jù)分別用一個64KB段的.exe文件

medium

代碼段可以有多個64KB段,數(shù)據(jù)段只有一個64KB

compact

代碼段只有一個64KB,數(shù)據(jù)段可以有多個64KB

large

代碼段和數(shù)據(jù)段都可以有多個64KB

huge

large,并且數(shù)據(jù)段中的一個數(shù)組也可以超過64KB

float

Win32程序使用的模式,代碼和數(shù)據(jù)使用同一個4GB

Windows 程序運行在保護(hù)模式下,系統(tǒng)把每一個Win32應(yīng)用程序都放到分開的虛擬地址空間中去運行,也就是說,每一個應(yīng)用程序都擁有其相互獨立的4GB地址空間,對Win32程序來說,只有一種內(nèi)存模式,即flat(平坦)模式,意思是內(nèi)存是很平坦地從0延伸到4GB,再沒有64KB段大小限制。對比一下DOSHello WorldWin32Hello World開始部分的不同,DOS程序中有這樣語句:

                            mov ax,data

                            mov ds,ax

意思是把數(shù)據(jù)段寄存器DS指向data數(shù)據(jù)段,data數(shù)據(jù)段在前面已經(jīng)用data segment語句定義,只要DS不重新設(shè)置,那么從此以后指令中涉及的數(shù)據(jù)默認(rèn)將從data數(shù)據(jù)段中取得,所以下面的語句是從data數(shù)據(jù)段取出szHello字符串的地址后再顯示:

                            mov ah,9

                            mov dx,offset szHello

                            int 21h

縱觀Win32匯編的源程序,沒有一處可以找到dses等段寄存器的使用,因為所有的4GB空間用32位的寄存器全部都能訪問到了,不必在頭腦中隨時記著當(dāng)前使用的是哪個數(shù)據(jù)段,這就是平坦內(nèi)存模式帶來的好處。

 

如果定義了.model flatMASM自動為各種段寄存器做了如下定義:

ASSUME cs:FLAT,ds:FLAT,ss:FLAT,es:FLAT,fs:ERROR,gs:ERROR

也就是說,CSDSSSES段全部使用平坦模式,FSGS寄存默認(rèn)不使用,這時若在源程序中使用FSGS,在編譯時會報錯。如果有必要使用它們,只需在使用前用下面的語句聲明一下就可以了:

assume fs:nothing,gs:nothing 或者 assume fs:flat,gs:flat

 

Win32匯編中,.model語句中還應(yīng)該指定語言模式,即子程序和調(diào)用方式,例子中用的是stdcall,它指出了調(diào)用子程序或Win32 API時參數(shù)傳遞的次序和堆棧平衡的方法,相對于stdcall,不同的語言類型還有C,SysCall,BASIC,FORTRANPASCALL,雖然各種高級語言在調(diào)用子程序時都是使用堆棧來傳遞參數(shù)。WindowsAPI調(diào)用使用是的stdcall格式,所以在Win32匯編中沒有選擇,必須在.model中加上stdcall參數(shù)。

 

3option語句

                            option casemap:none

option語句定義的選項有很多,如option language定義和option segment定義等,在Win32匯編程序中,需要的只是定義option casemap:none,這個語句定義了程序中的變量和子程序名是否對大小寫每感,由于Win32 API中的API名稱是區(qū)分大小寫的,所以必須指定這個選項,否則在調(diào)用API的時候會有問題。

 

 

 

段的定義

段的概念

把上面的Win32Hello World源程序中的語句歸納精簡一下,再列在下面:

       .386

       .model flat,stdcall

       option casemap:none

       <一些include語句>

       .data

       <一些字符串、變量定義>

       .code

              <代碼>

              <開始標(biāo)號>

                     <其他語句>

       end 開始標(biāo)號

模式定義中的模式、選項等定義并不會在編譯好的可執(zhí)行程序中產(chǎn)生什么東西,它們只是說明,而真正的數(shù)據(jù)和代碼是定義在各個段中的,如上面的.data段和.code段,考慮到不同的數(shù)據(jù)類型,還可以有其他種類的數(shù)據(jù)段,下面是包含全部段的源程序結(jié)構(gòu):

       .386

       .model flat,stdcall

       option casemap:none

       <一些include語句>

       .stack [堆棧段的大小]

       .data

       <一些初始化過的變量定義>

       .data?

       <一些沒有初始化過的變量定義>

       .const

       <一些常量定義>

       .code

              <代碼>

              <開始標(biāo)號>

                     <其他語句>

       end 開始標(biāo)號

.stack.data.data?.const.code是分段偽指令,Win32中實際上只有代碼和數(shù)據(jù)之分,.data.data?.const是數(shù)據(jù)段,.code是代碼段,和DOS匯編不同,Win32匯編不必考慮堆棧,系統(tǒng)會為程序分配一個向下擴(kuò)展的、足夠大的段作為堆棧段,所以.stack段定義常常被忽略。

 

注意,前面不是說過Win32環(huán)境下不用段了嗎?是的,這些“段”,實際上并不是DOS匯編中那種意義的段,而是內(nèi)存的“分段”。上一個段的結(jié)束就是下一個段的開始,所有的分段,合起來,包括系統(tǒng)使用的地址空間,就組成了整個可以尋址的4GB空間。Win32匯編的內(nèi)存管理使用了80386處理器的分頁機制,每個頁(4KB大小)可以自由指定屬性,所以上一個4KB可能是代碼,屬性是可執(zhí)行但不可寫,下一個4KB就有可能是既可讀也可寫但不可執(zhí)行的數(shù)據(jù),再下面呢?有可能是可讀不可寫也不可執(zhí)行的數(shù)據(jù)。Win32匯編源程序中“分段”的概念實際上是把不同類型的數(shù)據(jù)或代碼歸類,再放到不同屬性的內(nèi)存頁(也就是不同的“分段”)中,這中間不涉及使用不同的段選擇器。雖然使用和DOS匯編同樣的.code.data語句來定義,意思可是完全不同了!

 

 

數(shù)據(jù)段

.data.data?.const定義的是數(shù)據(jù)段,分別對應(yīng)不同方式的數(shù)據(jù)定義,在最后生成的可執(zhí)行文件中也分別放在不同的節(jié)區(qū)(Section)中。程序中的數(shù)據(jù)定義一段可以歸納為3類:

1)第一類是可讀可寫的已定義變量。這些數(shù)據(jù)在源程序中已經(jīng)被定義了初始值,而且在程序的執(zhí)行中有可能被更改,如一些標(biāo)志等,這些數(shù)據(jù)必須定義在.data段中,.data段是已初始化數(shù)據(jù)段,其中定義的數(shù)據(jù)是可讀可寫的,在程序裝入完成的時候,這些值就已經(jīng)在內(nèi)存中了,.data段存放在可執(zhí)行文件的_DATA節(jié)區(qū)內(nèi)。

 

2)第二類是可讀可寫的未定義變量。這些變量一般是當(dāng)做緩沖區(qū)或者在程序執(zhí)行后才開始使用的,這些數(shù)據(jù)可以定義在.data段中,也可以定義在.data?段中,但一般把它放到.data?段中。雖然定義在這兩種段中都可以正常使用,但定義在.data?段中不會增大.exe文件的大小。舉例說明,如果要用到一個100KB的緩沖區(qū),可以在數(shù)據(jù)段中定義:

       szBuffer         db           100 * 1024 dup (?)

如果放在.data段中,編譯器認(rèn)為這些數(shù)據(jù)在程序裝入時就必須有效,所以它在生成可執(zhí)行文件的時候保留了所有的100KB的內(nèi)容,即使它們是全零!如果程序其他部分的大小是50KB,那么最后的.exe文件就會是150KB大小,如果緩沖區(qū)定義為1MB,那么.exe文件會增大到1050KB.data?段則不同,其中的內(nèi)容編譯器會認(rèn)為程序在開始執(zhí)行后才會用到,所以在生成可執(zhí)行文件的時候只保留了大小信息,不會為它浪費磁盤空間。和上面同樣的情況下,即使緩沖區(qū)定義為1MB,可執(zhí)行文件同樣只有50KB!總之,.data?段是未初始化數(shù)據(jù)段,其中的數(shù)據(jù)也是可讀可寫的,但在可執(zhí)行文件中不占空間,.data?段在可執(zhí)行文件中存放在_BSS節(jié)區(qū)中。

 

3)第三類數(shù)據(jù)是一些常量。如一些要顯示的字符串信息,它們在程序裝入的時候也已經(jīng)有效,但在整個執(zhí)行過程中不需要修改,這些數(shù)據(jù)可以放在.const段中,.const段是常量段,它是可讀不可寫的。一般為了方便起見,在小程序中常常把常量一起定義到.data段中,而不另外定義一個.const段。在程序中如果不小心寫了對.const段中的數(shù)據(jù)做寫操作的指令,會引起保護(hù)錯誤,Windows會顯示一個提示框并結(jié)束程序。

Hello.exe – 應(yīng)用程序錯誤

“0x00401000”指令引用 ”0x00402010”內(nèi)存。該內(nèi)存不能為”written”

要終止程序,請單擊確定

要調(diào)試程序,請單擊取消

 

如果不怕程序可讀性不佳的話,把.const段中定義的東西混到.code段中去也可以正常使用,因為.code段也是可以讀的。

 

 

代碼段

.code段是代碼段,所有的指令都必須寫在代碼段中,在可執(zhí)行文件中,代碼段是放在_TEXT節(jié)區(qū)中的。Win32環(huán)境中的數(shù)據(jù)段是不可執(zhí)行的,只有代碼段有可執(zhí)行的屬性。對于工作在特權(quán)級3的應(yīng)用程序來說,.code段是不可寫的,在編寫DOS匯編程序的時候,好事的程序員往往有個習(xí)慣,就是靠改動代碼段中的代碼來做一些反跟蹤的事情,如果企圖在Win32匯編下做同樣的事情,結(jié)果就是和上面同樣 “非法操作”。

 

當(dāng)然事物總有兩面性,在Windows95下,在特權(quán)級0下運行的程序?qū)λ械亩味加凶x寫的權(quán)利,包括代碼段。另外,在優(yōu)先級3下運行的程序也不是一定不能寫代碼段,代碼段的屬性是由可執(zhí)行文件PE頭部中的屬性位決定的,通過編輯磁盤上的.exe文件,把代碼段屬性位改成可寫,那么在程序中就允許修改自己的代碼段。一個典型的應(yīng)用就是一些針對可執(zhí)行文件的壓縮軟件和加殼軟件,如UpxPeCompact等,這些軟件靠把代碼段進(jìn)行變換來達(dá)到解壓縮和解密的目的,被處理過的可執(zhí)行文件在執(zhí)行時需要由解壓代碼來將代碼段解壓縮,這就需要寫代碼段,所以這些軟件對可執(zhí)行文件代碼段的屬性預(yù)先做修改。

 

 

程序結(jié)束和程序入口

C語言源程序中,程序不必顯式地指定程序由哪里開始執(zhí)行,編譯器已經(jīng)約定好從main()函數(shù)開始執(zhí)行了。而在匯編程序中,并沒有一個main函數(shù),程序員可以指定從代碼段的任何一個地方開始執(zhí)行,這個地方由程序最后一句的end語句來指定:

       end [開始地址]

這句語句同時表示源程序結(jié)束,所有的代碼必須在end語句之前。

       end start

上述語句指定程序從start這個標(biāo)號開始執(zhí)行。當(dāng)然,start標(biāo)號必須在程序的代碼段中有所定義。

 

但是,一個源程序不必非要指定入口標(biāo)號,這時候可以把開始地址忽略不寫,這種情況發(fā)生在編寫多模塊程序的單個模塊的時候。當(dāng)分開寫多個程序模塊時,每個模塊的源程序中也可以包括.data.data?.const.code段,結(jié)構(gòu)就和上面的Win32 Hello World一樣,只是其他模塊最后的end語句必須不帶開始地址。當(dāng)最后把多個模塊鏈接在一起的時候,只能有一個主模塊指定入口地址,在多個模塊中指定入口地址或者沒有一個模塊指定了入口地址,鏈接程序都會報錯。

 

 

注釋和換行

注釋是源程序中不可忽略的一部分,匯編源程序的注釋以分號(;)開始,注釋既可以在一行的頭部,也可以在一行的中間,一行中所有在分號之后的字符全部當(dāng)做注釋處理,但在字符串的字義中包含的引號內(nèi)的分號不當(dāng)做是注釋的開始。

       ;這里是注釋

       call _PrintChar             ;這里是注釋

       szChar    db    ‘Hello, world; ’,0dh,0ah               ;world后面的分號不是注釋,后面的才是

 

當(dāng)源程序的某一行過長,不利于閱讀的時候,可以分行書寫,分行的辦法是在一行的最后用反斜杠(\)做換行符,如:

       invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK      

可以寫為:

       invoke MessageBox, \

                     NULL, \                       ;父窗口句柄

                     offset szText, \                     ;消息框中的文字

                     offset szCaption, \         ;標(biāo)題文字

                     MB_OK  

一行的最后,指的是最后一個有用的字符,反斜杠后面多幾個空格或加上注釋并不影響換行符的使用,如上例所示,這一點和makefile文件中換行符的規(guī)定有所不同。

 

 

 

調(diào)用API

API是什么?

Win32程序是構(gòu)筑在Win32 API基礎(chǔ)上的。在Win32 API中,包括了大量的函數(shù)、結(jié)構(gòu)和消息等,它不僅為應(yīng)用程序所調(diào)用,也是Windows自身的一部分,Windows自身的運行也調(diào)用這些API函數(shù)。

 

DOS下,操作系統(tǒng)的功能是通過各種軟中斷來實現(xiàn)的,如大家都知道int 21hDOS中斷,int 13hint 10hBIOS中的磁盤中斷和視頻中斷。當(dāng)應(yīng)用程序要引用系統(tǒng)功能時,要把相應(yīng)的參數(shù)放在各個寄存器中再調(diào)用相應(yīng)的中斷,程序控制權(quán)轉(zhuǎn)到中斷中去執(zhí)行,完成以后會通過iret中斷返回指令回到應(yīng)用程序中。如DOS匯編下的Hello World程序中有下列語句:

mov ah,9

mov dx,offset szHello

int 21h

3條語句調(diào)用DOS系統(tǒng)模塊中的屏幕顯示功能,功能號放在ah中,9號功能表示屏幕顯示,要輸出到屏幕上的內(nèi)容的地址放在dx中,然后去調(diào)用int 21h,字符串就會顯示到屏幕上。

 

這個例子說明了應(yīng)用程序調(diào)用系統(tǒng)功能的一般過程。首先,系統(tǒng)提供功能模塊并約定參數(shù)的定義方法,同時約定調(diào)用的方式,同時約定調(diào)用的方式,應(yīng)用程序按照這個約定來調(diào)用系統(tǒng)功能。在這里,ah中放功能號9dx中放字符串地址就是約定的參數(shù),int 21h是約定的調(diào)用方式。

 

下面來看看這種方法的不便這處。首先,所有的功能號定義是冷冰冰的數(shù)字,int 21h的說明文檔是這樣的:

       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ù)定義僅從字面上看,是看不出一點頭緒來的。

 

另外,80x86系列處理器能處理的中斷最多只能有256個,不同的系統(tǒng)服務(wù)程序使用了不同的中斷號,這少得可憐的中斷數(shù)量就顯得太少了,結(jié)果到最后是中斷掛中斷,大家搶來搶去的,把好好的一個系統(tǒng)搞得像接力賽跑一樣。

 

對于這些弱點,程序員們都有個愿望:系統(tǒng)功能如果能以功能名作為子程序名直接調(diào)用就好了,參數(shù)也最好定義的有意義一點,這樣一來寫程序就會方便得多,編系統(tǒng)擴(kuò)展模塊也就不必老是擔(dān)心往哪個中斷上面掛了,最好能把上面int 21h/ah=9的調(diào)用寫成下面這副樣子:

call PrintString, addr szHello

 

終于,好消息出來了,Win32環(huán)境中的編程接口就是這個樣子,這就是API,它實際上是以一種新的方法代替了DOS中用軟中斷的方式。和DOS的結(jié)構(gòu)相比,Win32的系統(tǒng)功能模塊放在Windows的動態(tài)鏈接庫(DLL)中,DLL是一種Windows的可執(zhí)行文件,采用的是和.exe文件同樣的PE格式,在PE格式文件頭的導(dǎo)出表中,以字符串形式指出了這個DLL能提供的函數(shù)列表。應(yīng)用程序使用字符串類型的函數(shù)名指定要調(diào)用的函數(shù)。

 

應(yīng)用程序在使用的時候由Windows自動載入DLL程序并調(diào)用相應(yīng)的函數(shù)。

 

實際上,Win32的基礎(chǔ)就是由DLL組成的。Win32 API的核心由3DLL提供,它們是:

KERNEL32.DLL——系統(tǒng)服務(wù)功能。包括內(nèi)存管理、任務(wù)管理和動態(tài)鏈接等。

GDI32.DLL——圖形設(shè)備接口。利用VGADRV之類的顯示設(shè)備驅(qū)動程序完成顯示文本和矩形等功能。

USER32.DLL——用戶接口服務(wù)。建立窗口和傳送消息等。

 

當(dāng)然,Win32 API還包括其他很多函數(shù),這些也是由DLL提供的,不同的DLL提供了不同的系統(tǒng)功能。如使用TCP/IP協(xié)議進(jìn)行網(wǎng)絡(luò)通信的DLLWsock32.dll,它所提供的API稱為Socket API;專用于電話服務(wù)方面的API稱為TAPITelephony API),包含在Tapi32.dll中,所有的這些DLL提供的函數(shù)組成了現(xiàn)在使用的Win32編程環(huán)境。

 

 

調(diào)用API

和在DOS中用中斷方式調(diào)用系統(tǒng)功能一樣,用API方式調(diào)用存放在DLL中的函數(shù)必須同樣約定一個規(guī)范,用來定義函數(shù)的調(diào)用方法、參數(shù)的傳遞方法和參數(shù)的定義,洋洋灑灑幾百MBWindows系統(tǒng)比起才幾百KB規(guī)模的DOS,其系統(tǒng)函數(shù)的規(guī)模和復(fù)雜程度都上了一個數(shù)量級,所在使用一個API時,帶的參數(shù)數(shù)量多達(dá)十幾個是常有的事,在DOS下用寄存來傳遞參數(shù)的方法顯然已經(jīng)不能勝任了。

 

Win32 API是用堆棧來傳遞參數(shù)的,調(diào)用者把參數(shù)一個個壓入堆棧,DLL中的函數(shù)程序再從堆棧中取出參數(shù)處理,并在返回之前將堆棧中已經(jīng)無用的參數(shù)丟棄。在Microsoft發(fā)布的《Microsoft Win32 Programmer’s Reference》中定義了常用API的參數(shù)和函數(shù)聲明,先來看消息框函數(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

              );

最后還有一句說明:

Library: Use User32.lib

上述函數(shù)聲明說明了MessageBox4個參數(shù),它們分別是HWND類型的窗口句柄(hWnd),LPCTSTR類型的要顯示的字符串地址(lpText)和標(biāo)題字符串地址(lpCaption),還有UINT類型的消息框類型(uType)。這些數(shù)據(jù)類型看起來很復(fù)雜,但有一點是很重要的,對于匯編語言來說,Win32環(huán)境中的參數(shù)實際上只有一種類型,那就是一個32位的整數(shù),所以這些HWNDLPCTSTRUINT實際上就是匯編中的dworddouble word,雙字型,4個字節(jié),兩個字,32位),之所以定義為不同的模樣,是用來說明了用途。由于Windows是用C寫成的,世界上的程序員好像也是用C語言的最多,所以Windows所有編程資料發(fā)布的格式也是C格式。

 

上面的聲明用匯編的格式來表達(dá)就是:

       MessageBox Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword

上面最后一句Library:Use User32.lib則說明了這個函數(shù)包括在User32.dll中。

 

有了函數(shù)原型的定義后,就是調(diào)用的問題了,Win32 API調(diào)用中要把參數(shù)放入堆棧,順序是最后一個參數(shù)最先進(jìn)棧,在匯編中調(diào)用MessageBox函數(shù)的方法是:

       push uType

       push lpCaption

       push lpText

       push hWnd

       call MessageBox

在源程序編譯鏈接成可執(zhí)行文件后,call MessageBox語句中的MessageBox會被換成一個地址,指向可執(zhí)行文件中的導(dǎo)入表,導(dǎo)入表中指向MessageBox函數(shù)的實際地址會在程序裝入內(nèi)存的時候,根據(jù)User32.dll在內(nèi)存中的位置由Windows系統(tǒng)動態(tài)填入。

 

使用invoke語句

API是可以調(diào)用了,另一個煩人的問題又出現(xiàn)了,Win32API動輒就是十幾個參數(shù),整個源程序一眼看上去基本上都是把參數(shù)壓堆棧的push指令,參數(shù)的個數(shù)和順序很容易搞錯,由此引起的莫名其妙的錯誤源源不斷,源程序的可讀性看上去也很差。如果寫的時候少寫了一句push指令,程序在編譯和鏈接的時候都不會報錯,但在執(zhí)行的時候必定會崩潰,原因是堆棧對不齊了。

 

有不有解決的辦法呢?最好是像C語言一樣,能在同一句中打入所有的參數(shù),并在參數(shù)使用錯誤的時候能夠提示。

好消息又來了,Microsoft終于做了一件好事,在MASM中提供了一個偽指令實現(xiàn)了這個功能,那就是invoke偽指令,它的格式是:

       invoke 函數(shù)名 [,參數(shù)1][,參數(shù)2]…[,參數(shù)n]

MessageBox的調(diào)用在MASM中可以寫成:

       invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK

 

注意,invoke并不是80386處理器的指令,而是一個MASM編譯器的偽指令,在編譯的時候它把上面的指令展開成我們需要的4push指令和一個call指令,同時,進(jìn)行參數(shù)數(shù)量的檢查工作,如果帶的參數(shù)數(shù)量和聲明時的數(shù)量不符,編譯器報錯:

       error A2137: too few arguments to INVOKE

編譯時看到這樣的錯誤報告,首先要檢查的是有沒有少寫一個參數(shù)。對于不帶參數(shù)的API調(diào)用,invoke偽指令的參數(shù)檢查功能可有可無,所以既可以用call API_Name這樣的語法,也可以用invoke API_Name這樣的語法。

 

 

API函數(shù)的返回值

有的API函數(shù)有返回值,如MessageBox定義的返回值是int類型的數(shù),返回值的類型對匯編程序來說也只有dword一種類型,它永遠(yuǎn)放在eax中。如果要返回的內(nèi)容不是一個eax所能容納的,Win32 API采用的方法一般是返回一個指針,或者在調(diào)用參數(shù)中提供一個緩沖區(qū)地址,干脆把數(shù)據(jù)直接返回到緩沖區(qū)中去。

 

 

函數(shù)的聲明

在調(diào)用API函數(shù)的時候,函數(shù)原型也必須預(yù)先聲明,否則,編譯器會不認(rèn)這個函數(shù)。invoke偽指令也無法檢查參數(shù)個數(shù)。聲明函數(shù)的格式是:

       函數(shù)名 proto [距離] [語言] [參數(shù)1]:數(shù)據(jù)類型, [參數(shù)2]:數(shù)據(jù)類型,

句中的proto是函數(shù)聲明的偽指令,距離可以是NEARFARNEAR16NEAR32FAR16FAR32Win32中只有一個平坦的段,無所謂距離,所以在定義時是忽略的;語言類型就是.model那些類型,如果忽略,則使用.model定義的默認(rèn)值。

 

后面就是參數(shù)的列表了,對Win32匯編來說只存在dword類型的參數(shù),所以所有參數(shù)的數(shù)據(jù)類型永遠(yuǎn)是dword,另外對于編譯器來說,它只關(guān)心參數(shù)的數(shù)量,參數(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共有兩類,分別對應(yīng)兩個字符集:一類是處理ANSI字符集的,另一類是處理Unicode字符集的。前一類函數(shù)名字的尾部帶一個A字符,處理Unicode的則帶一個W字符。

 

我們比較熟悉的ANSI字符串是以NULL結(jié)尾的一串字符數(shù)組,每一個ANSI字符占一個字節(jié)寬。對于歐洲語言體系,ANSI字符集已足夠了,但對于有成千上萬個不同字符的幾種東方語言體系來說,Unicode字符集更有用。每一個Unicode字符占兩個字節(jié)的寬度,這樣一來就可以在一個字符串中使用65536個不同的字符了。

 

MessageBox和顯示字符串有關(guān),同樣它有兩個版本,嚴(yán)格地說,系統(tǒng)中有兩個定義:

       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》中只有一個MessageBox定義,但User32.dll中確確實實沒有MessageBox,而只有MessageBoxAMessageBoxW,那么為什么還是可以使用MessageBox呢?實際上在程序的頭文件user32.inc中有一句:

       MessageBox    equ    <MessageBoxA>

它把MessageBox偷梁換柱變成了MessageBoxA。在源程序中繼續(xù)沿用MessageBox是為了程序的可讀性以及保持和手冊的一致性,但對于編譯器來說,實際是在使用MessageBoxA

 

由于并不是每個Win32系統(tǒng)都支持W系統(tǒng)的API,在Windows 9x系列中,對Unicode是不支持的,很多的API只有ANSI版本,只有Windows NT系列才對Unicode完全支持。為了編寫在幾個平臺中通用的程序,一般應(yīng)用程序都使用ANSI版本的API函數(shù)集。

 

為了使程序更有移植性,在源程序中一般不直接指明使用Unicode還是ANSI版本,而是使用宏匯編中的條件匯編功能來統(tǒng)一替換,如在源程序中使用MessageBox,但在頭文件中定義:

       if UNICODE

                     MessageBox    equ   <MessageBoxW>

       else

                     MessageBox    equ   <MessageBoxA>

       endif

所有涉及版本問題的API都可以按此方法定義,然后在源程序的頭指定UNICODE=1UNICODE=0,重新編譯后就能產(chǎn)生不同的版本。

 

 

include語句

對于所有要用到的API函數(shù),在程序的開始部分都必須預(yù)先聲明,但這一個步驟顯然是比較麻煩的,為了簡化操作,可以采用各種語言通用的解決辦法,就是把所有的聲明預(yù)先放在一個文件中,在用到的時候再用include語句包含進(jìn)來。現(xiàn)在回到Win32 Hello World程序,這個程序用到了兩個API函數(shù):MessageBoxExitProcess,它們分別在User32.dllKernel32.dll中,在MASM32工具包中已經(jīng)包括了所有DLLAPI函數(shù)聲明列表,每個DLL對應(yīng)<DLL.inc>文件,在源程序中只要使用include語句包含進(jìn)來就可以了:

       include user32.inc

       include kernel32.inc

 

當(dāng)用到其他的API函數(shù)時,只需相應(yīng)增加對應(yīng)的include語句。

include語句還用來在源程序中包含別的文件,當(dāng)多個源程序用到相同的函數(shù)定義、常量定義、甚至源代碼時,可以把相同的部分寫成一個文件,然后在不同的源程序中用include語句包含進(jìn)來。

 

編譯器對include語句的處理僅是簡單地把這一行用指定的文件內(nèi)容替換掉而而已。

include語句的語法是:

              include 文件名

        include <文件名>

當(dāng)遇到要包括的文件名和MASM的關(guān)鍵字同名等可能會引起編譯器混淆的情況時,可以用<>將文件名括起來。

 

 

includelib語句

DOS匯編中,使用中斷調(diào)用系統(tǒng)功能是不必聲明的,處理器自己知道到中斷向量表中去取中斷地址。在Win32匯編中使用API函數(shù),程序必須知道調(diào)用的API函數(shù)存在于哪個DLL中,否則,操作系統(tǒng)必須搜索系統(tǒng)中存在的所有DLL,并且無法處理不同DLL中的同名函數(shù),這顯然是不現(xiàn)實的,所以,必須有個文件包括DLL庫正確的定位信息,這個任務(wù)是由導(dǎo)入庫來實現(xiàn)的。

 

在使用外部函數(shù)的時候,DOS下有函數(shù)庫的概念,那時的函數(shù)庫實際上是靜態(tài)庫,靜態(tài)庫是一組已經(jīng)編寫好的代碼模塊,在程序中可以自由引用,在源程序編譯成目標(biāo)文件,最后要鏈接可執(zhí)行文件的時候,由link程序從庫中找出相應(yīng)的函數(shù)代碼,一起鏈接到最后的可執(zhí)行文件中。DOSC語言的函數(shù)庫就是典型的靜態(tài)庫。庫的出現(xiàn)為程序員節(jié)省了大量的開發(fā)時間,缺點就是每個可執(zhí)行文件中都包括了要用到的相同函數(shù)的代碼,占用了大量的磁盤空間,在執(zhí)行的時候,這些代碼同樣重復(fù)占用了寶貴的內(nèi)存。

 

Win32環(huán)境中,程序鏈接的時候仍然要使用函數(shù)庫來定位函數(shù)信息,只不過由于函數(shù)代碼放在DLL文件中,庫文件中只留有函數(shù)的定位信息和參數(shù)數(shù)目等簡單信息,這種庫文件叫做導(dǎo)入庫,一個DLL文件對應(yīng)一個導(dǎo)入庫,如User32.dll文件用于編程的導(dǎo)入庫是User32.libMASM32工具包中包含了所有DLL的導(dǎo)入庫。

 

為了告訴鏈接程序使用哪個導(dǎo)入庫,使用的語句是:

              includelib 庫文件名

        includelib <庫文件名>

include的用法一樣,在要包括讓編譯器混淆的文件名時加括號。

Win32 Hello World程序用到的兩個API函數(shù)MessageBoxExitProcess分別在User32.dllKernel32.dll中,那么在源程序使用的相應(yīng)語句為:

       includelib user32.lib

       includelib kernel32.lib

 

include語句的處理不同,includelib不會把.lib文件插入到源程序中,它只是告訴鏈接器在鏈接的時候到指定的庫文件中去找而已。

 

 

API參數(shù)中的等值定義

再回過頭來看顯示消息框的語句:

       invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK

uType這個參數(shù)中使用了MB_OK,這個MB_OK是什么意思?

在《Microsoft Win32 Programmer’s Reference》中的說明:

uType——定義對話框的類型,這個參數(shù)可以是以下標(biāo)志的合集:

要定義消息框上顯示按鈕,用下面的某一個標(biāo)志:

MB_ABORTRETRYIGNORE——消息框有三個按鈕:終止,重試和忽略

MB_HELP——消息框上顯示一個幫助按鈕,按下后發(fā)送WM_HELP消息

MB_OK——消息框上顯示一個確定按鈕,這是默認(rèn)值

……

要在消息框中顯示圖標(biāo),用下面的某一個標(biāo)志:

MB_ICONWARNING——顯示驚嘆號圖標(biāo)

MB_ICONINFORMATION——顯示消息圖標(biāo)

……

 

這些是uType參數(shù)說明中的一小半,可以看出,參數(shù)可以用的值有很多種。

 

MB_ICONWARINGMB_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就是4MB——ICONWARNING就是30h,默認(rèn)的MB_OK就是0Win32 API的參數(shù)使用這樣的定義方法顯然是為了免除程序員死記數(shù)值定義的麻煩。在編寫Win32匯編程序的時候,MASM32工具包中的Windows.inc也包括了所有這些參數(shù)的定義,只要在程序的開頭包含這個定義文件:

       include windows.inc

就可以方便地完全按照API手冊來使用Win32函數(shù)。

 

打開\masm32\include 目錄下的Windows.inc查看一下,可以發(fā)現(xiàn)整個文件總共有兩萬六千多行,包括了幾乎所有的Win32 API參數(shù)中的常量和數(shù)據(jù)結(jié)構(gòu)定義。

 

 

 

標(biāo)號、變量和數(shù)據(jù)結(jié)構(gòu)

當(dāng)程序中要跳轉(zhuǎn)到另一位置時,需要有一個標(biāo)識來指示新的位置,這就是標(biāo)號,通過在目的地址的前面放上一個標(biāo)號,可以在指令中使用標(biāo)號來代替直接使用地址

 

使用變量是任何編程語言都要遇到的工作,Win32匯編也不例外,在MASM中使用變量也有需要注意的幾個問題,錯誤地使用變量定義或用錯誤的方法初始化變量會帶來難以定位的錯誤。

 

變量是計算機內(nèi)存中已命名的存儲位置,在C語言中有很多種類的變量,如整數(shù)型、浮點型和字符型等,不同的變量有不同的用途和尺寸,比如說雖然長整數(shù)和單精度浮點數(shù)都是32位長,但它們的用途不同。

 

顧名思義,變量的值在程序運行中是需要改變的,所以它必須定義在可寫的段內(nèi),如.data.data?,或者在堆棧內(nèi)。按照定義的位置不同,MASM中的變量也分為全局變量和局部變量兩種。

 

MASM中標(biāo)號和變量的命名規(guī)范是相同的,它們是:

1)可以用字母、數(shù)字、下劃級及符號@$?

2)第一個符號不能是數(shù)字。

3)長度不能超過240個字符。

4)不能使用指令名等關(guān)鍵字。

5)在作用域內(nèi)必須是唯一的。

 

 

標(biāo)號

標(biāo)號的定義

當(dāng)在程序中使用一條跳轉(zhuǎn)指令的時候,可以用標(biāo)號來表示跳轉(zhuǎn)的目的地,編譯器在編譯的時候會把它替換成地址,標(biāo)號既可以定義在目的指令同一行的頭部,也可以在目的指令前一行單獨用一行定義,標(biāo)號定義的格式是:

       標(biāo)號名: 目的指令

標(biāo)號的作用域是當(dāng)前的子程序,在單個子程序中的標(biāo)號不能同名,否則編譯器不知該用哪個地址,但在不同的子程序中可以有相同名稱的標(biāo)號,這意味著不能從一個子程序中用跳轉(zhuǎn)指令跳到另一個子程序中。

 

在低版本的MASM中,標(biāo)號在整個程序中是唯一的,子程序中的標(biāo)號也可以從整個程序的任何地方轉(zhuǎn)入。但Win32匯編使用的高版本MASM中不允許這樣,這是為了提供對局部變量和參數(shù)的支持,由于在子程序入口有對堆棧的初始化指令,所以一個子程序不允許有多個入口,其結(jié)果主是標(biāo)號的作用域變成了單個子程序范圍。

 

 

MASM中的@@

DOS時代,為標(biāo)號起名是個麻煩的事情,因為匯編指令用到跳轉(zhuǎn)指令特別多,任何比較和測試等都要涉及跳轉(zhuǎn),所以在程序中會有很多標(biāo)號,在整個程序范圍內(nèi)起個不重名的標(biāo)號要費一番功夫,結(jié)果常常用addr1addr2之類的標(biāo)號一直延續(xù)下去,如果后來要在中間插一個標(biāo)號,那么就常常出現(xiàn)addr1_1loop10_5之類奇怪的標(biāo)號。

 

實際上,很多標(biāo)號會使用一到兩次,而且不一定非要起個有意義的名稱,如匯編程序中下列代碼結(jié)構(gòu)很多:

              mov cx,1234h

              cmp flag,1

              je loc1

              mov cx,1000h

   loc1:

        loop loc1

loc1在別的地方就再也用不到了,對于這種情況,高版本的MASM@@標(biāo)號去代替它:

              mov cx,1234h

              cmp flag,1

              je @F

              mov cx,1000h

   @@:

        loop @B

當(dāng)用@@做標(biāo)號時,可以用@F@B來引用它,@F表示本條指令后的第一個@@標(biāo)號,@B表示本條指令前的第一個@@標(biāo)號,程序中可以有多個@@標(biāo)號,@B@F只尋找匹配最近的一個。

 

不要在間隔太遠(yuǎn)的代碼中使用@@標(biāo)號,因為在以后的修改中@@@B@F中間可能會被無意中插入一個新的@@,這樣一來,@B@F就會引用到錯誤的地方去,源程序中@@標(biāo)號和跳轉(zhuǎn)指令之間的距離最好限制在編輯器能夠顯示的同一屏幕的范圍內(nèi)。

 

 

全局變量

全局變量的定義

全局變量的作用域是整個程序,Win32匯編的全局變量定義在.data.data?段內(nèi),可以同時定義變量的類型和長度,格式是:

變量名    類型       初始值1, 初始值2,…

變量名    類型       重復(fù)數(shù)量 dup (初始值1,初始值2,…)

 

MASM中可以定義的變量類型相當(dā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

有符號字節(jié)(sign byte

sbyte

 

1

有符號字(sign word

sword

 

2

有符號雙字(sign dword

sdword

 

4

單精度浮點數(shù)

real4

 

4

雙精度浮點數(shù)

real8

 

8

10字節(jié)浮點數(shù)

real10

 

10

 

所有使用到變量類型的情況中,只有定義全局變量的時候類型才可以用縮寫,現(xiàn)在先來看全局變量定義的幾個例子:

       .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定義了一個未初始化的word類型變量,名稱為wHour

2定義了一個名為wMinuteword類型變量。

3定義了一個雙字類型的變量_hWnd

4定義了一組字,以0001000200010002,的順序在內(nèi)存中重復(fù)100遍,一共是200個字節(jié)。

5定義了一個1024字節(jié)的緩沖區(qū)。

6定義了一個字符串,總共占用了12個字節(jié)。兩頭的單引號是定界的符號,并不屬于字符串中真正的內(nèi)容。

 

byte類型變量的定義中,可以用引號定義字符串和數(shù)值定義的方法混用,假設(shè)要定義兩個字符串Hello,World!Hello again,每個字符串后面中回車和換行符,最后以一個0字符結(jié)尾,可以定義如下:

       szText db ‘Hello,World!’,0dh,0ah,’Hello again’,0dh,0ah,0

 

 

全局變量的初始化值

全局變量在定義中既可以指定初值,也可以只用問題預(yù)留究竟,在.data?段中,只能用問號預(yù)留究竟,因為.data?段中不能指定初始值,這里就有一個問題:既然可以用問號預(yù)留空間,那么在實際運行的時候,這個未初始化的值是隨機的還是確定的呢?在全局變量中,這個值就是0,所以用問號指定的全局變量如果要以0為初始值的話,在程序中可以不必為它賦值。

 

 

 

局部變量

局部變量這個名稱最早源于高級語言,主要是為了定義一些僅在單個函數(shù)里面有用的變量而提出的,使用局部變量能帶來一些額外的好處,它使程序的模塊化封裝變得可能,試想一下,如果要用到的變量必須定義在程序的數(shù)據(jù)段里面,假設(shè)在一個子程序中要用到一些變量,當(dāng)把這個子程序移植到別的程序時,除了把代碼移過去以外,還必須把變量定義移過去。而即使把變量定義移過去了,由于這些變量定義在大家都可以用的數(shù)據(jù)段中,就無法對別的代碼保持透明,別的代碼有可能有意無意地修改它們。還有,在一個大的工程項目中,存在很多的子程序,所有的子程序要用到的變量全部定義在數(shù)據(jù)段中,會使數(shù)據(jù)段變得很大,混在一起的變量也使維護(hù)變得非常不方便。

 

局部變量這個概念出現(xiàn)以后,兩個以上子程序都要用到的數(shù)據(jù)才被定義為全局變量統(tǒng)一放在數(shù)據(jù)段中,僅在子程序內(nèi)部使用的變量則放在堆棧中,這樣子程序可以編成黑匣子的模樣,使程序的模塊結(jié)構(gòu)更加分明。

 

局部變量的作用域是單個子程序,在進(jìn)入子程序的時候,通過修改堆棧指針esp來預(yù)留出需要的空間,在用ret指令返回主程序之前,同樣通過恢復(fù)esp丟棄這些空間,這些變量就隨之無效了。它的缺點就是因為空間是臨時分配的,所以無法定義含有初始化值的變量,對局部變量的初始化一般在子程序中由指令完成。

 

DOS時代,低版本的宏匯編本來無所謂全局變量和局部變量,所有的變量都是定義在數(shù)據(jù)段里面的,能讓被所有的子程序或主程序存取,就相當(dāng)于現(xiàn)在所說的全局變量,用匯編語言在堆棧中定義局部變量是很麻煩的一件事情。要和高級語言做混合編程的時候,程序員往往很痛苦地在邊上準(zhǔn)備一張表,表上的內(nèi)容是局部變量名和ebp指針的位置關(guān)系。

 

局部變量的定義

MASMlocal偽指令提供了對局部變量的支持。定義的格式是:

       local 變量名1 [[重復(fù)數(shù)量]] [:類型], 變量名2 [[重復(fù)數(shù)量]] [:類型] ……

local偽指令必須緊接在子程序定義的偽指令proc后、其他指令開始前,這是因為局部變量的數(shù)目必須在子程序開始的時候就確定下來,在一個local語句定義不下的時候,可以有多個local語句,語法中的數(shù)據(jù)類型不能用縮寫,如果要定義數(shù)據(jù)結(jié)構(gòu),可以用數(shù)據(jù)結(jié)構(gòu)的名稱當(dāng)做類型。Win32匯編默認(rèn)的類型是dword,如果定義dword類型的局部變量,則類型可以省略。當(dāng)定義數(shù)組的時候,可以[]括號起來。不能使用定義全局變量的dup偽指令。局部變量不能和已定義的全局變量同名。局部變量的作用域是當(dāng)前子程序,所以在不同的子程序中可以有同名的局部變量。

 

定義局部變量的例子:

       local        local[1024]:byte                   ;1

       local        loc2                                    ;2

       local        loc3:WNDCLASS                 ;3

1定義了一個1024字節(jié)長的局部變量loc1

2定義了一個名為loc2的局部變量,類型是默認(rèn)值dword

3定義了一個WNDCLASS數(shù)據(jù)結(jié)構(gòu),名為loc3

 

下面是局部變量使用的一個典型的例子:

       TestProc        proc

                            local        @loc1:dword, @loc2:word

                            local        @loc3:byte

 

                            mov eax,@loc1

                            mov ax,@loc2

                            mov al,@loc3

                            ret

       TestProc        endp

這是一個名為TestProc的子程序,用local語句定義了3個變量,@loc1dword類型,@loc2word類型,@loc3byte類型,在程序中分別有3句存取3個局部變量的指令,然后就返回了,編譯成可執(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í)行過程中可能隨時用到,不可能用esp來隨時存取局部變量,ebp寄存器是以堆棧段為默認(rèn)數(shù)據(jù)段的,所以,可以用ebp做指針,于是,在初始化前,先用一句push ebp指令把原來的dbp保存起來,然后把esp的值放到ebp中,供存取局部變量做指針用,再后面就是堆棧中預(yù)留空間了,由于堆棧是向下增長的,所以要在esp中加一個負(fù)值,FFFFFFF8就是-8,慢著!一個dword加一個word加一個字節(jié)不是7嗎,為什么是8呢?這是因為在80386處理器中,dword為界對齊時存取內(nèi)存速度最快,所以MASM寧可浪費一個字節(jié),執(zhí)行了這3句指令后,初始化完成,就可以進(jìn)行正常的操作了,從指令中可以看出局部變量在堆棧中的位置排列。

 

在程序退出的時候,必須把正確的esp設(shè)置回去,否則,ret指令會從堆棧中取出錯誤的地址返回,看程序可以發(fā)現(xiàn),ebp就是正確的esp值,因為子程序開始的時候已經(jīng)有一句mov ebp,esp,所以要返回的時候只要先mov esp,ebp,然后再pop ebp,堆棧就是正確的了。

 

80386指令集中有一條指令可以在一句中實現(xiàn)這些功能,就是leave指令,所以,編譯器在ret指令之前只使用了一句leave指令。

 

明白了局部變量使用的原理,就很容易理解使用時的注意點:ebp寄存器是關(guān)鍵,它起到保存原始esp的作用,并隨時用做存取局部變量的指針基址,所以在任何時刻,不要嘗試把ebp用于別的用途,否則會帶來意想不到的后果。

 

Win32匯編中局部變量的使用方法可以解釋一個很有趣的現(xiàn)象:在DOS匯編的時候,如果在子程序中的push指令和pop指令不配對,那么返回的時候ret指令從堆棧里得到的肯定是錯誤的返回地址,程序也就死掉了。但在Win32匯編中,push指令和pop指令不配對可能在邏輯上產(chǎn)生錯誤,卻不會影響子程序正常返回,原因就是在返回的時候esp不是靠相同數(shù)量的pushpop指令來保持一致的,而是靠leave指令從保存在ebp中的原始值中取回來的,也就是說,即使把esp改得一塌糊涂也不會影響到子程序的返回,當(dāng)然,竅門就在ebp,把ebp改掉,程序就玩完了!

 

局部變量的初始化值

顯然,局部變量是無法在定義的時候指定初始化值的,因為local偽指令只是簡單地把空間給留出來,那么開始使用時它里面是什么值呢?和全局變量不一樣,局部變量的初始值是隨機的,是其他子程序執(zhí)行后在堆棧里留下的垃圾,所以,對局部變量的值一定要初始化,特別是定義為結(jié)構(gòu)后當(dāng)參數(shù)傳遞給API函數(shù)的時候。

 

API函數(shù)使用的大量數(shù)據(jù)結(jié)構(gòu)中,往往用0做默認(rèn)值,如果用局部變量定義數(shù)據(jù)結(jié)構(gòu),初始化時只定義了其中的一些字段,那么其余字段的當(dāng)前值可以是編程者預(yù)想不到的數(shù)值,傳給API函數(shù)后,執(zhí)行的結(jié)果可能是意想不到的,這是初學(xué)者很容易忽略的一個問題。所以最好的辦法是:在賦值前首先將整個數(shù)據(jù)結(jié)構(gòu)填0,然后再初始化要用的字段,這樣其余的字段就不必一個個地去填0了,RtlZeroMemory這個API函數(shù)就是實現(xiàn)填0的功能的。

 

 

數(shù)據(jù)結(jié)構(gòu)

數(shù)據(jù)結(jié)構(gòu)實際上是由多個字段組成的數(shù)據(jù)樣板,相當(dāng)于一種自定義的數(shù)據(jù)類型,數(shù)據(jù)結(jié)構(gòu)中間的每一個字段可以是字節(jié)、字、雙字、字符串或所有可能的數(shù)據(jù)類型。

 

比如在API函數(shù)RegisterClass中要使用到一個叫做WNDCLASS的數(shù)據(jù)結(jié)構(gòu),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語言格式的,這個數(shù)據(jù)結(jié)構(gòu)包含了10個字段,字段的名稱是stylelpfnWndProccbClsExtra等,前面的UINTWNDPROC等是這些字段的類型,在匯編中,數(shù)據(jù)結(jié)構(gòu)的寫法如下:

       結(jié)構(gòu)名    struct

 

       字段1     類型              ?

       字段2     類型              ?

       ……

 

       結(jié)構(gòu)名    ends

上面的WNDCLASS結(jié)構(gòu)定義用匯編的格式來表示就是:

       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)實際上只是定義了一個樣板,上面的定義語句并不會在哪個段中產(chǎn)生數(shù)據(jù),和Word中使用各種信紙與文書等模板類似,定義了數(shù)據(jù)結(jié)構(gòu)以后就可以多次在源程序中用這個樣板當(dāng)做數(shù)據(jù)類型來定義數(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,>

              ……

這個例子定義了一個以WNDCLASS為結(jié)構(gòu)的變量stWndClass,第一段的定義方法是未初始化的定義方法,第二段是在定義的同時指定結(jié)構(gòu)中各字段的初始化值,各字段的初始值用逗號隔開,在這個例子中10個字段的初始值都指定為1

 

在匯編中,數(shù)據(jù)結(jié)構(gòu)的引用方法有好幾種,以上面的定義為例,如果要使用stWndClass中的lpfnWndProc字段,最直接的辦法是:

       mov eax,stWndClass.lpfnWndProc

它表示把lpfnWndProc字段的值放入eax中去,假設(shè)stWndClass在內(nèi)存中的地址是403000h,這句指令會被編譯成mov eax,[403004h],因為lpfnWndProcstWndClass中的第二個字段,第一個字段是dword,已經(jīng)占用了4字節(jié)的空間。

 

在實際使用中,常常有使用指令存取數(shù)據(jù)結(jié)構(gòu)的情況,如果使用esi寄存器做指針尋址,可以使用下列語句完成同樣的功能:

       mov esi,offset stWndClass

       move ax,[esi + WNDCLASS.lpfnWndProc]

注意:第二句是[esi + WNDCLASS.lpfnWndProc]而不是[esi + stWndClass.lpfnWndProc],因為前者被編譯成mov eax,[esi + 4],而后者被編譯成mov eax,[esi + 403004h],后者的結(jié)果顯然是錯誤的!如果要對一個數(shù)據(jù)結(jié)構(gòu)中的大量字段進(jìn)行了操作,這種寫法顯然比較煩瑣,MASM還有一個用法,可以用assume偽指令把寄存器預(yù)先定義為結(jié)構(gòu)指針,再進(jìn)行操作:

       mov esi,offset stWndClass

       assume esi:ptr WNDCLASS

       move ax,[esi].lpfnWndProc

       ……

       assume esi:nothing

 

這樣,使用寄存器也可以用逗號引用字段名,程序的可讀性比較好。這樣的寫法在最后編譯成可執(zhí)行程序的時候產(chǎn)生同樣的代碼。注意:在不再使用esi寄存器做指針的時候要用assume esi:nothing取消定義。

 

結(jié)構(gòu)的定義也可以嵌套,如果要定義一個新的NEW_WNDCLASS結(jié)構(gòu),里面包含一個老的WNDCLASS結(jié)構(gòu)和一個新的dwOption字段,那么可以如下定義:

       NEW_WNDCLASS       struct

 

       DwOption             dword                   ?

       OldWndClass         WNDCLASS         <>

 

       NEW_WNDCLASS       ends

 

假設(shè)現(xiàn)在esi是指向一個NEW_WNDCLASS的指針,那么引用里面嵌套的oldWndClass中的lpfnWndProc字段時,就可以用下面的語句:

       move ax,[esi].oldWndClass.lpfnWndProc

 

結(jié)構(gòu)的嵌套在Windows的數(shù)據(jù)定義中也常有,熟練掌握數(shù)據(jù)結(jié)構(gòu)的使用對Win32匯編編程是很重要的!

 

 

 

變量的使用

以不同的類型訪問變量

這個話題有點像C語言中的數(shù)據(jù)類型強制轉(zhuǎn)換,C語言中的類型轉(zhuǎn)換指的是把一個變量的內(nèi)容轉(zhuǎn)換成另外一種類型,轉(zhuǎn)換過程中,數(shù)據(jù)的內(nèi)容已經(jīng)發(fā)生了變化,如把浮點數(shù)轉(zhuǎn)換成整數(shù)后,小數(shù)點后的內(nèi)容就丟失了。在MASM中以不同的類型訪問不會對變量造成影響。

 

例如,以db方式定義一個緩沖區(qū):

       szBuffer         db           1024 dup (?)

然后從其他地方取得了數(shù)據(jù),但數(shù)據(jù)的格式是字方式組織的,要處理數(shù)據(jù),最有效的方法是兩個字節(jié)兩個字節(jié)處理,但如果在程序中把szBuffer的值放入ax:

       mov ax,szBuffer

編譯器會報一個錯:

       error A2070: invalid instruction operands

意思是無效的指令操作,為什么呢?因為szBuffer是用db定義的,而ax的尺寸是一個word,等于兩個字節(jié),尺寸不符合。MASM中,如果要用指定類型之外的長度訪問變量,必須顯式地指出要訪問的長度,這樣,編譯器忽略語法上的長度檢驗,僅使用變量的地址。使用的方法是:

       類型 ptr 變量名

類型可以是byte, word, dword, fword, qword, real8real10。如:

       mov ax,word ptr szBuffer

       mov eax,dword ptr szBuffer

DOS匯編中也有這種用法。

上述語句能通過編譯,當(dāng)然,類型必須和操作的寄存器長度匹配。在這里要注意的是,指定類型的參數(shù)訪問并不會去檢測長度是否溢出,看下面一段代碼:

                            .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這一句很顯然使al12h,下面的兩句呢,axeax難道等于0012h00000012h嗎?實際運行結(jié)果是3412h78123412h,為什么呢?(DOS匯編基礎(chǔ)不錯的同學(xué),應(yīng)該能理解)先來看反匯編的內(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段中的變量是按順序從低地址往高地址排列的,對于超過一個字節(jié)的數(shù)據(jù),80386處理器的數(shù)據(jù)排列方式是低位數(shù)據(jù)在低地址,所以wTest21234h在內(nèi)存中的排列是34h 12h,因為34h是低位。同樣,dwTest3在內(nèi)存中以78h 56h 34h 12h從低地址往高地址存放,在執(zhí)行指令mov ax,word ptr bTest1的時候,是從bTest1的地址403000h處取一個字,其長度已經(jīng)超過了bTest1的范圍并落到了wTest2中,從內(nèi)存中看,是取了bTest1的數(shù)據(jù)12hwTest2的低位34h,在這兩個字節(jié)中,12h位于低地址,所以ax中的數(shù)值是3412h。同理,看另一條指令:

       move ax,dword ptr bTest1

這條指令取了bTest1wTest2的全部和dwTest3的最低位78h,在內(nèi)存中的排列是12h 34h 12h 78h,所以eax等于78123412h

 

這個例子說明了匯編中用ptr強制覆蓋變量長度的時候,實質(zhì)上是只用了變量的地址而禁止編譯器進(jìn)行檢驗,編譯器并不會考慮定界的問題,程序員在使用的時候必須對內(nèi)存中的數(shù)據(jù)排列有個全局概念,以免越界存取到意料之外的數(shù)據(jù)。

 

如果程序員的本意是類似于C語言的強制類型轉(zhuǎn)換,想把bTest1的一個字節(jié)擴(kuò)展到一個字或一個雙字再放到axeax中,高位保持0而不是越界存取到其他的變量,可以用80386的擴(kuò)展指令來實現(xiàn)。80386處理器提供的movzx指令可以實現(xiàn)這個功能,例如:

       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中。

3cl中的8位值擴(kuò)展到32位放入eax中。

4ax中的16位值擴(kuò)展到32位放入eax中。

movzx指令進(jìn)行數(shù)據(jù)長度擴(kuò)展是Win32匯編中經(jīng)常用到的技巧。

 

 

變量的尺寸和數(shù)量

在源程序中用到變量的尺寸和數(shù)量的時候,可以用sizeoflengthof偽指令來實現(xiàn),格式是:

       sizeof      變量名、數(shù)據(jù)類型或數(shù)據(jù)結(jié)構(gòu)名

       lengthof   變量名

sizeof偽指令可以取得變量、數(shù)據(jù)類型或數(shù)據(jù)結(jié)構(gòu)以字節(jié)為單位的長度,lengthof可以取得變量中數(shù)據(jù)的項數(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)的長度40ebx同樣是40ecx的值是13,就是Hello,world!字符串的長度加上一個字節(jié)的0結(jié)束符,edx的值是一個雙字的長度:4,而esi則等于4個雙字的長度16

 

如果把所有的sizeof換成lengthof,那么eax會等于1,因為只定義了1WNDCLASS,而ecx同樣等于13esi則等于4,而lenghof WNDCLASSTlengthof dword是非法的用法,編譯程序會報錯。

 

要注意的是,sizeoflengthof的數(shù)值是編譯時產(chǎn)生的,由編譯器傳遞到指令中去,上邊的指令最后產(chǎn)生的代碼就是:

       mov eax,40

       mov ebx,40

       mov ecx,13

       mov edx,4

       mov esi,16

如果為了把HelloWorld分兩行定義,szHello是這樣定義的:

       szHello           db           ‘Hello’,odh,oah

                            db           ‘World’,0

那么sizeof szHello是多少呢?注意!是7而不是13MASM中的變量定義只認(rèn)一行,后一行db ‘World’,0實際上是另一個沒有名稱的數(shù)據(jù)定義,編譯器認(rèn)為sizeof szHello是第一行字符的數(shù)量。雖然把szHello的地址當(dāng)參數(shù)傳給MessageBox等函數(shù)顯示時會把兩行都顯示出來,但嚴(yán)格地說這是越界使用變量。雖然在實際的應(yīng)用中這樣定義長字符串的用法很普遍,因為如果要顯示一屏幕幫助,一行是不夠的,但要注意的是:要用到這種字符串的長度時,千萬不要用sizeof去表示,最好是在程序中用lstrlen函數(shù)去計算。

 

 

獲取變量地址

獲取變量地址的操作對于全局變量和局部變量是不同的。

對于全局變量,它的地址在編譯的時候已經(jīng)由編譯器確定了,它的用法大家都不陌生:

       mov 寄存器, offset 變量名

其中offset是取變量地址的偽操作符,和sizeof偽操作符一樣,它僅把變量的地址帶到指令中去,這個操作是在編譯時而不是在運行時完成的。

 

對于局部變量,它是用ebp來做指針操作的,假設(shè)ebp的值是40100h,那么局部變量l的地址是ebp-4400FCh,由于ebp的值隨著程序的執(zhí)行環(huán)境不同可能是不同的,所以局部變量的地址值在編譯的時候也是不確定的,不可能用offset偽操作符來獲取它的地址。

 

80386處理器中有一條指令用來取指針的地址,就是lea指令,如:

       lea eax,[ebp-4]

該指令可以在運行時按照ebp的值實際計算出地址放到eax中。

 

如果要在invoke偽指令的參數(shù)中用到一個局部變量的地址,該怎么辦呢?參數(shù)中是不可能寫入lea指令的,用offset又是不對的。MASM對此有一個專用的偽操作符addr,其格式為:

       addr 局部變量名和全局變量名

當(dāng)addr后跟全局變量名的時候,用法和offset是相同的;當(dāng)addr后面跟局部變量名的時候,編譯器自動用lea指令先把地址取到eax中,然后用eax來代替變量地址使用。注意addr偽操作符只能在invoke的參數(shù)中使用,不能用在類似于下列的場合:

       move ax, addr 局部變量名          ;注意:錯誤用法

 

假設(shè)在一個子程序中有如下invoke指令:

       invoke Test,eax, addr szHello

其中Test是一個需要兩個參數(shù)的子程序,szHello是一個局部變量,會發(fā)生什么結(jié)果呢?編譯器會把invoke偽指令和addr翻譯成下面這個模樣:

       lea eax,[ebp-4]

       push eax                ;參數(shù)2addr szHello

       push eax                ;參數(shù)1eax

       call Test

發(fā)現(xiàn)了什么?到push第一個參數(shù)eax之前,eax的值已經(jīng)被lea eax,[ebp-4]指令覆蓋了!也就是說,要用到的eax的值不再有效,所以,當(dāng)在invoke中使用addr偽操作符時,注意在它的前面不能用eax,否則eax的值會被覆蓋掉,當(dāng)然eaxaddr的后面的參數(shù)中用是可以的。幸虧MASM編譯器對這種情況有如下錯誤提示:

       error A2133:register value overwritten by INVOKE

否則,不知道又會引出多少莫名其妙的錯誤!

 

 

 

 

使用子程序

當(dāng)程序中相同功能的一段代碼用得比較頻繁時,可以將它分離出來寫成一個子程序,在主程序中用call指令來調(diào)用它。這樣可以不用重復(fù)寫相同的代碼,而用call指令就可以完成多次同樣的工作了。Win32匯編中的子程序也采用堆棧來傳遞參數(shù),這樣就可以用invoke偽指令來進(jìn)行調(diào)用和語法檢查工作。

 

子程序的定義

子程序的定義方式如下所示:

       子程序名 proc [距離] [語言類型] [可視區(qū)域] [USES寄存器列表] [,參數(shù):類型]…[VARARG]

 

                            local 局部變量列表

 

                            指令

 

         子程序名 endp

procendp偽指令定義了子程序開始和結(jié)束的位置,proc后面跟的參數(shù)是子程序的屬性和輸入?yún)?shù)。子程序的屬性有:

       距離。可以是NEARFARNEAR16NEAR32FAR16FAR32Win32中只有一個平坦的段,無所謂距離,所以對距離的定義往往忽略。

       語言類型表示參數(shù)的使用方式和堆棧平衡的方式,可以是StdCallCSysCallBASICFORTRANPASCAL,如果忽略,則使用程序頭部.model定義的值。

       可視區(qū)域,可以是PRIVATEPUBLICEXPORTPRIVATE表示子程序只對本模塊可見;PUBLIC表示對所有的模塊可見(在最后編譯鏈接完成的.exe文件中);EXPORT表示是導(dǎo)出的函數(shù),當(dāng)編寫DLL的時候要將某個函數(shù)導(dǎo)出的時候可以這樣使用。默認(rèn)的設(shè)置是PUBLIC

       USES寄存器列表,表示由編譯器在子程序指令開始前自動安排push這些寄存器的指令,并且在ret前自動安排pop指令,用于保存執(zhí)行環(huán)境,但筆者認(rèn)為不如自己在開頭和結(jié)尾用pushadpopad指令一次保存和恢復(fù)所有寄存器來得方便。

       參數(shù)和類型。參數(shù)指參數(shù)的名稱,在定義參數(shù)名的時候不能跟全局變量和子程序中的局部變量重名。對于類型,由于Win32中的參數(shù)類型只有32位(dword)一種類型,所以可以省略。在參數(shù)定義的最后還可以跟VARARG,表示在已確定的參數(shù)后還可以跟多個數(shù)量不確定的參數(shù),在Win32匯編中唯一使用VARARGAPI就是wsprintf,類似于C語言中的printf,其參數(shù)的個數(shù)取決于要顯示的字符串中指定的變量個數(shù)。

 

完成了定義之后,可以用invoke偽指令來調(diào)用子程序,當(dāng)invoke偽指令位于子程序代碼之前的時候,處理到invoke語句的時候編譯器還沒有掃描到子程序定義信息的記錄,所以會有以下錯誤的信息:

       error A2006: undefined symbol: _ProcWinMain

這并不是說子程序的編寫有錯誤,而是invoke偽指令無法得知子程序的定義情況,所以無法進(jìn)行參數(shù)的檢測。在這種情況下,為了讓invoke指令能正常使用,必須在程序的頭部用proto偽操作定義子程序的信息,提前告訴invoke語句關(guān)于子程序的信息,當(dāng)然,如果子程序定義在前的話,用proto的定義就可以省略了。

 

由于程序的調(diào)試過程中可能常常對一些子程序的參數(shù)個數(shù)進(jìn)行調(diào)整,為了使它們保持一致,就需要同時修改proc語句和proto語句。在寫源程序的時候有意識地把子程序的位置提到invoke語句的前面,省略掉proto語句,可以簡化程序和避免出錯。

 

 

 

參數(shù)傳遞和堆棧平衡

了解了子程序的定義方法后,讓我們繼續(xù)深入了解了程序的使用細(xì)節(jié)。在調(diào)用子程序時,參數(shù)的傳遞是通過堆棧進(jìn)行的,也就是說,調(diào)用者把要傳遞給子程序的參數(shù)壓入堆棧,子程序在堆棧中取出相應(yīng)的值再使用,比如,如果要調(diào)用:

       SubRouting(Var1, Var2, Var3)

經(jīng)過編譯后的最終代碼可能是(注意只是可能):

       push Var3

       push Var2

       push Var1

       call SubRouting

       add esp,12

也就是說,調(diào)用者首先把參數(shù)壓入堆棧,然后調(diào)用子程序,在完成后,由于堆棧中先前壓入的數(shù)不再有用,調(diào)用者或者被調(diào)用者必須有一方把堆棧指針修正到調(diào)用前的狀態(tài),即堆棧的平衡。參數(shù)是最右邊的先入堆棧還是最左邊的先入堆棧、還有由調(diào)用者還是被調(diào)用者來修正堆棧都必須有個約定,不然就會產(chǎn)生錯誤的結(jié)果,這就是在上述文字中使用“可能”這兩個字的原因。各種語言中調(diào)用子程序的約定是不同的,所以在proc以及proto語句的語言屬性中確定語言類型后,編譯器才可能將invoke偽指令翻譯成正確的樣子,不同語言的不同點如下:

 

C

SysCall

StdCall

BASIC

FORTRAN

PASCAL

最先入棧參數(shù)

清除堆棧者

調(diào)用者

子程序

子程序

子程序

子程序

子程序

允許使用VARARG

注:VARARG表示參數(shù)的個數(shù)可以是不確定的,如wsprintf函數(shù),本表中特殊的地方是StdCall的堆棧清除平時是由子程序完成的,但使用VARARG時是由調(diào)用者清除的。

 

為了了解編譯器對不同類型子程序的處理方式,先來看一段源程序:

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

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)換處理不同類型的子程序的:

       ;這里是Sub1 – C類型

       :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類型

       :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類型

       :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類型

       :00401025      6A02                            push 00000002

       :00401027      6A01                            push 00000001

       :00401029      E8D2FFFFFF               call 00401000

       :0040102E      83C408                        add esp,00000008

       ;這里是invoke Sub2,1,2       -- PASCAL類型

       :00401031      6A01                            push 00000001

       :00401033      6A02                            push 00000002

       :00401035      E8D1FFFFFF               call 0040100B

       ;這里是invoke Sub3,1,2 – StdCall類型

       :0040103A      6A02                            push 00000002

       :0040103C      6A01                            push 00000001

       :0040103E      E8D5FFFFFF               call 00401018

可以清楚地看到,在參數(shù)入棧順序上,C類型和StdCall類型是先把右邊的參數(shù)先壓入堆棧,而PASCAL類型是先把左邊的參數(shù)壓入堆棧。在堆棧平衡上,C類型是在調(diào)用者在使用call指令完成后,自行用add esp,8指令把8個字節(jié)的參數(shù)空間清除,而PASCALStdCall的調(diào)用者則不管這個事情,堆棧平衡的事情是由子程序用ret 8來實現(xiàn)的ret指令后面加一個操作數(shù)表示在ret后把堆棧指針esp加上操作數(shù),完成的是同樣的功能

 

Win32約定的類型是StdCall,所以在程序中調(diào)用子程序或系統(tǒng)API后,不必自己來平衡堆棧,免去了很多麻煩。

 

存取參數(shù)和局部變量都是通過堆棧來定義的,所以參數(shù)的存取也是通過ebp做指針來完成的。在探討局部變量的時候,已經(jīng)就沒有參數(shù)的情況下ebp指針和局部變量的對應(yīng)關(guān)系做了分析,現(xiàn)在來分析一下ebp指針和參數(shù)之間的對應(yīng)關(guān)系,注意,這里是以Win32中的StdCall為例,不同的語言類型,指針的順序可能是不同的。

 

假定在一個子程序中有兩個參數(shù),主程序調(diào)用時在push第一個參數(shù)前的堆棧指針espX,那么壓入兩個參數(shù)后的espX-8,程序開始執(zhí)行call指令,call指令把返回地址壓入堆棧,這時候espX-C,接下去是子程序中用push ebp來保存ebp的值,esp變?yōu)?/span>X-10,再執(zhí)行一句mov ebp,esp,就可以開始用ebp存取參數(shù)和局部變量了。

 

在源程序中,由于參數(shù)、局部變量和ebp的關(guān)系是由編譯器自動維護(hù)的,所以讀者不必關(guān)心它們的具體關(guān)系,但到了用Soft-ICE等工具來分析其他軟件的時候,遇到調(diào)用子程序的時候一定要先看清楚它們之間的類型差別。

 

在子程序中使用參數(shù),可以使用與存取局部變量同樣的方法,因為這兩者的構(gòu)造原理幾乎一模一樣,所以,在子程序中有invoke語句時,如果要用到輸入?yún)?shù)的地址當(dāng)做invoke的參數(shù),同樣要遵循局部變量的使用方式,不能用offset偽操作符,只能用addr來完成。同樣,所有對局部變量使用的限制幾乎都可以適用于參數(shù)。

 

 

 

 

高級語法

以前高級語言和匯編的最大差別就是條件測試、分支和循環(huán)等高級語法。

高級語言中,程序員可以方便地用類似于ifcaseloopwhile等語句來構(gòu)成程序的結(jié)構(gòu)流程,不僅條理清楚、一目了然,而且維護(hù)性相當(dāng)好。而匯編程序員呢?只能在cmp指令后面絞盡腦汁地想究竟用幾十種跳轉(zhuǎn)語句中的哪一種,這里就能列出近三十個條件跳轉(zhuǎn)指令來: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等。雖然其中的很多指令我們一輩子也不會用到,但就是這些指令和一些looploopnz以及被loop涉及的ecx等寄存器糾纏在一起,使在匯編中書寫結(jié)構(gòu)清晰、可讀性好的代碼變得相當(dāng)困難,這也是很多人視匯編為畏途的一個原因。

 

現(xiàn)在好了,MASM中新引入了一系列的偽指令,涉及條件測試、分支和循環(huán)語句,利用它們,匯編語言有了和高級語言一樣的結(jié)構(gòu),配合對局部變量和調(diào)用參數(shù)等高級語言中覺元素的支持,為使用Win32匯編編寫大規(guī)模的應(yīng)用程序奠定了基礎(chǔ)。

 

 

條件測試語句

在高級語言中,所有的分支和循環(huán)語句首先要涉及條件測試,也就是涉及一個表達(dá)式的結(jié)果是真還是假的問題,表達(dá)式中往往有用來做比較和計算的操作符,MASM也不例外,這就是條件測試語句。

 

MASM條件測試的基本表達(dá)式是:

       寄存器或變量 操作符 操作數(shù)

兩個以上的表達(dá)式可以用邏輯運算符連接:

       (表達(dá)式1) 邏輯運算符 (表達(dá)式2) 邏輯運算符 (表達(dá)式3) …

允許的操作符和邏輯運算符如下所示:

                                   條件溑或的操作符

操作符和邏輯運算符

操作

用途

==

等于

變量和操作數(shù)之間的比較

!=

不等于

變量和操作數(shù)之間的比較

大于

變量和操作數(shù)之間的比較

>=

大于等于

變量和操作數(shù)之間的比較

小于

變量和操作數(shù)之間的比較

<=

小于等于

變量和操作數(shù)之間的比較

&

位測試

將變量和操作數(shù)做與操作

!

邏輯取反

對變量取反或?qū)Ρ磉_(dá)式的結(jié)果取反

&&

邏輯與

對兩個表達(dá)式的結(jié)果進(jìn)行邏輯與操作

||

邏輯或

對兩個表達(dá)式的結(jié)果進(jìn)行邏輯或操作

舉例,左邊為表達(dá)式,右邊是表達(dá)式為真的條件:

x == 3                          ;x等于3

eax != 3                       ;eax不等于3

(y>=3) && ebx             ;y大于等于3ebx為非零值

(z&1) ||!eax                  ;z1進(jìn)行“與”操作后非零或eax取反后非零

                                   ;也就是說z的位0等于1eax為零

細(xì)心的讀者一定會發(fā)現(xiàn),MASM的條件測試采用的是和C語言相同的語法。如!&是對變量的操作符(取反和與操作),||&&是表達(dá)式結(jié)果之間的邏輯與和邏輯或,而==!=><等是比較符。同樣,對于不含比較符的單個變量或寄存器,MASM也是將所有非零認(rèn)為是真,零值認(rèn)為是假。

 

MASM的條件測試語句有幾個限制,首先是表達(dá)式的左邊只能是變量或寄存器,不能為常數(shù);其次表達(dá)的兩邊不能同時為變量,但可以同時是寄存器。這些限制來自于80x86的指令,因為條件測試偽操作符只是簡單地把每個表達(dá)式翻譯成cmptest指令,80x86的指令集中沒有cmp 0,eax之類的指令,同時也不允許直接操作兩個內(nèi)存中的數(shù),所以對這兩個限制是很好理解的。

 

除了這些和高級語言類似的條件測試偽操作,匯編語言還有特殊的要求,就是程序中常常要根據(jù)系統(tǒng)標(biāo)志寄存器中的各種標(biāo)志位來做條件跳轉(zhuǎn),這些在高級語言中是用不到的,所以又增加了以下一些標(biāo)志位的狀態(tài)指示,它們本身相當(dāng)于一個表達(dá)式:

       CARRY?                表示Carry位是否置位

       OVERFLOW?        表示Overflow位是否置位

       PARITY?               表示Parity位是否置位

       SIGN?                  表示Sign位是否置位

       ZERO?                  表示Zero位是否置位

 

要測試eax等于ebx同時Zero位置位,條件表達(dá)式可以寫為:

       (eax == ebx) && ZERO?

要測試eaxebx同時Zero位清零,條件表達(dá)式可以寫為:

       (eax == ebx) && !ZERO?

C語言的條件測試同樣,MASM的條件測試偽指令并不會改變被測試的變量或寄存器的值,只是進(jìn)行測試而已,到最后它會被編譯器翻譯成類似于cmptest之類的比較或位測試指令。

 

 

分支語句

分支語句用來根據(jù)條件表達(dá)式測試的真假執(zhí)行不同的代碼模塊,MASM中的分支語句的語法如下:

       .if 條件表達(dá)式1

              表達(dá)式1為“真”時執(zhí)行的指令

       [.elseif 條件表達(dá)式2]

              表達(dá)式2為“真”時執(zhí)行的指令

       [.elseif 條件表達(dá)式3]

              表達(dá)式3為“真”時執(zhí)行的指令

       ……

       [.else]

              所有表達(dá)式為“否”時執(zhí)行的指令

       .endif

注意:關(guān)鍵字if/elseif/else/endif的前面有個小數(shù)點,如果不加小數(shù)點,就變成宏匯編中的條件匯編偽操作了,結(jié)果可是天差地別。

 

這些偽指令把匯編程序的可讀性基本上提高到了高級語言的水平。

 

注意:使用.if/.else/.endif構(gòu)成分支偽指令的時候,不要漏寫前面的小數(shù)點,if/else/endif是宏匯編中條件匯編宏操作的偽操作指令,作用是根據(jù)條件決定在最后的可執(zhí)行文件中包不包括某一段代碼。這和.if/.else/.endif構(gòu)成分支的偽指令完全是兩回事情。

 

 

循環(huán)語句

循環(huán)是重復(fù)執(zhí)行的一組指令,MASM的循環(huán)偽指令可以根據(jù)條件表達(dá)式的真假來控制循環(huán)是否繼續(xù),也可以在循環(huán)體中直接退出,使用循環(huán)的語法是:

       .while      條件測試表達(dá)式

              指令

              [.break [.if 退出條件]]

              [.continue]

       .endw

       .repeat

              指令

              [.break [.if 退出條件]]

              [.continue]

       .until 條件測試表達(dá)式 (.untilcxz [條件測試表達(dá)式])

 

.while/.endw循環(huán)首先判斷條件測試表達(dá)式,如果結(jié)果是真,則執(zhí)行循環(huán)體內(nèi)的指令,結(jié)束后再回到.while處判斷表達(dá)式,如此往復(fù),一直到表達(dá)式結(jié)果為假為止。.while/.endw指令有可能一遍也不會執(zhí)行到循環(huán)體內(nèi)的指令,因為如果第一次判斷表達(dá)式時就遇到結(jié)果為假的情況,那么就直接退出循環(huán)。

 

.repeat/.until循環(huán)首先執(zhí)行一遍循環(huán)體內(nèi)的指令,然后再判斷條件測試表達(dá)式,如果結(jié)果為真的話,就退出循環(huán),如果為假,則返回.repeat處繼續(xù)循環(huán),可以看出,.repeat/.until不管表達(dá)式的值如何,至少會執(zhí)行一遍循環(huán)體內(nèi)的指令。

 

也可中以把條件表達(dá)式直接設(shè)置為固定值,這樣就可以構(gòu)建一個無限循環(huán),對于.while/.end直接使用TRUE,對于.repeat/until直接使用FALSE來當(dāng)表達(dá)式就是如此,這種情況下,可以使用.break偽指令強制退出循環(huán),如果.break偽指令后面跟一個.if測試偽指令的話,那么當(dāng)退出條件為真時才執(zhí)行.break偽指令。

 

在循環(huán)體中也可以用.continue偽指令忽略以后的指令,遇到.continue偽指令時,不管下面還有沒有其他循環(huán)體中的指令,都會直接回到循環(huán)頭部開始執(zhí)行。

 

 

 

代碼風(fēng)格

隨著程序功能的增加和版本的提高,程序越來越復(fù)雜,源文件也越來越多,風(fēng)格規(guī)范的源程序會對軟件的升級、修改和維護(hù)帶來極大的方便,要想開發(fā)一個成熟的軟件產(chǎn)品,必須在編寫源程序的時候就有條不紊,細(xì)致嚴(yán)謹(jǐn)。

 

在編程中,在程序排版、注釋、命名和可讀性等問題上都有一定的規(guī)范,雖然編寫可讀性良好的代碼并不是必然的要求,但好的代碼風(fēng)格實際上是為自己將來維護(hù)和使用這些代碼節(jié)省時間。

 

下面是對匯編語言代碼風(fēng)格的建議。

   

變量和函數(shù)的命名

匈牙利表示法

匈牙利表示法主要用在變量和子程序的命名,這是現(xiàn)在大部分程序員都在使用的命名約

定。匈牙利表示法這個奇怪的名字是為了紀(jì)念匈牙利籍的Microsoft 程序員Charles

Simonyi,他首先使用了這種命名方法。

 

匈牙利表示法用連在一起的幾個部分來命名一個變量,格式是類型前綴加上變量說明,類型用小寫字母表示,如用h表示句柄,用dw表示double word,用sz表示以0結(jié)尾的字符串等,說明則用首字母大寫的幾個英文單詞組成,如TimeCounterNextPoint等,可以令人一眼看出變量的含義來,在匯編語言中常用字的類型前綴有:

       b            表示byte

       w           表示word

       dw          表示dword

       h            表示句柄

       lp            表示指針

       sz           表示以0結(jié)尾的字符串

       lpsz         表示指向以0結(jié)尾的字符串的指針

       f             表示浮點數(shù)

       st            表示一個數(shù)據(jù)結(jié)構(gòu)

 

這樣一來,變量的意思就很好理解:

       hWinMain              主窗口的句柄

       dwTimeCount        時間計數(shù)器,以雙字定義

       szWelcome            歡迎信息字符串,以0結(jié)尾

       lpBuffer                 指向緩沖區(qū)的指針

 

很明顯,這些變量名比count1abccommandlinebufferFILEFLAG之類的命名要易于理解。由于匈牙利表示法既描述了變量的類型,又描述了變量的作用,所以能幫助程序員及早發(fā)現(xiàn)變量的使用錯誤,如把一個數(shù)值當(dāng)指針來使用引發(fā)的內(nèi)存頁錯誤等。

 

對于函數(shù)名,由于不會返回多種類型的數(shù)值,所以命名時一般不再用類型開頭,但名稱還是用表示用途的單詞組成,每個單詞的首字母大寫。Windows API是這種命名方式的絕好例子,當(dāng)人們看到ShowWindowGetWindowTextDeleteFileGetCommandLine之類的API函數(shù)名稱時,恐怕不用查手冊,就能知道它們是做什么用的。比起int 21h/09hint 13h/02h之類的中斷調(diào)用,好處是不必多講的。

 

對匈牙利表示法的補充

使用匈牙利表示法已經(jīng)基本上解決了命名的可讀性問題,但相對于其他高級語言,匯編語言有語法上的特殊性,考慮下面這些匯編語言特有的問題:

·對局部變量的地址引用要用lea指令或用addr偽操作,全局變量要用offset;對局部變量的使用要特別注意初始化問題。如何在定義中區(qū)分全局變量、局部變量和參數(shù)?

·匯編的源代碼占用的行數(shù)比較多,代碼行數(shù)很容易膨脹,程序規(guī)模大了如何分清一個函數(shù)是系統(tǒng)的API還是本程序內(nèi)部的子程序?

 

實際上上面的這些問題可以歸納為區(qū)分作用域的問題。為了分清變量的作用域,命名中對全局變量、局部變量和參數(shù)應(yīng)該有所區(qū)別,所以我們需要對匈牙利表示法做一些補充,以適應(yīng)Win32匯編的特殊情況,下面的補充方法僅供參考:

·全局變量的定義使用標(biāo)準(zhǔn)的匈牙利表示法,在參數(shù)的前面加下劃線,在局部變量的前面加@符號,這樣引用的時候就能隨時注意到變量的作用域。

·在內(nèi)部子程序的名稱前面加下劃線,以便和系統(tǒng)API區(qū)別。

 

如下面是一個求復(fù)數(shù)模的子程序,子程序名前面加下劃線表示這是本程序內(nèi)部模塊,兩個參數(shù)——復(fù)數(shù)的實部和虛部用_dwX_dwY表示,中間用到的局部變量@dwResult則用@號開頭:

       _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

 

(說實話,上面這段Win32匯編子程序,我只能看懂20%。看了一個月的匯編了,痛哉!痛哉!)

 

 

代碼的書寫格式

排版方式

程序的排版風(fēng)格應(yīng)該遵循以下規(guī)則。

首先是大小寫的問題,匯編程序中對于指令和寄存器的書寫是不分大小寫的,但小寫代碼比大寫代碼便于閱讀,所以程序中的指令和寄存器等要采用小寫字母,而用equ偽操作符定義的常量則使用大寫,變量和標(biāo)號使用匈牙利表示法,大小寫混合。

 

其次是使用Tab的問題。匯編源程序中Tab的寬度一般設(shè)置為8個字符。在語法上,指令和操作數(shù)之間至少有一個空格就可以了,但指令的助記符長度是不等長的,用Tab隔開指令和操作數(shù)可以使格式對齊,便于閱讀。如:

       xor eax,eax

       fistp dwNumber

       xchg eax,ebx

上述代碼的寫法就不如下面的寫法整齊:

       xor         eax,eax

       fistp        dwNumber

       xchg      eax,ebx

 

還有就是縮進(jìn)格式的問題。程序中的各部分采用不同的縮進(jìn),一般變量和標(biāo)號的定義不縮進(jìn),指令用兩個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)嵌套錯誤,當(dāng)縮進(jìn)過多的時候,可以意識到嵌套過深,該改進(jìn)程序結(jié)構(gòu)了。

 

 

注釋和空行

沒有注釋的程序是很難維護(hù)的,但注釋的方法也很有講究,寫注釋要遵循以下的規(guī)則:

·不要寫無意義的注釋,如:將1放到eax中,跳轉(zhuǎn)到 exit標(biāo)號處。

·修改代碼同時修改相應(yīng)的注釋,以保證注釋與代碼的一致性。

·注釋以描寫一組指令實現(xiàn)的功能為主,不要解釋單個指令的用法,那是應(yīng)該由指令手冊來完成的,不要假設(shè)看程序的人連指令都不熟悉。

·對于子程序,要在頭部加注釋說明參數(shù)和返回值,子程序可以實現(xiàn)的功能,以及調(diào)用時應(yīng)該注意的事項。

 

由于匯編語言是以一條指令為一行的,實現(xiàn)一個小功能就需要好幾行,沒有分段的程序很難看出功能模塊來,所以要合理利用空行來隔開不同的功能塊,一般以在高級語言中可以用一句語句來完成的一段匯編指令為單位插入一個空行。

 

 

避免使用宏

MASM的宏功能中最好只使用條件匯編,用來選擇編譯不同的代碼塊來構(gòu)建不同的版本,其他如宏定義和宏調(diào)用只會破壞程序的可讀性,能夠不用就盡量不用,雖然展開后只有一兩句的宏定義不在此列,但既然展開后也只有一兩句,那么和直接使用指令也就沒有什么區(qū)別了。

 

在匯編中避免使用宏定義的理由是:匯編中隨時要用到各個寄存器,宏定義不同于子程序,可以有選擇地保護(hù)現(xiàn)場,在使用中很容易忽略里面用了哪個寄存器,從而對程序結(jié)構(gòu)構(gòu)成威脅。高級語言的宏定義則不會有這個問題。

 

最極端的使用宏定義的程序是MicroMediaDirector SDK100行左右的例子中幾乎有90%都是宏定義,雖然例子很容易改成其他功能的程序,但要在里面加新的功能則幾乎是不可能的,因為程序中連C語言函數(shù)開始和結(jié)束的花括號都被改成了宏定義,這樣一來,如果要真正使用這個開發(fā)包,則必須把宏定義“翻譯”回原來的樣子才能真正理解程序的流程。

 

 

代碼的組織

程序中要注意變量的組織和模塊的組織方式。

過多的全局變量會影響程序的模塊化結(jié)構(gòu),所以不要設(shè)置沒必要的全局變量,盡量把變量定義成局部變量。

把僅在子程序中使用的變量設(shè)置為局部變量可以使子程序更容易封裝成一個黑匣子,如果無法把全部變量設(shè)置為局部變量,則盡量把這些數(shù)據(jù)改為參數(shù)輸入輸出,如果無法改為參數(shù),那么意味著這個子程序不能不經(jīng)修改地直接放到別的程序中使用。

 

在主程序中使用比較頻繁的部分,以及便于封裝成黑匣子在別的程序上用的代碼,都應(yīng)該寫上子程序,但一個子程序的規(guī)模不應(yīng)該太大,行數(shù)盡量限制在幾百行之內(nèi),功能則限于完成單個功能。對于子程序,定義參數(shù)的時候要盡可能精簡,對可能引起程序崩潰的參數(shù),如指針等,要進(jìn)行合法性檢測。

 

子程序中在使用完申請的資源的時候,注意在退出前要釋放所用資源,包括申請的內(nèi)存和其他句柄等,對于打開的文件則要關(guān)閉。

 

對于程序員來說,開發(fā)每一個軟件都是要從頭做起是很浪費時間的,一般的做是從自己以前做的程序中拷貝相似的代碼,但修改還是要花一定時間,最好的辦法就是盡量把子程序做成一個黑匣子,可以不經(jīng)修改地直接拿過來用,這樣,每次編程相當(dāng)于只是編寫新增的部分,隨著代碼的積累,開發(fā)任何程序都將是很快的事情。

 

 

posted on 2010-08-11 16:49 luqingfei 閱讀(10952) 評論(0)  編輯 收藏 引用 所屬分類: Win32匯編程語言序設(shè)計

導(dǎo)航

<2010年8月>
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234

統(tǒng)計

留言簿(6)

隨筆分類(109)

隨筆檔案(105)

Blogers

Game

Life

NodeJs

Python

Useful Webs

大牛

搜索

積分與排名

最新評論

閱讀排行榜

評論排行榜

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            鲁大师成人一区二区三区| 久久久久国产一区二区三区四区 | 亚洲桃色在线一区| 一本色道久久88精品综合| av72成人在线| 亚洲欧美日韩精品久久久久| 亚洲欧美日本在线| 久久久久久久国产| 亚洲国产精品高清久久久| 亚洲激情在线视频| 亚洲影视在线| 免费观看30秒视频久久| 欧美精品免费观看二区| 国产麻豆精品久久一二三| 激情久久久久| 亚洲欧美日本国产专区一区| 久久免费午夜影院| 在线看成人片| 这里只有精品视频| 久久久亚洲欧洲日产国码αv | 在线视频精品一区| 香蕉久久夜色| 亚洲国产裸拍裸体视频在线观看乱了 | 99在线观看免费视频精品观看| 亚洲影视九九影院在线观看| 久久香蕉国产线看观看网| 亚洲欧洲一区二区在线播放| 欧美一级二区| 欧美性生交xxxxx久久久| 精品成人久久| 午夜精品av| 亚洲另类在线一区| 美女黄色成人网| 国产色综合网| 亚洲综合第一页| 亚洲另类春色国产| 欧美.日韩.国产.一区.二区| 国产精品午夜在线| 一区二区三区精品国产| 欧美α欧美αv大片| 亚洲一区二区视频在线观看| 欧美国产在线观看| 91久久精品美女| 麻豆精品在线播放| 欧美一区二区三区四区在线| 国产精品久久久久久av福利软件| 亚洲美女精品成人在线视频| 久久天天狠狠| 久久狠狠婷婷| 黄色一区二区在线| 久久精品国亚洲| 亚洲永久免费精品| 国产精品一页| 欧美在线视频观看| 午夜精品一区二区三区在线视| 欧美伦理视频网站| 亚洲美女在线看| 亚洲国产精品一区| 欧美国产在线观看| 99re6热在线精品视频播放速度| 久久伊人一区二区| 欧美一区二区| 激情视频一区| 欧美激情精品久久久久久大尺度 | 国产乱码精品一区二区三| 亚洲视频在线一区观看| 亚洲精品午夜精品| 欧美视频在线免费| 欧美在线高清| 亚洲永久免费av| 一区二区三区四区精品| 欧美性一区二区| 欧美一级在线播放| 欧美尤物巨大精品爽| 国产一区二区三区在线观看免费| 久久精品免费看| 久久精品国产2020观看福利| 永久免费精品影视网站| 欧美国产亚洲另类动漫| 欧美精品一区二区三区视频 | 国产亚洲一区在线| 欧美h视频在线| 亚洲欧美日韩国产精品| 在线成人中文字幕| 亚洲精品护士| 国产精品综合不卡av| 久久久免费观看视频| 久久综合五月| 亚洲无亚洲人成网站77777| 亚洲一区欧美| 亚洲国产精品综合| 中文欧美在线视频| 精品动漫一区二区| 一区二区三区欧美成人| 国产精品永久在线| 欧美激情国产日韩| 国产精品午夜春色av| 欧美va天堂va视频va在线| 欧美日本在线| 久久精品一区中文字幕| 免费国产一区二区| 亚洲欧洲av一区二区三区久久| 久久久99精品免费观看不卡| 亚洲视频第一页| 久久视频国产精品免费视频在线| 一区二区欧美日韩视频| 久久av红桃一区二区小说| 亚洲美女诱惑| 久久久久久伊人| 欧美一区二区| 欧美日韩一区二区免费视频| 免费国产一区二区| 国产日韩欧美高清免费| 一本一道久久综合狠狠老精东影业| 国产最新精品精品你懂的| 一本一本久久a久久精品牛牛影视| 曰韩精品一区二区| 性欧美大战久久久久久久久| 亚洲无亚洲人成网站77777 | 欧美在线高清| 亚洲欧美日韩国产一区二区| 欧美激情a∨在线视频播放| 久久这里只有| 国产有码在线一区二区视频| 亚洲一区二区三区四区视频| 亚洲手机成人高清视频| 欧美国产极速在线| 亚洲福利视频二区| 在线观看亚洲精品| 久久全球大尺度高清视频| 国产亚洲欧美一区| 亚洲亚洲精品在线观看| 亚洲一级影院| 欧美视频二区| 夜夜嗨av一区二区三区四区 | 女人香蕉久久**毛片精品| 国产一区二区在线观看免费| 亚洲女同同性videoxma| 午夜精品免费| 国产性色一区二区| 久久久久久精| 亚洲电影免费观看高清完整版在线| 影音先锋日韩资源| 欧美成人a视频| 亚洲人成网站999久久久综合| 亚洲乱码精品一二三四区日韩在线 | 91久久在线观看| 99精品国产在热久久婷婷| 欧美日韩国产123区| 亚洲精品一区二区三| 亚洲一区精品在线| 国产麻豆综合| 久久久91精品国产| 亚洲成色最大综合在线| 日韩亚洲国产欧美| 国产精品久久久一区二区| 香蕉久久一区二区不卡无毒影院| 久久久免费观看视频| 最新日韩av| 国产精品久99| 久久久人成影片一区二区三区| 欧美成人激情在线| 亚洲手机成人高清视频| 国产综合色一区二区三区| 可以看av的网站久久看| 日韩写真在线| 每日更新成人在线视频| 亚洲精品一区二区三区99| 欧美三级第一页| 久久久免费精品| 在线亚洲成人| 欧美高潮视频| 欧美一级欧美一级在线播放| 亚洲黄色性网站| 国产视频丨精品|在线观看| 欧美大片一区二区| 亚洲欧美日韩区| 亚洲精品乱码久久久久久日本蜜臀 | 另类激情亚洲| 亚洲中字在线| 亚洲日韩第九十九页| 国产午夜精品视频免费不卡69堂| 六月婷婷久久| 欧美一区二区三区的| 99re国产精品| 欧美韩国日本一区| 久久久天天操| 欧美一级黄色录像| 亚洲一区二区不卡免费| 亚洲国产精品久久久久婷婷老年| 国产精品日韩在线| 欧美日韩免费视频| 美女精品自拍一二三四| 国产一区二区三区在线观看精品| 亚洲人成网站777色婷婷| 久久久久国产精品麻豆ai换脸 | 久久综合狠狠综合久久综合88| 亚洲图片在线| 亚洲另类自拍| 亚洲黄色免费网站|