Fasm---Win32彙編學習3

Fasm---Win32彙編學習3

                                         第三課-完整的界面

  

在今天這節課程中,我們來寫一個Windows的界面。

 

理論:

    Windows程序中,在寫圖形界面時需要調用大量的標準 Windows GUI函數。其實這對於程序員和用戶都是好事。對於用戶,面對的是同一套標準的窗口,對這些窗口的操作都是一樣的,所以使用不同的應用程序無須每次都要進行重新學習操作。對程序員來說,這些Gui源碼都是經過了無數次的測試,隨時都可以拿來用。當然至於具體地寫程序對於程序員來說還是有難度的。爲了創建基於窗口的應用程序,必須嚴格遵守Windows的規範。做到這一點不難,只要用基於模塊化或面向對象的編程方法即可。。

 

 其實我們可以這樣想,在windows中我們就處於另一個虛擬世界,那麼這個世界裏有很多預定義的對象,那麼每個對象就是windows這個世界之前靜態的定義的對象的實例。。      例如,每個進程就是windows靜態定義的“進程對象”的實例。。那麼每個窗口也就是windows預定義的窗口對象的實例,我們必須遵守這個規則,否則我們就會被踢出局。

 

那麼我們要想創建一個windows界面就得遵守以下的規則:

   下面我就列出在桌面顯示一個窗口的基本步驟。

    1.得到您應用程序的句柄。

    2.得到命令行參數。(可選)。

    3.註冊窗口類。

    4.產生窗口。(必須)

    5.在桌面顯示窗口。

    6.刷新窗口客戶區。

    7.進入無限獲取的窗口消息循環。

    8.如果有消息到達,則由負責該窗口的回調函數處理。

    9.如果用戶關閉窗口,進行退出處理。

 

    對於單用戶的dos下編程來說,windows下的程序框架結構是相當複雜的。但是windows和DOS是截然不同的。Windows是一個多任務的操作系統,故系統中同時有多個應用程序彼此協同工作,這就要求windows程序員必須嚴格恪守編程規範,養成良好的規範。

內容:下面是一段簡單的窗口程序源代碼。

 

 

說明:

  1.你最好把這個程序中所需要的所有的結構以及常量包含到一個頭文件中,然後在我們的程序中包含這個頭文件。當然這個我們的fasm已經幫我們做好了,我們只需要引用一個“win32ax.inc”頭文件即可,這個是個擴展的頭文件,裏面包含了一些擴展的宏語句,例如addr 取局部變量,否則如果我們引用“win32a.inc”,我們必須通過lea指令來獲取局部內存單元的偏移地址。

 

2.利用library宏語句來引用相應的我們需要調用函數所在的dll動態鏈接庫名稱,這樣以便我們編譯器構建輸入表。因爲windows是以動態鏈接庫的形式提供給我們接口函數的,我們要調用這些接口,必須通過指定相應的動態鏈接庫名稱以及函數名稱,並通過特定的格式夠雜輸入表,這樣我們的程序在被載入到內存後相應的動態鏈接庫纔會被載入內存,以及重定向我們調用函數的地址。。最後通過include語句包含相應的動態庫的頭文件,這裏麪包含了相應函數的格式。。。

 

3.在其他地方運用頭文件定義函數原型,常數和結構體的時,要嚴格保持和頭文件中一致,包括大小寫。在函數查詢時,這將節約你很多時間。

    format PE GUI 4.0
    include 'win32ax.inc'
   

macro memmov [dst, src]
{
        common
        push [src]
        pop [dst]
}

    LPSTR equ dd
    ;************************數據********************************
    szClassName db 'first Windows',0
    szWndName   db '我的第一個程序',0
    lpCommand LPSTR ?
    hIcon      rd 1
    hInstanse  rd 1
    hCursor    rd 1
    hWnd       rd 1


    entry $
        invoke GetModuleHandle,NULL
;必須的,我們必須獲得我們程序的模塊句柄。如上面說的,如果不遵循我們將over。
        mov    [hInstanse], eax       
;在win32模式下, hMoudule == hInstance  mov  [hInstance], eax
        invoke GetCommandLine,NULL
;不是必須的,如果你的程序不處理命令行,則這句代碼可以省去。
        mov [lpCommand], eax    
        stdcall _WinMain,hInstanse, NULL, [lpCommand], SW_SHOWDEFAULT
