在本課中,我們將用匯編語言寫一個 Windows 程序,程序運行時將彈出一個消息框並顯示"Win32 assembly is great!"。
理論:
Windows 為編寫應用程序提供了大量的資源。其中最重要的是Windows API (Application Programming Interface)。 Windows API是一大組功能強大的函數,它們本身駐扎在 Windows 中供人們隨時調用。這些函數的大部分被包含在幾個動態鏈接庫(DLL)中,譬如:kernel32.dll、 user32.dll 和 gdi32.dll。 Kernel32.dll中的函數主要處理內存管理和進程調度;user32.dll中的函數主要控制用戶界面;gdi32.dll中的函數則負責圖形方面的操作。除了上面主要的三個動態鏈接庫,您還可以調用包含在其他動態鏈接庫中的函數,當然您必須要有關於這些函數的足夠的資料。
動態鏈接庫,顧名思義,這些 API 的代碼本身並不包含在 Windows 可執行文件中,而是當要使用時才被加載。為了讓應用程序在運行時能找到這些函數,就必須事先把有關的重定位信息嵌入到應用程序的可執行文件中。這些信息存在於引入庫中,由鏈接器把相關信息從引入庫中找出插入到可執行文件中。您必須指定正確的引入庫,因為只有正確的引入庫才會有正確的重定位信息。
當應用程序被加載時 Windows 會檢查這些信息,這些信息包括動態鏈接庫的名字和其中被調用的函數的名字。若檢查到這樣的信息,Windows 就會加載相應的動態鏈接庫,並且重定位調用的函數語句的入口地址,以便在調用函數時控制權能轉移到函數內部。
如果從和字符集的相關性來分,API 共有兩類:一類是處理 ANSI 字符集的,另一類是處理 UNICODE 字符集的。前一類函數名字的尾部帶一個"A"字符,處理UNICODE的則帶一個"W"字符(我想"W"也許是代表寬字符的意思吧)。我們比較熟悉的ANSI字符串是以 NULL 結尾的一串字符數組,每一個ANSI字符是一個 BYTE 寬。對於歐洲語言體系,ANSI 字符集已足夠了,但對於有成千上萬個唯一字符的幾種東方語言體系來說就只有用 UNICODE 字符集了。每一個 UNICODE 字符占有兩個 BYTE 寬,這樣一來就可以在一個字符串中使用 65336 個不同字符了。
這也是為什麼引進 UNICODE 的原因。在大多數情況下我們都可以用一個包含頭文件,在其中定義一個宏,然後在實際調用函數時,函數名後不需要加後綴"A"或"W"。
<譯者注:如在頭文件中定義函數foo();
#ifdef UNICODE
#define foo() fooW()
#else
#define foo() fooA()
#endif
>
例子:
我先把框架程序放在下面,然後我們再向裡面加東西。
.386
.model flat, stdcall
.data
.code
start:
end start
應用程序的執行是從 END 定義的標識符後的第一條語句開始的。在上面的框架程序中就是從 START 開始。程序逐條語句執行一直到遇到 JMP,JNE,JE,RET 等跳轉指令。這些跳轉指令將把執行權轉移到其他語句上,若程序要退出 Windows,則必須調用函數 ExitProcess。
ExitProcess proto uExitCode:DWORD
上面一行是函數原型。函數原型會告訴編譯器和鏈接器該函數的屬性,這樣在編譯和鏈接時,編譯器和鏈接器就會作相關的類型檢查。 函數的原型定義如下:
FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...
簡言之,就是在函數名後加偽指令PROTO,再跟一串由逗號相隔的數據類型鏈表。在前面的 ExitProcess 定義中,該函數有一個 DWORD 類型的參數。當您使用高層調用語句 INVOKE 時,使用函數原型定義特別有用,您可以簡單地認為 INVOKE 是一個有參數類型檢查的調用語句。譬如,假設您這樣寫:
call ExitProcess
若您事先沒把一個DWORD類型參數壓入堆棧,編譯器和鏈接器都不會報錯,但毫無疑問,在您的程序運行時將引起崩潰。但是,當您這樣寫:
invoke ExitProcess
連接器將報錯提醒您忘記壓入一個 DWORD 類型參數。所以我建議您用 INVOKE 指令而不是CALL去調用一個函數。INVOKE 的語法如下:
INVOKE expression [,arguments]
expression 既可以是一個函數名也可以是一個函數指針。參數由逗號隔開。大多數API函數的原型放在頭文件中。 如果您用的是 hutch 的 MASM32,這些頭文件在文件夾MASM32/include 下, 這些頭文件的擴展名為 INC,函數名和 DLL 中的函數名相同,譬如:KERNEL32.LIB 引出的函數 ExitProcess 的函數原形聲明於kernel.inc中。您也可以自己聲明函數原型。 在我的教學課程中都使用 hutch 的windows。inc,這些頭文件您可以從http://win32asm.cjb.net下載。
好,我們現在回到ExitProcess 函數,參數uExitCode 是您希望當您的應用程序結束時傳遞 Windows 的。 您可以這樣寫:
invoke ExitProcess,0
把這一行放到開始標識符下,這個應用程序就會立即退出 Windows,當然毫無疑問個應用程序本身是一個完整的 Windows 程序。
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
start:
invoke ExitProcess,0
end start
option casemap:none 一句的意思是告訴 MASM 要區分標號的大小寫,譬如:start 和 START 是不同的。請注意新的偽指令 include,跟在其後的文件名所指定的文件在編譯時將“插”在該處。在我們上面的程序段中,當MASM處理到語句 include \masm\include\windows.inc 時,它就會打開文件夾\MASM32\include 中的文件windows.inc,這和您把整個文件都粘貼到您的源程序中的效果是一樣的。 hutch 的 windows.inc 包含了 WIN32 編程所需要的常量和結構體的定義。 但是它不包含函數原型的定義。盡管 hutch 和我盡力包含所有的常量和結構體的定義,但仍會有不少遺漏,為此我們將不斷加入新的內容。請隨時注意我們主頁,下載最新的頭文件。
您的應用程序除了從 windows.inc 中得到相關變量結構體的定義外,還需要從其他的頭文件中得到函數原型的聲明,這些頭文件都放在 \masm32\include 文件夾中。 在我們上面的例子中調用了駐扎在 kernel.dll 中的函數,所以需要包含有這個函數原型聲明的頭文件 kernel.inc。如果用文本編輯器打開該文件您會發現裡面全是從 kernel.dll中引出的函數的聲明。如果您不包含kernel.inc,您仍然可以調用(call)ExitProcess,但不能夠調用(invoke)ExitProcess(這會無法通過編譯器和連接器的參數合法性檢查)。所以若用 invoke 去調用一個函數,您就必須事先聲明,當然不一定要包含我們的頭文件,您完全可以在調用該函數前在源代碼的適當位置進行聲名。包含頭文件主要是為了節省時間(譯者:當然還有正確性)
接下來我們來看看 includelib 偽指令,和 include 不同,它僅僅是告訴編譯器您的程序引用了哪個庫。當編譯器處理到該指令時會在生成的目標文件中插入鏈接命令告訴鏈接器鏈入什麼庫。當然您還可以通過在鏈接器的命令行指定引入庫名稱的方法來達到和用includelib指令相同的目的,但考慮到命令行僅能夠傳遞128個字符而且要不厭其煩地在命令行敲字符,所以這種方法是非常不可取的。
好了,現在保存例子,取名為msgbox.asm。把 ml.exe 的路徑放到 PATH 環境變量中,鍵入下面一行 進行編譯:
ml /c /coff /Cp msgbox。asm (譯者注:命令行參數大小寫是有區別的)
/c 是告訴MASM只編譯不鏈接。這主要是考慮到在鏈接前您可能還有其他工作要做。
/coff 告訴MASM產生的目標文件用 coff 格式。MASM 的 coff 格式是COFF(Common Object File Format:通用目標文件格式) 格式的一種變體。在 UNIX 下的 COFF 格式又有不同。
/Cp 告訴 MASM 不要更改用戶定義的標識符的大小寫。若您用的是 hutch 的包含文件的話,在.model 指令下加入 "option casemap:none" 語句,可達到同樣的效果。
當您成功的編譯了 msgbox.asm 後,編譯器會產生 msgbox.obj 目標文件,目標文件和可執行文件只一步之遙,目標文件中包含了以二進制形式存在的指令和數據,比可執行文件相差的只是鏈接器加入的重定位信息。
好,我們來鏈接目標文件:
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj
/SUBSYSTEM:WINDOWS 告訴鏈接器可執行文件的運行平台
/LIBPATH:〈path to import library〉 告訴鏈接器引入庫的路徑。
鏈接器做的工作就是根據引入庫往目標文件中加入重定位信息,最後產生可執行文件。 既然得到了可執行文件,我們來運行一下。好,一、二、三,GO!屏幕上什麼都沒有。哦,對了,我們除了調用了 ExitProcess 函數外,甚麽都還沒做呢!但是別一點成就感都沒有哦,因為我們用匯編所寫的是一個真正 Windows 程序,不信的話,查查您磁盤上的 msgbox.exe文件,在我的機器上它的大小足有1,536字節呢。
下面我們來做一點可以看的見摸的著的,我們在程序中加入一個對話框。該函數的原型如下:
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
hWnd 是父窗口的句柄。句柄代表您引用的窗口的一個地址指針。它的值對您編 Windows 程序並不重要(譯者注:如果您想成為高手則是必須的),您只要知道它代表一個窗口。當您要對窗口做任何操作時,必須要引用該窗口的指針。
lpText 是指向您要顯示的文本的指針。指向文本串的指針事實上就是文本串的首地址。
lpCaption 是指向您要顯示的對話框的標題文本串指針。
uType 是顯示在對話框窗口上的小圖標的類型。
下面是源程序
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0
.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start
編譯、鏈接上面的程序段,得到可執行文件。運行,哈哈,窗口上彈出了一個對話框,上面有一行字:“Win32 Assembly is Great!”。想一想,我們是用匯編寫出來的,所以我們有理由為編寫了一個最簡單的 WIN32 程序感到高興。(譯者注:如果明天我們能夠像在 DOS 下那樣每一行都用匯編寫,那我們有理由為自己感到自豪。)
好,我們回過頭來看看上面的源代碼。我們在.DATA“分段”定義了兩個NULL結尾的字符串。我們用了兩個常量:NULL 和 MB_OK。這些常量在windows.inc 文件中有定義,使用常量使得您的程序有較好的可讀性。 addr 操作符用來把標號的地址傳遞給被調用的函數,它只能用在 invoke 語句中,譬如您不能用它來把標號的地址賦給寄存器或變量,如果想這樣做則要用 offset 操作符。在 offset 和 addr 之間有如下區別:
addr不可以處理向前引用,offset則能。所謂向前引用是指:標號的定義是在invoke 語句之後,譬如在如下的例子:
invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
......
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0
如果您是用 addr 而不是 offset 的話,那 MASM 就會報錯。
addr可以處理局部變量而 offset 則不能。局部變量只是在運行時在堆棧中分配內存空間。而 offset 則是在編譯時由編譯器解釋,這顯然不能用offset 在運行時來分配內存空間。編譯器對 addr 的處理是先檢查處理的是全局還是局部變量,若是全局變量則把其地址放到目標文件中,這一點和 offset 相同,若是局部變量,就在執行 invoke 語句前產生如下指令序列:
lea eax, LocalVar
push eax
因為lea指令能夠在運行時決定標號的有效地址,所以有了上述指令序列,就可以保證 invoke 的正確執行了。