• <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>

            luqingfei@C++

            為中華之崛起而崛起!
            兼聽(tīng)則明,偏聽(tīng)則暗。

            Win32匯編--使用MASM

             

            使用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)很新鮮的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

            怎么樣,看來(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就夠了。80486Pentium處理器指令是80386處理器指令的超集,同樣道理,如果程序中要用80486處理器或Pentium處理器的指令,則必須定義.486.586。

             

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

                                        .386

                                        .mmx

             

            2model語(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ì)比一下DOSHello WorldWin32Hello 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)有一處可以找到dses等段寄存器的使用,因?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ō),CSDS,SSES段全部使用平坦模式,FSGS寄存默認(rèn)不使用,這時(shí)若在源程序中使用FSGS,在編譯時(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,FORTRANPASCALL,雖然各種高級(jí)語(yǔ)言在調(diào)用子程序時(shí)都是使用堆棧來(lái)傳遞參數(shù)。WindowsAPI調(diào)用使用是的stdcall格式,所以在Win32匯編中沒(méi)有選擇,必須在.model中加上stdcall參數(shù)。

             

            3option語(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)題。

             

             

             

            段的定義

            段的概念

            把上面的Win32Hello 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í)行文件的壓縮軟件和加殼軟件,如UpxPeCompact等,這些軟件靠把代碼段進(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 21hDOS中斷,int 13hint 10hBIOS中的磁盤(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è)備接口。利用VGADRV之類(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ò)通信的DLLWsock32.dll,它所提供的API稱(chēng)為Socket API;專(zhuān)用于電話服務(wù)方面的API稱(chēng)為TAPITelephony 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ù)的定義,洋洋灑灑幾百MBWindows系統(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ō)明了MessageBox4個(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ù),所以這些HWNDLPCTSTRUINT實(shí)際上就是匯編中的dworddouble 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)了,Win32API動(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,FARNEAR16,NEAR32,FAR16FAR32,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,而只有MessageBoxAMessageBoxW,那么為什么還是可以使用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=1UNICODE=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ù):MessageBoxExitProcess,它們分別在User32.dllKernel32.dll中,在MASM32工具包中已經(jīng)包括了所有DLLAPI函數(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í)行文件中。DOSC語(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.libMASM32工具包中包含了所有DLL的導(dǎo)入庫(kù)。

             

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

                          includelib 庫(kù)文件名

                    includelib <庫(kù)文件名>

            include的用法一樣,在要包括讓編譯器混淆的文件名時(shí)加括號(hào)。

            Win32 Hello World程序用到的兩個(gè)API函數(shù)MessageBoxExitProcess分別在User32.dllKernel32.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_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就是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é)果常常用addr1addr2之類(lèi)的標(biāo)號(hào)一直延續(xù)下去,如果后來(lái)要在中間插一個(gè)標(biāo)號(hào),那么就常常出現(xiàn)addr1_1loop10_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è)名為wMinuteword類(lèi)型變量。

            3定義了一個(gè)雙字類(lèi)型的變量_hWnd。

            4定義了一組字,以000100020001,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)系。

             

            局部變量的定義

            MASMlocal偽指令提供了對(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è)變量,@loc1dword類(lèi)型,@loc2word類(lèi)型,@loc3byte類(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ù)量的pushpop指令來(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)是stylelpfnWndProccbClsExtra等,前面的UINTWNDPROC等是這些字段的類(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>lpfnWndProcstWndClass中的第二個(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, real8real10。如:

                   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這一句很顯然使al12h,下面的兩句呢,axeax難道等于0012h00000012h嗎?實(shí)際運(yùn)行結(jié)果是3412h78123412h,為什么呢?(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ù)在低地址,所以wTest21234h在內(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ù)12hwTest2的低位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è)雙字再放到axeax中,高位保持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中。

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

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

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

             

             

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

            在源程序中用到變量的尺寸和數(shù)量的時(shí)候,可以用sizeoflengthof偽指令來(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 WNDCLASSTlengthof dword是非法的用法,編譯程序會(huì)報(bào)錯(cuò)。

             

            要注意的是,sizeoflengthof的數(shù)值是編譯時(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í)際上是另一個(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-4400FCh,由于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ù)2addr szHello

                   push eax                ;參數(shù)1eax

                   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)然eaxaddr的后面的參數(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

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

                   距離。可以是NEAR,FAR,NEAR16,NEAR32FAR16FAR32Win32中只有一個(gè)平坦的段,無(wú)所謂距離,所以對(duì)距離的定義往往忽略。

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

                   可視區(qū)域,可以是PRIVATE,PUBLICEXPORTPRIVATE表示子程序只對(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é)尾用pushadpopad指令一次保存和恢復(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匯編中唯一使用VARARGAPI就是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ù)空間清除,而PASCALStdCall的調(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ù)前的堆棧指針espX,那么壓入兩個(gè)參數(shù)后的espX-8,程序開(kāi)始執(zhí)行call指令,call指令把返回地址壓入堆棧,這時(shí)候espX-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,loopwhile等語(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大于等于3ebx為非零值

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

                                               ;也就是說(shuō)z的位0等于1eax為零

            細(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á)式翻譯成cmptest指令,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è)試eaxebx同時(shí)Zero位清零,條件表達(dá)式可以寫(xiě)為:

                   (eax == ebx) && !ZERO?

            C語(yǔ)言的條件測(cè)試同樣,MASM的條件測(cè)試偽指令并不會(huì)改變被測(cè)試的變量或寄存器的值,只是進(jìn)行測(cè)試而已,到最后它會(huì)被編譯器翻譯成類(lèi)似于cmptest之類(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,commandlinebufferFILEFLAG之類(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,GetWindowTextDeleteFileGetCommandLine之類(lèi)的API函數(shù)名稱(chēng)時(shí),恐怕不用查手冊(cè),就能知道它們是做什么用的。比起int 21h/09hint 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)題。

             

            最極端的使用宏定義的程序是MicroMediaDirector 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ā)任何程序都將是很快的事情。

             

             

            posted on 2010-08-11 16:49 luqingfei 閱讀(10889) 評(píng)論(0)  編輯 收藏 引用 所屬分類(lèi): Win32匯編程語(yǔ)言序設(shè)計(jì)

            導(dǎo)航

            <2010年11月>
            31123456
            78910111213
            14151617181920
            21222324252627
            2829301234
            567891011

            統(tǒng)計(jì)

            留言簿(6)

            隨筆分類(lèi)(109)

            隨筆檔案(105)

            Blogers

            Game

            Life

            NodeJs

            Python

            Useful Webs

            大牛

            搜索

            積分與排名

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            精品久久久久中文字幕日本| 国产精品99久久久久久董美香| www.久久精品| 亚洲国产婷婷香蕉久久久久久| 国产精品久久久久9999高清| 伊人色综合久久天天人守人婷| 日本精品久久久久中文字幕8| 国内精品伊人久久久影院| 91久久香蕉国产熟女线看| 久久精品亚洲精品国产色婷| 99久久免费国产精品特黄| 国产2021久久精品| 99精品国产在热久久| 久久久SS麻豆欧美国产日韩| 久久亚洲天堂| 久久AAAA片一区二区| 久久无码av三级| 粉嫩小泬无遮挡久久久久久| 狠狠色噜噜色狠狠狠综合久久| 久久综合成人网| 久久精品国产99久久久香蕉| 色综合合久久天天综合绕视看| 久久精品国产亚洲77777| 狠狠色婷婷久久综合频道日韩| 97精品伊人久久大香线蕉| 青春久久| 亚洲欧美成人久久综合中文网| 久久精品国产99久久丝袜| 91久久精品电影| 国产精品gz久久久| 国产99久久久国产精品~~牛| 久久亚洲综合色一区二区三区| 国产精品久久久久久| 久久福利青草精品资源站| 99久久精品国产麻豆| 国产美女久久精品香蕉69| 国产精品无码久久综合| 国产高潮国产高潮久久久| 国产精品久久久久久| 亚洲精品国产成人99久久| 亚洲综合久久综合激情久久|