;調用主函數
        invoke ExitProcess,NULL
       
    proc _WinMain    hInstance:DWORD, hPrevInstance:DWORD, lpCmdLine:DWORD, nCmdShow:DWORD
        local @wc : WNDCLASSEX    
;創建局部變量
        local @msg : MSG
       
       
        invoke RtlZeroMemory,addr @wc,sizeof.WNDCLASSEX       
;填寫wc(WNDCLASSEX結構)的成員
        invoke LoadIcon,NULL, IDI_WINLOGO
        mov [hIcon], eax
        invoke LoadCursor,NULL, IDC_ARROW
        mov [hCursor], eax
        mov [@wc.cbSize], sizeof.WNDCLASSEX
        mov [@wc.style], CS_HREDRAW or CS_VREDRAW
        mov [@wc.lpfnWndProc], _WndProc
        mov [@wc.cbClsExtra], NULL
        mov [@wc.cbWndExtra], NULL
        memmov @wc.hInstance, hInstance
        memmov @wc.hIcon, hIcon
    memmov @wc.hCursor, hCursor
        mov [@wc.hbrBackground], COLOR_WINDOW
        mov [@wc.lpszMenuName], NULL
        mov [@wc.lpszClassName], szClassName
      
;註冊窗口類
        invoke RegisterClassEx,addr @wc 
       
;建立窗口
   
        invoke CreateWindowEx, NULL, szClassName, szWndName,/
WS_OVERLAPPEDWINDOW,/
    100, 100, 600, 400,/
    NULL, NULL, [hInstanse], NULL
        mov [hWnd], eax
        invoke ShowWindow,[hWnd],SW_SHOWNORMAL
        invoke UpdateWindow,[hWnd]
   
        GetMsg:
        invoke GetMessage,addr @msg, NULL, 0, 0
        or eax, eax
        jz EndMsg
        invoke TranslateMessage,addr @msg
        invoke DispatchMessage,addr @msg
        jmp GetMsg
       
EndMsg:
        mov eax, [@msg.wParam]
        ret
    endp
   
   
    proc _WndProc uses ebx esi edi,hWnd:DWORD, wMsg:DWORD, wParam:DWORD, lParam:DWORD
       
        cmp [wMsg], WM_DESTROY
        jz Quit
        invoke DefWindowProc,[hWnd],[wMsg],[wParam],[lParam]
        ret
   
   
    Quit:
        invoke PostQuitMessage,NULL
        jmp endWnd
       
   
    endWnd:
        xor eax,eax
        ret
    endp

   
;///////////////////////////輸入表////////////////////////////////////////////////

   
section '.import' data import readable writeable
       
   
    library kernel32, 'kernel32.dll',/
                user32, 'user32.dll'
    include 'api/kernel32.inc'
    include 'api/user32.inc'

分析:

  看到上面一堆代碼,是不是想撤,呵呵。我也有同樣的感覺,不過上面的是模板而已,模板是說上面的代碼對差不多所有的windows程序來說基本是相同的。在寫windows的時候,你可以將其拷來拷去,當然把這些代碼放到一個庫中也挺好。其實真正要寫的代碼在_WinMain中。這和一些c的編譯器一樣,無需關心其他雜物。唯一不同的是c編譯器要求你的源代碼中必須要有一個WinMain。否則c不知道將那個函數和有關前後代碼鏈接。相對c,彙編語言提供了較大的靈活性,它不強行叫WinMain函數。

下面開始分析,你可要做好準備,這可不是一件容易的事情。

 

    format PE GUI 4.0
    include 'win32ax.inc'

 

   entry $

  

  proc _WinMain        hInstance:DWORD, hPrevInstance:DWORD, lpCmdLine:DWORD, nCmdShow:DWORD

 

;///////////////////////////輸入表////////////////////////////////////////////////

   
section '.import' data import readable writeable
       
   
    library kernel32, 'kernel32.dll',/
                user32, 'user32.dll'
    include 'api/kernel32.inc'
    include 'api/user32.inc'

 

  你可以把前兩行以及最後的部分看成是必須的。

   format 指定我們的輸出文件格式(例如PE格式(32位), MZ格式(16位)), 後面的GUI表示我們的子系統是windows窗口模式       4.0爲子系統的版本號(一般編寫win32程序我們設置爲4.0)。

    接下來是我們的entry僞指令,這個僞指令是指定我們的入口點,$標示當前的地址。也就是指示我們當前的地址爲入口點。

  然後是我們的WinMain函數的原型,因爲稍後要用到,所以先聲明。我們我們必須包含win32ax.inc頭文件,因爲其中包含了一些頭文件中 是我們要用到的常量以及結構的定義。該文件是一個文本文件,你可以用任何編輯器打開。

  

   由於我們用到的相應的函數駐紮在user32.dll 和kernel32.dll中,所以我們必須通過library來引用相應的動態鏈接庫 。(譬如:RegisterClassEx -- user32.dll ,       ExitProcess -- kernel32.dll)。

   接下來我問您需要把什麼庫引入到程序中?

   答案是:先查你要調用的函數在什麼庫中,然後包含進來。 譬如:例如你要調用gdi32.dll中的函數,則必須引入gdi32.dll,以及相應庫的頭文件。 gid32.inc 。

 

 

 接下來是

   LPSTR equ dd
    ;************************數據********************************
    szClassName db 'first Windows',0
    szWndName   db '我的第一個程序',0
    szCommand LPSTR ?
    hIcon      rd 1
    hInstanse  rd 1
    hCursor    rd 1
    hWnd       rd 1

  這裏是定義的相應的數據,以及符號常量。。 

   符號常量是表示在編譯程序的時候,所有的符號常量都被其後的真實值所替代,有點像C語言的中宏。 符號常量通過equ僞指令來定義。格式由符號常量名跟equ來定義 。。

   db僞指令可以定義以''括起來的字符串,這樣編譯的時候這段地址空間將是一段字節序列。因爲我們windows中的字符串處理函數是以00來標示結尾的,所以我們在定義字符串的時候一定要在後面加上一個00。

   rd僞指令爲保留數據,也就是它們是未初始化的,程序在啓動時候它們是什麼值無關緊要,只不過佔了一段內存,以後在利用。

   hInstanse代表程序的實例句柄,lpCommand保存的是傳入過來的命令行參數。我們之所以通過定義符號常量只是用來幫助我們記憶。例如LPSTR equ dd,我們之後直接就可以引用LPSTR,我們看到就知道是什麼意思。。      這樣實質程序在編譯符號常量的時候是將其用真實值替換也就是用dd替換。

   entry $ 這裏包含了我們的所有代碼。

    entry $
        invoke GetModuleHandle,NULL
;必須的,我們必須獲得我們程序的模塊句柄。如上面說的,如果不遵循我們將over。
        mov    [hInstanse], eax       
;在win32模式下, hMoudule == hInstance  mov  [hInstance], eax
        invoke GetCommandLine,NULL
;不是必須的,如果你的程序不處理命令行,則這句代碼可以省去。
        mov [lpCommand], eax    
        stdcall _WinMain,hInstanse, NULL, [lpCommand], SW_SHOWDEFAULT
;調用主函數
        invoke ExitProcess,NULL

  我們的第一條語句GetModuleHandle來獲得我們程序的模塊句柄,在win32環境下模塊的句柄和程序的句柄是一樣的。你可以把實例句柄看成您應用程序的ID號。我們在調用幾個函數都是把它作爲參數來進行傳遞,所以在一開始獲得並保存它,省了很多事情。

 

特別注意:Win32環境下的實例句柄實際是您應用程序在內存的線性地址。

WIN32環境中如果函數由返回則,則是通過eax寄存器來傳遞的,其他的值可以用來傳遞參數地址來進行返回。一個win32函數被調用時,總是保存好段寄存器和ebx, esi edi 和ebp寄存器。而ecx 和edx值是不固定的,不能在返回時應用。特別注意:在windows api函數返回時, eax ecx edx的值和調用前不同。當函數返回時,返回值放在eax寄存器中。 如果你應用程序中的函數提供給windows調用,也必須遵守這一點,則必須在入口處保存段寄存器和 ebx esi edi ebp寄存器。如果不這樣以來的話,你的程序很容易崩潰。從您的程序中提供給 Windows 調用的函數大體上有兩種:Windows 窗口過程和 Callback 函數。

 

  如果你的程序不處理命令行參數,則無需調用GetCommandLine函數,這裏只是告訴你如果要調用該怎麼做。

  proc _WinMain        hInstance:DWORD, hPrevInstance:DWORD, lpCmdLine:DWORD, nCmdShow:DWORD

   上面是WinMain的定義,注意proc 後跟着函數名,這點和masm不同。。  函數名後面parameter:type 。它們是由調用者傳遞給WinMain的。我們直接引用參數名即可。至於退棧 壓棧 fasm在編譯的時候加入了前序和後序的處理指令。  local @wc : WNDCLASSEX    local @msg : MSG      僞指令用於在堆棧中分配內存。所有的local指令必須緊跟proc後。

  local後跟的局部變量的聲明格式是 local 變量名: 變量類型 。例如

  local @wc : WNDCLASSEX

    是告訴編譯器在棧中分配WNDCLASSEX結構體長度的內存空間,然後我們再使用的時候無需考慮堆棧的問題,考慮到dos下的彙編,這不能不說是一種恩賜。不過這就要求這樣申明的局部變量再函數結束時釋放棧空間,(也不能再函數體外被引用)。另一個缺點是你無法初始化你的局部變量,只能在稍後對其進行賦值。

 invoke RtlZeroMemory,addr @wc,sizeof.WNDCLASSEX       ;填寫wc(WNDCLASSEX結構)的成員
        invoke LoadIcon,NULL, IDI_WINLOGO
        mov [hIcon], eax
        invoke LoadCursor,NULL, IDC_ARROW
        mov [hCursor], eax
        mov [@wc.cbSize], sizeof.WNDCLASSEX
        mov [@wc.style], CS_HREDRAW or CS_VREDRAW
        mov [@wc.lpfnWndProc], _WndProc
        mov [@wc.cbClsExtra], NULL
        mov [@wc.cbWndExtra], NULL
        memmov @wc.hInstance, hInstance
        memmov @wc.hIcon, hIcon
    memmov @wc.hCursor, hCursor
        mov [@wc.hbrBackground], COLOR_WINDOW
        mov [@wc.lpszMenuName], NULL
        mov [@wc.lpszClassName], szClassName
      
;註冊窗口類
        invoke RegisterClassEx,addr @wc 
       
;建立窗口

上面的幾行代碼概念上說其實非常簡單,只要幾行代碼就可以了。其中主要的概念就是窗口類,一個窗口類就有窗口的規範,這個規範定義了幾個主要的窗口元素,如圖標,光標,背景色和負責處理該窗口的函數。你需要產生一個窗口必須要有這樣的窗口類,這是windows世界窗口對象的規範。如果你需要產生不止一個的窗口,最好的方法是將這個窗口存儲起來,這種方法可以節約很多的內存空間。也許今天你體會不到,但是你想想之前pc機只有一M內存的時候。這麼做是非常有必要的。如果您要定義自己創建窗口類就必須在一個WNDCLASS 或者WNDCLASSEX指明相關的成員。然後調用RegisterClass 或者 RegisterClassEx。在根據該窗口類,產生窗口。對不同特色的窗口定義不同的窗口類。windows有幾個預定義的窗口類,例如按鈕,編輯框等。要產生這種風格的窗口無需在定義了,只要包含相應的預定義的類名作爲參數調用給CreateWindowEx就可以了。

  

上面調用invoke RtlZeroMemory,addr @wc,sizeof.WNDCLASSEX     ;將我們的@wc結構初始化爲0。

  WNDCALSSEX結構最重要的成員是lpfnWndProc,它指向的是函數的一個長指針,在win32中由於內存模式是flat型,所以沒有near和far之分,每一個窗口類必須由一個窗口過程,當windows把屬於特定窗口的消息發送給窗口的時候,該窗口的窗口類負責處理所有的消息,如鍵盤消息,鼠標消息等。由於窗口過程智能的處理了所有的窗口消息循環,所以你只要在其加入消息處理過程即可。。下面我講解WNDCALSSEX的每一個成員。

STRUCT WNDCLASSEX
cbSize DWORD ?
style DWORD ?
lpfnWndProc DWORD ?
cbClsExtra DWORD ?
cbWndExtra DWORD ?
hInstance DWORD ?
hIcon DWORD ?
hCursor DWORD ?
hbrBackground DWORD ?
lpszMenuName DWORD ?
lpszClassName DWORD ?
hIconSm DWORD ?
ENDS

cbSize:WNDCLASSEX 的大小。我們可以用sizeof(WNDCLASSEX)來獲得準確的值。
style:從這個窗口類派生的窗口具有的風格。您可以用“or”操作符來把幾個風格或到一起。
lpfnWndProc:窗口處理函數的指針。
cbClsExtra:指定緊跟在窗口類結構後的附加字節數。
cbWndExtra:指定緊跟在窗口事例後的附加字節數。如果一個應用程序在資源中用CLASS僞指令註冊一個對話框類時,則必須把這個成員設成DLGWINDOWEXTRA。
hInstance:本模塊的事例句柄。
hIcon:圖標的句柄。
hCursor:光標的句柄。
hbrBackground:背景畫刷的句柄。
lpszMenuName:指向菜單的指針。
lpszClassName:指向類名稱的指針。
hIconSm:和窗口類關聯的小圖標。如果該值爲NULL。則把hCursor中的圖標轉換成大小合適的小圖標。

 註冊窗口類後,我們將調用CreateWindowEx來產生實際的窗口,請注意該函數有12個參數。

 

     invoke CreateWindowEx, NULL, szClassName, szWndName,/
WS_OVERLAPPEDWINDOW,/
    100, 100, 600, 400,/
    NULL, NULL, [hInstanse], NULL

我們來看下這個函數的參數

  dwExStyle    :附加的窗口風格,對於之前的createWindow這是一個新的參數。在9x/nt中你可以使用新的風格。你可以在dwExStyle中指定一般的窗口風格,但是一些特殊的窗口風格,如頂層窗口則必須在此參數中指定。如果你不想指定任何特別的風格,則把此參數設置NULL。

lpClassName :ASCIIZ形式的窗口類名稱的地址,可以是你自定義的類,也可以是你定義的類名。像上面說的,每個窗口必須有一個窗口類。

lpWindowName : ASCIIZ形式的窗口名稱的地址,該名稱會顯示到標題上。如果該參數空白,則標題欄什麼都米。

dwStyle : 窗口的風格,在此你可以指定窗口的外觀,可以指定該參數爲零,但是那樣該窗口就沒有系統菜單。也沒有最大化最小化按鈕。那樣你不得不按alt + F4 來進行關閉。最普通的窗口風格是WS_OVERLAPPEDWINDOW 。一種窗口風格是按位掩碼,你可以通過"or"來將其連接起來。WS_OVERLAPPEDWINDOW 就是由幾種不同的風格用or集成的。

x ,y:指定窗口左上角的以像素爲單位的屏幕座標位置,缺省的指定爲CW_USEDEFAULT,這樣windows則默認的選擇合適的位置。

nWidth, nHeight :以像素爲單位的窗口大小,缺省可以爲CW_USEDEFAULT。這樣 Windows 會自動爲窗口指定最合適的位置。

 

hWndParent:父窗口的句柄(如果有的話)。這個參數告訴 Windows 這是一個子窗口和他的父窗口是誰。這和 MDI(多文檔結構)不同,此處的子窗口並不會侷限在父窗口的客戶區內。他只是用來告訴 Windows 各個窗口之間的父子關係,以便在父窗口銷燬是一同把其子窗口銷燬。在我們的例子程序中因爲只有一個窗口,故把該參數設爲 NULL。

hMenu:Windows菜單的句柄,如果只用系統菜單,則此參數可以設置NULL。回頭看一看WNDCLASSEX 結構中的 lpszMenuName 參數,它也指定一個菜單,這是一個缺省菜單,任何從該窗口類派生的窗口若想用其他的菜單需在該參數中重新指定。其實該參數有雙重意義:一方面若這是一個自定義窗口時該參數代表菜單句柄,另一方面,若這是一個預定義窗口時,該參數代表是該窗口的 ID 號。Windows 是根據lpClassName 參數來區分是自定義窗口還是預定義窗口的。

hInstance: 產生該窗口的應用程序的實例句柄。

 

lpParam: (可選)指向欲傳給窗口的結構體數據類型參數的指針。如在MDI中在產生窗口時傳遞 CLIENTCREATESTRUCT 結構的參數。一般情況下,該值總爲零,這表示沒有參數傳遞給窗口。可以通過GetWindowLong 函數檢索該值。

 

        mov [hWnd], eax
        invoke ShowWindow,[hWnd],SW_SHOWNORMAL
        invoke UpdateWindow,[hWnd]
    調用CreateWindowEx函數成功後,返回值在eax寄存器中。因爲我們win32的api函數返回值都是在eax寄存器。我們必須保留已被後用,我們剛剛產生的窗口不會顯示,我們必須通過ShowWindow函數來顯示窗口。然後調用UpdateWindow函數來更新客戶區。

 

     
        GetMsg:
        invoke GetMessage,addr @msg, NULL, 0, 0
        or eax, eax
        jz EndMsg
        invoke TranslateMessage,addr @msg
        invoke DispatchMessage,addr @msg
        jmp GetMsg

此時我們已經可以顯示窗口了,但是此時的窗口不從外界接受消息。我們必須給他提供相關的消息。我們是通過一項消息循環來完成這樣的工作的。每一個模塊僅有一個消息循環,我們不斷的通過GetMessage來獲取windows維護給我們程序消息隊列中的消息。GetMessage傳遞一個MSG結構體給Windows。然後windows在此MSG結構體填充相關的消息,一直到windows填充好後,GetMessage才返回。在這段時間內,相關的控制權可能轉移給其他的程序,這樣就構成win16位下的 多任務。如果GetMessage獲取的是WM_QUIT消息就會返回false,使結束消息循環。TranslateMessage函數是一個實用函數,它從鍵盤接受原始按鍵消息,然後解釋成WM_CHAR消息,在把WM_CHAR消息放入消息隊列中,由於經過解釋後的程序含有ASCII碼,這比原始的按鍵消息好理解的多。如果你的應用程序不處理按鍵消息的話,則可以不調用該函數。DisPatchMessage會把消息發送給負責該窗口的過程函數。

 

   EndMsg:
        mov eax, [@msg.wParam]
        ret

   如果消息返回了,則退出嗎在wParam參數中,你可以把它放在eax寄存器中返回給windows 操作系統。windows目前沒有利用這個退出碼,但是我們爲了防止意外。

    proc _WndProc uses ebx esi edi,hWnd:DWORD, wMsg:DWORD, wParam:DWORD, lParam:DWORD
       
       這是我們的窗口過程函數,函數名可以任意設置,第一個參數hWnd是我們要接受消息的窗口句柄。wMsg是接受的消息,注意wMsg不是一個MSG結構體,它是一個DWORD類型。Windows定義了成千上百個消息,大多數你的程序用不到。當該窗口由消息發生時,windows會發送相關消息給該窗口。窗口過程函數會智能的處理這些函數,wParam 和 lParam 只是附加參數,以方便傳遞更多的和該消息有關的數據。

cmp [wMsg], WM_DESTROY
        jz Quit
        invoke DefWindowProc,[hWnd],[wMsg],[wParam],[lParam]
        ret
   
   
    Quit:
        invoke PostQuitMessage,NULL
        jmp endWnd
       
   
    endWnd:
        xor eax,eax
        ret
    endp

 

這裏可以說是重要的部分,這也是我們編寫windows時需要改寫的部分,此處程序檢查windows傳遞過來的消息,如果是我們感興趣的則處理,處理完後eax清0,然後返回,否則必須調用DefWindowProc,把該窗口過程接受到的參數傳遞給缺省的窗口過程函數,所有的消息你必須處理的是WM_DESTROY,當你的應用程序結束是,windows把它傳來。當你的應用程序解說到該消息時,它已經在屏幕上消失了,這僅僅是通知你的應用程序已經銷燬,你必須自己返回操作系統。在此消息你可以做一些清理工作,但無法阻止退出程序。如果你要那樣做的話,可以調用WM_CLOSE消息,處理完工作後,你必須調用PostQuitMessage,該函數會把WM_QUIT消息傳回給你的應用程序,該消息會使得GetMessage返回,並在eax寄存器中加入0,然後結束消息循環並退出操作系統。DestroyWindow 函數,它會發送一個 WM_DESTROY 消息給您自己的應用程序,從而迫使它退出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章