程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Win32 Asm教程

Win32 Asm教程

編輯:關於C++

這是我的Win32匯編教程。它總是在創建中,我會不停地添加內容。通過上面的next和prev鏈接,你可以轉到後面和前面一頁。

導言

先來對這個教程做個小小的介紹。Win32Asm不是一個非常流行的編程語言,而且只有為數不多(但很好)的教程。大多數教程都集中在編程的win32部分(例如,WinAPI,標准Windows編程技術的使用等),而不是匯編語言本身,例如偽代碼(opcodes),寄存器(registers)的使用等。雖然你能在其他教程中找到這些內容,但那些教程通常是解釋Dos編程的。它當然可以幫你學習匯編語言,但在Windows中編程,你不再需要了解Dos中斷(interrupt)和端口(port)In/Out函數。在Windows中,WindowsAPI提供了你可在你的程序中使用的標准函數,後面還會對此有更多內容。這份教程的目的是在解釋用匯編編Win32程序的同時學習匯編語言本身。

1.0-介紹匯編語言

匯編語言是創造出來代替原始的只能由處理器理解的二進制代碼的。很久以前,尚沒有任何高級語言,程序都是用匯編寫的。匯編代碼直接描述處理器可以執行的代碼,例如:

add eax,edx

add這條指令把兩個值加到一起。eax和edx被稱為寄存器,它們可以在處理器內部保存值。這條代碼被轉換為66 03 c2(16進制)。處理器閱讀這行代碼,並執行它所代表的指令。像C這樣的高級語言把它們自己的語言翻譯為匯編語言,而匯編器又把它轉換為二進制代碼:

C 代碼
a = a + b; >> C編譯器 >> 匯編語言
add eax, edx >>匯編器>> 原始輸出(十六進制)
66 03 C2

(注意該處的匯編語言的代碼被簡化了,實際輸出決定於C代碼的上下文)

1.1-為什麼?(Why?)
既然用匯編寫程序很困難,那麼為什麼你用A匯編而不是C或者別的什麼??-匯編產生的程序更小而且更快。在像如有人工智能一般的非常高級編程語言中,編譯器要產生輸出代碼比起匯編來更困難。雖然編譯器變得越來越好,編譯器仍然必須指出最快(或最小)的方式產生匯編代碼。而且,你自己來寫(匯編)代碼(包括可選的代碼優化)能生成更小更快的代碼。但是,當然,這比使用高級語言難多了。還有另一個與某些使用運行時dll的高級語言不同的地方,它們在大多數時運行良好,但有時由於dll(dll hell)而產生問題,用戶總是要安裝這些Dll。對於Visual C++,這不是一個問題,它們是與Windows一同安裝的。而Visual Basic甚至不把自己的語言轉換為匯編語言(雖然5以及更高的版本進行了一些這樣的轉換,但不完全)。它高度依賴msvbvm50.dll-Visual Baisc虛擬機。由VB產生的exe文件僅僅存在簡單的代碼和許多對這些dll的調用。這就是vb慢的原因。匯編是所有中最快的。它僅僅用系統的dll如Kernel32.dll, User32.dll等。

譯者注:dll hell是指由於dll新的版本被舊的版本給代替了。由於使用了dll新版本的程序仍然調用新的函數,導致了致命的錯誤。

另一個誤解是許多人認為匯編不可能用來編程。當然,它難,但不是不可能。用匯編創建大的工程的確很難,我只是用它來寫小程序,用於需要速度的代碼被寫在能被其他語言導入的dll中。而且,Dos和Windows還有一個很大的區別。Dos程序把中斷當“函數”用。像中斷10用於顯示,中斷13用於文件存儲等。在Windows中,API函數只有名字(比如MessageBox, CreateWindowsEx)。你能導入庫(DLL)並使用其中的函數。這使得用asm寫程序簡單多了。你將在下一章中學習更多關於這方面的知識。

2.0-開始前的准備

介紹已經夠多了,現在讓我們開始吧。要用匯編寫程序,你需要一些工具。下面,你能看到我將在本教程中使用哪些工具。我建議你安裝同樣的工具,因為這樣你能跟著教程試驗文中的例子。我也給出其他的一些選擇,雖然其中的大部分你都可以選擇,但是要警告的是在匯編器(masm,tasm和nasm)中有很大的區別。在這個教程中,將使用masm,因為它有許多很有用的功能(例如invoke),它使得編程更容易。當然,你可以自己選擇你更喜歡的匯編器,但這將使你跟著教程走難一些而且你不得不把教程中的例子進行轉換使它可以在你用的匯編器中運行。

匯編器

我的選擇:Masm(在win32asm包中)

網址:win32asm.cjb.net

描述:一個把偽代碼(opcodes)翻譯為給處理器讀的原始輸出(object文件)的匯編器

相關內容:Masm,宏(macro)匯編器,是一個有很多有用的特色的匯編器。像“invoke”,它可以簡化對API函數的調用並對數據類型進行檢查。你將在本教程的後面學習這些。如果你讀了上面的文字你就知道本教程推薦使用masm。

供選擇:Tasm[dl],nasm[dl]

鏈接器

我的選擇:微軟Incremental鏈接器(link.exe)

網址:win32asm.cjb.net(在win32asm包中)

描述:鏈接器把目標(object)文件和庫文件(用於導入DLL中的函數)“鏈接”到一起輸出最終的可執行文件。

關於:我用Iczelion的Win32asm包中的link.exe。但大多數的鏈接器都可以用。

供選擇:Tasm linker[dl]

資源編輯器

我的選擇:Borland Resource Workshop

網址:www.crackstore.com

描述:用於創建資源(圖形,對話框,位圖,菜單等)的資源編輯器。

關於:大多數的編輯器都行。我個人愛好是resource workshop但你可以用你喜歡的。注意由於resource workshop創建的資源文件有時給資源編譯帶來麻煩,如果你想使用這個編輯器,你應當把tasm一起下下來,他裡面包含了用於編譯borland式資源的brc32.exe。

供選擇:Symantec資源編輯器,Resource Builder等等

文本編輯器

我的選擇:ultraedit

網址:www.ultraedit.com

描述:一個文本編輯器需要說明嗎?

關於:文本編輯器的選擇是十分個性化的。我非常喜歡ultraedit。你可以下載我為ultraedit寫的語法文件,它可以使匯編代碼語法高亮。但至少,選一個支持語法高亮的文本編輯器(關鍵字會自動標色)。這非常有用而且它使你的代碼更容易讀和寫。Ultraedit還有一個可以使你在代碼中快速跳轉到某一個函數的函數列表。

供選擇:數百萬的文本編輯器中的一個

參考手冊

我的選擇:win32程序員參考手冊

網址:www.crackstore.com(或搜索互聯網)

描述:你需要參考一些API函數的用法。最重要的是“win32程序員參考手冊”(win32.hlp)。這是個大文件,大約24mb(一些版本是12mb,但不全)。在這個文件中,對所有系統dll的函數(kernel,user,gdi,shell等)都做了說明。你至少需要這個文件,其他的參考(sock2.hlp, mmedia.hlp, ole.hlp等)也是有幫助的但不一定需要。

供選擇:N/A

(譯者注:該教程寫成較早,現在有極好的MSDN供選擇)

2.1-安裝工具

現在你已經得到這些工具了,把它們安裝到你硬盤的某個角落吧。這有幾個值得注意的地方:

把masm包安裝到你打算寫匯編源程序的那個分區。這保證了包含文件路徑的正確性。把masm(和tasm)的bin目錄加到autoexec.bat的path中,並重新啟動。

如果你用的是ultraedit,使用你可以在前面下載的語法文件並啟用function-listview(函數列表視圖)。

2.2-為你的源文件准備目錄

在某個地方創建一個win32文件夾(或其他你喜歡的名字),並為你創建的每一個工程創建一個子文件夾。

3.0-匯編基礎知識

這章將教你匯編語言的基礎知識

3.1-偽代碼(opcodes)

匯編程序是用偽代碼創建的。一個偽代碼是一條處理器可以理解的指令。例如:

ADD

Add指令把兩個數加到一起。大部分偽代碼有參數

ADD eax, edx

ADD有兩個參數。在加法的情況下,一個源一個目標。它把源值加到目標值中,並把結果保存在目標中。參數有很多不同的類型:寄存器,內存地址,直接數值(immediate values)參見下文。

3.2-寄存器

有幾種大小的寄存器:8位,16位,32位(在MMX處理器中有更多)。在16位程序中,你僅能使用16位和8位的寄存器。在32位的程序中,你可以使用32位的寄存器。

一些寄存器是別的寄存器的一部分:例如,如果EAX保存了值EA7823BBh這裡是其他寄存器的值。

EAX EA 78 23 BB
AX EA 78 23 BB
AH EA 78 23 BB
AL EA 78 23 BB

ax,ah,al是eax的一部分。eax是一個32位的寄存器(僅在386以上存在),ax包含了eax的低16位(2字節),ah包含了ax的高字節,而al包含了ax的低字節。因而ax是16位的,al和ax是8位的。在上面的例子中,這些是那些寄存器的值:

eax = EA7823BB (32-bit)
ax = 23BB (16-bit)
ah = 23 (8-bit)
al = BB (8-bit)

使用寄存器的例子(不要管那些偽代碼,只看寄存器的說明)

mov eax, 12345678h
;Mov把一個值載入寄存器(注意:12345678h是一個十六進制值,因為h這個後綴。

mov cl, ah
;把ax的高字節移入cl

sub cl, 10
;從cl的值中減去10(十進制)

mov al, cl
;並把cl存入eax的最低字節

讓我們來分析上面的代碼:

mov指令可以把一個值從寄存器,內存和直接數值移入另一個寄存器。在上面的例子中,eax包含了12345678h,然後ah的值(eax左數第三個字節)被復制入了cl中(ecx寄存器的最低字節)。然後,cl減10並移回al中(eax的最低字節)

寄存器的不同類型:

全功能(General Purpose)

這些32位(它們的組成部分為16/8位)寄存器可以用來做任何事情:

eax (ax/ah/al) 加法器
ebx (bx/bh/bl) 基(base)
ecx (cx/ch/cl) 計數器
edx (dx/dh/dl) 數據

雖然它們有名字,但是你可以用它們做任何事。

段(Segment)寄存器

段寄存器定義了哪一段內存被使用。你可能在win32asm中用不著它們,因為windows有一個平坦(flat)的內存系統。在Dos中,內存被分為64kb的段,因而如果你想要定一個內存地址。你指定一個段,並用一個offset(偏移址)(像0172:0500(segment:offset))。在windows中,段有4GB的大小,所以你在Windows中不需要段。段總是16位寄存器。

CS 代碼段
DS 數據段
SS 棧段
ES 擴展段
FS (only 286+) 全功能段
GS (only 386+) 全功能段

指針寄存器

實際上,你可以把指針寄存器當作全功能寄存器來使用(除了eip),只要你保存並恢復它們的原始值。指針寄存器之所以這麼叫是因為它們經常被用來存儲內存地址。一些偽代碼(movb,scasb等)也要用它們。

esi (si) 源索引
edi (di) 目標索引
eip (ip) 指令指針

eip(在16位編程中為ip)包含了指向處理器將要執行的下一條指令的指針。因而你不能把eip當作全功能寄存器來用。

棧寄存器

有2個棧寄存器:esp和ebp。esp裝有內存中當前棧的位置(在下章中,對此有更多的內容)。Ebp在函數中被用成指向局部變量的指針。

esp (sp) 棧指針
ebp (bp) 基(base)指針

4.0-內存

這部分將解釋在Windows中內存是如何被管理的。

在運行於Dos和Win3.xx的16位程序中,內存被分成許多個段。這些段的大小為64kb。為了存儲內存,需要一個段指針和一個偏移址指針。段指針標明要使用的是哪個段,offset(偏移址)指針標明在段位置。看下圖:

                          內存 
段 1 (64kb) 段 2 (64kb) 段 3 (64kb) 段 4(64kb) 更多

注意下面關於16位程序的解釋,後面有更多關於32位的內容(但不要跳過這部分,要理解32位的內存管理,這部分很重要)上表是全部的內存,被劃分成了多個64kb的段。最多有65536個段。現在取出一段:

                         段 1(64kb) 
Offset 1 Offset 2 Offset 3 Offset 4 Offset 5 更多

為了指向段中的位置,需要使用offset。一個offset是段內部的一個位置。每個段最多有65536個offset。內存中地址的記法是:

SEGMENT:OFFSET

例如:

0030:4012(均為16進制)

它的意思是:段30,offset4012。為了查看那個地址中有什麼。你先要到段30,然後到該段的offset4012。在前一章中,你已經學過了段和指針寄存器。例如,段寄存器有:

CS 代碼段
DS 數據段
SS 棧段
ES 擴展段
FS (only 286+) 全功能段
GS (only 386+) 全功能段

顧名思義:代碼段(CS)包括了當前的代碼執行到了哪部分。數據段是用來標明在哪段中取出數據。棧指棧段(後面有更多)。ES,FS, GS是全功能的寄存器,並且可以用於任何段(雖然在Windows中不是如此)。

指針寄存器大多數時裝有offset,但全功能寄存器(ax, bx, cx, dx等)也可以這麼用。IP標明當前指令執行到了哪個offset。Sp保存了當前棧的在ss(棧段中)的offset。

4.2-32位Windows

你可能已經注意到了關於段的一切是乏味的。在16位編程中,段是必不可少的。幸運的是,這個問題已經在32位Windows(95及以上)中得到解決。你仍然有段,但不用管他們了因為它們不再是64kb,而是4GB。你如果嘗試著改變段寄存器中的一個,windows甚至會崩潰。這稱為平坦(flat)內存模式。只有offset,而且是32位的,因而范圍從0到4,294,967,295。內存中的每一個地址都是用offset表示的。這真是32位勝於16位的最大優點。所以,你現在可以忘了段寄存器並把精神集中在其他的寄存器上。

5.0-偽代碼

偽代碼是給處理器的指令,它實際上是原始十六進制代碼的可讀版。因此,匯編是最低級的編程語言。匯編中的所有東西被直接翻譯為十六進制碼。換句話說,你沒有把高級語言翻譯為低級語言的編譯器上的煩惱,匯編器僅僅把匯編代碼轉化為原始數據。

本章將討論一些用來運算,位操作等的偽代碼。還有跳轉指令,比較等偽代碼在後面介紹。

5.1-一些基本的計算偽代碼

MOV

這條指令用來把一個地方移往(事實上是復制到)另一個地方。這個地方可以是寄存器,內存地址或是直接數值(當然只能作為源值)。Mov指令的語法是:

mov 目標,源

你可把一個寄存器移往另一個(注意指令是在復制那個值到目標中,盡管“mov”這個名字是移的意思)

mov edx, ecx

上面的這條指令把ecx的內容復制到了ecx中,源和目標的大小應該一致。例如這個指令是非法的:

mov al, ecx;非法

這條偽代碼試圖把一個DWORD(32位)值裝入一個字節(8位)的寄存器中。這不能個由mov指令來完成(有其他的指令干這事)。但這些指令是允許的因為源和目標在大小上並沒有什麼不同:

mov al, bl
mov cl, dl
mov cx, dx
mov ecx, ebx

內存地址由offset指示(在win32中,前一章中有更多信息)你也能從地址的某一個地方獲得一個值並把它放入一個寄存器中。下面有一個例子:

offset 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42
data 0D 0A 50 32 44 57 25 7A 5E 72 EF 7D FF AD C7

每一個塊代表一個字節

offset的值這裡是用字節的形式表示的,但它事實上是32位的值,比如3A(這不是一個常見的offset的值,但如果不這樣簡寫表格裝不下),這也是一個32位的值:0000003Ah。只是為了節省空間,使用了一些不常見的低位offset。所有的值均為16進制。

看上表的offset 3A。那個offset的數據是25, 7A, 5E, 72, EF等。例如,要把這個位於3A的值用mov放入寄存器中:

mov eax, dword ptr[0000003Ah]

(h後綴表明這是一個十六進制值)

mov eax, dword ptr[0000003Ah]這條指令的意思是:把位於內存地址3A的DWORD大小的值放入eax寄存器。執行了這條指令後,eax包含了值725E7A25h。可能你注意到了這是在內存中時的反轉結果:25 7A 5E 72。這是因為存儲在內存中的值使用了little endian格式。這意味著越靠右的字節位數越高:字節順序被反轉了。我想一些例子可以使你把這個搞清楚。

十六進制dword(32位)值放在內存中時是這樣:40, 30, 20, 10(每個值占一個字節(8位))

十六進制word(16位)值放在內存中時是這樣:50, 40

回到前面的例子。你也可以對其他大小的值這麼做:

mov cl, byte ptr [34h] ; cl得到值0Dh(參考上表)

mov dx, word ptr [3Eh] ; dx將得到值 7DEFh (看上表,記住反序)

大小有時不是必須的。

Mov eax,[00403045h]

因為eax是32位寄存器,編譯器假定(也只能這麼做)它應該從地址403045(十六進制)取個32位的值。

可以直接使用數值:

mov edx, 5006

這只是使得edx寄存器裝有值5006,綜括號[和]用來從括號間的內存地址處取值,沒有括號就只是這個值。寄存器和內存地址也可以(他應該是32位程序中的32位寄存器):

mov eax,403045h;使eax裝有值403045h(十六進制)

mov cx,[eax];把位於內存地址eax的word大小的值(403045)移入cx寄存器。

在mov cx, [eax]中,處理器會先查看eax裝有什麼值(=內存地址),然後在那個內存地址中有什麼值,並把這個word(16位,因為目標-cx-是個16位寄存器)移入cx。

ADD, SUB, MUL, DIV

許多偽代碼做計算工作。你可以猜出它們中的大多數的名字:add(加),sub(減),mul(乘),div(除)等。

Add偽代碼有如下語法:

Add 目標,源

執行的運算是 目標=目標+源。下面的格式是允許的。

目標      源                例子 
Register Register add ecx, edx
Register Memory add ecx, dword ptr [104h] / add ecx, [edx]
Register Immediate value add eax, 102
Memory Immediate value add dword ptr [401231h], 80
Memory Register add dword ptr [401231h], edx

這條指令非常簡單。它只是把源值加到目標值中並把結果保存在目標中。其他的數學指令有:

sub 目標,源(目標=目標-源)
mul 目標,源(目標=目標×源)
div 源(eax=eax/源,edx=余數)

減法和加法一樣做,乘法是目標=目標×源。除法有一點不同,因為寄存器是整數值(注意,繞回數不是浮點數)除法的結果被分為商和余數。例如:

28/6->商=4,余數=4
30/9->商=3,余數=3
97/10->商=9,余數=7
18/6->商=3,余數=0

現在,取決於源的大小,商(一部分)被存在eax中,余數(一部分)在edx:

源大小          除法              商存於 余數存於 
BYTE(8-bits) ax / source AL AH
WORD (16-bits) dx:ax* / source AX DX
DWORD (32-bits) edx:eax* / source EAX EDX

*:例如,如果dx=2030h,而ax=0040h,dx:ax=20300040h。dx:ax是一個雙字值。其中高字代表dx,低字代表ax,Edx:eax是個四字值(64位)其高字是edx低字是eax。

Div偽代碼的源可以是

an 8-bit register (al, ah, cl,...)
a 16-bit register (ax, dx, ...)
a 32-bit register (eax, edx, ecx...)
an 8-bit memory value (byte ptr [xxxx])
a 16-bit memory value (word ptr [xxxx])
a 32-bit memory value (dword ptr [xxxx])

源不可以是直接數值因為處理器不能決定源參數的大小。

位操作

這些指令都由源和目標,除了“NOT”指令。目標中的每位與源中的每位作比較,並看是那個指令,決定是0還是1放入目標位中。

指令   AND     OR      XOR     NOT 
源位 0 0 1 1 0 0 1 1 0 0 1 1 0 1
目標位 0 1 0 1 0 1 0 1 0 1 0 1 X X
輸出位 0 0 0 1 0 1 1 1 0 1 1 0 1 0

如果源和目標均為1,AND把輸出位設為1。

如果源和目標中有一個為1,OR把輸出位設為1。

如果源和目標位不一樣,XOR把輸出位設為1。

NOT反轉源位

一個例子:

mov ax, 3406
mov dx, 13EAh
xor ax,dx
ax=3406(十六進制)是二進制的0000110101001110
dx=13EA(十六進制)是二進制的0001001111101010

對這些位進行xor操作:

源      0001001111101010 (dx) 
目標 0000110101001110 (ax)
輸出 0001111010100100 (new ax)

新dx是0001111010100100 (十進制的7845, 十六進制的1EA4)

另一個例子:

mov ecx, FFFF0000h

not ecx

FFFF0000在二進制中是11111111111111110000000000000000(16個1,16個0)如果反轉每位會得到

00000000000000001111111111111111(16個0,16個1)在十六進制中是0000FFFF。因而執行NOT操作後,ecx是0000FFFFh。

步增/減

有兩個很簡單的指令,DEC和INC。這些指令使內存地址和寄存器步增或步減,就是這樣:

inc reg -> reg = reg + 1
dec reg -> reg = reg - 1
inc dword ptr [103405] -> 位於103405的值步增
dec dword ptr [103405] -> 位於103405的值步減

NOP

這條指令什麼都不干。它僅僅占用空間和時間。它用作填充或給代碼打補丁的目的。

移位(Bit Rotation 和 shifiting)

注意:下面的大部分例子使用8位數,但這只是為了使目的清楚。

Shifting函數

SHL 目標,計數(count)
SHR 目標,計數(count)

SHL和SHR在寄存器,內存地址中像左或向右移動一定數目(count)的位。

例如:

;這兒al=01011011(二進制)
shr al, 3

它的意思是:把al寄存器中的所有位向右移三個位置。因而al會變成為00001011。左邊的字節用0填充,而右邊的字節被移出。最後一個被移出的位保存在carry-flag中。Carry-flag是處理器標志寄存器的一位,它不是像eax或ecx一樣的,你可以訪問的寄存器(雖然有偽代碼干這活),但它的值決定於該指令的結構。它(carry-flag)會在後面解釋,你要記住的唯一一件事是carry是標志寄存器的一位且它可以被打開或者關閉。這個位等於最後一個移出的位。

shl和shr一樣,只不過是向左移。

;這兒bl=11100101(二進制)
shl bl, 2

執行了指令後bl是10010100(二進制)。最後的兩個位是由0填充的,carry-flag是1,因為最後移出的位是1。

還有兩個偽代碼:

SAL 目標, 計數(算術左移)

SAR 目標, 計數(算術右移)

SAL和SHL一樣,但SAR不完全和SHR一樣。SAR不是用0來填充移出的位而是復制MSB(最高位)例如:

al = 10100110
sar al, 3
al = 11110100
sar al, 2
al = 11101001
bl = 00100110
sar bl, 3
bl = 00000100

Rotation(循環移動) 函數

Rol 目標,計數;循環左移

Ror 目標,計數;循環右移

Rcl 目標,計數;通過carry循環左移

Rcr 目標,計數;通過carry循環右移

循環移動(Rotation)看上去就像移(Shifting),只是移出的位又到了另一邊。

例如:ror(循環右移)

如你在上圖所見,位循環了。注意,每個被推出的位又移到了另一邊。和Shifting一樣,carry位裝有最後被移出的位。Rcl和Rcr實際上和Rol,Rcr一樣。它們的名字暗示了它們用carry位來表明最後移出的位,但和Rol和Ror干同樣的事情。它們沒有什麼不同。

交換

XCHG指令也非常簡單。它同在兩個寄存器和內存地址之間交換:

eax = 237h
ecx = 978h
xchg eax, ecx
eax = 978h
ecx = 237h

6.0-文件結構

匯編源文件被分成了幾個部分。這些部分是code,data,未初始化data,constants,resource和relocations,資源部分是資源文件創建的,後面會有更多的討論。Relocation部分對我們不重要(它包含了使PE-loader可以在內存的不同的位置裝載入程序的信息)。重要的部分是code,data,未初始化data和constants。可能你已經猜到,code部分包含了代碼。Data裝有數據,並有讀寫權限。整個data部分被包括在exe文件並可以用數據初始化。

未初始化data在啟動時沒有內容,甚至沒有包括在exe文件本身。它只是由Windows“保留”的一部分內存。這部分也有讀寫權限。Constants和data部分一樣,但只讀。雖然這部分可用作常數,但把常數定義在包含文件中更簡單也更快捷,並用作直接數值。

6.1-代表各部分的符號

在你的源文件(*.asm)中,你可以用部分標識符定義各部分:

.code;代碼部分由此開始

.data;數據部分由此開始

.data?;未初始化數據部分由此開始

.const;常量部分由此開始

可執行文件(*.exe,*.dll和其他)是(在win32中)可移植執行格式(PE),我不會詳細的討論它但是有幾點是重要的。部分(Sections)的一些屬性定義在PE頭中:

Section名,RVA,offset,原始大小,虛擬大小和標志。Rva(相對虛擬地址)是將要裝入的section部分的相對內存地址。這裡相對的意思是相對於程序載入的基地址。這個地址也在PE頭中,但可以由PE-loader改變(使用relocation部分)。Offset是初始化數據所在的exe文件本身的原始offset。虛擬大小是程序在內存中將達到的大小。標志是讀/寫/可執行等。

6.2-例子

這有一個示例程序:

.data
Number1 dd 12033h
Number2 dw 100h,200h,300h,400h
Number3 db "blabla",0
.data?
Value dd ?
.code
mov eax, Number1
mov ecx, offset Number2
add ax, word ptr [ecx+4]
mov Value, eax

這個程序不能編譯但沒關系。

在你的匯編程序中,你放入“部分”中的所有東西都會進入exe文件而且當程序被載入內存時,位於某個內存地址。在上面的data部分,有3個標簽:Number1, Number2, Number3。這些標簽會保存它們在程序中的offset因而你可以在你的程序中使用它們來指示位置。

DD直接把一個dword放在那,DW是Word而DB是byte。你也可以用db放字符串,因為它實際上是一串byte值。在例子中,data部分會變成內存中的這樣:

33,20,01,00,00,01,00,02,00,03,00,04,62,6c,61,62,6c,61,00(均為十六進制值)

(每個值位一byte)

我給其中的一些數字上了色。Number1指向byte 33所在的內存地址,Number2指向紅色00的位置,Number3是綠色的62。現在,如果你在你的程序中這麼寫:

mov eax, Number1

它實際意為:

mov ecx, dword ptr[12033h所在的內存地址]

但這樣:

mov ecx, offset Number1

意為:

mov ecx, 12033h所在的內存地址

在第一個例子中,ecx會得到Number1的內存地址的值。在第二個中,ecx會稱為內存地址(offset)本身。下面的兩個例子有相同的效果:

mov ecx, Number1

(2)

mov ecx, offset Number1
mov ecx, dword ptr [ecx] ( or mov ecx, [ecx])

現在讓我們回到前面的例子中:

.data
Number1 dd 12033h
Number2 dw 100h,200h,300h,400h
Number3 db "blabla",0
.data?
Value dd ?
.code
mov eax, Number1
mov ecx, offset Number2
add ax, word ptr [ecx+4]
mov Value, eax

標簽可以使用像Number1,Number2和Number3等值,但它啟動時包含0。因為它在未初始化data部分。這樣的優點是,你在.data?中定義的所有東西不在可執行文件中而在內存中。

.data?
ManyBytes1 db 5000 dup (?)
.data
ManyBytes2 db 5000 dup (0)

(5000dup意為:5000個副本。值db 4,4,4,4,4,4,4和值db 7dup(4)一樣)

ManyBytes1不會在文件本身,只是5000個預分配在內存中的字節。但Manybytes2會在可執行文件中使文件變大5000個字節。雖然你的文件會包含5000個零,但並沒有什麼用。

Code部分被匯編(翻譯為原始代碼)並放入可執行文件中去(當然載入後在內存中)。

7.0-條件跳轉

在Code部分,你可以看到像這樣的標簽:

.code
mov eax, edx
sub eax, ecx
cmp eax, 2
jz loc1
xor eax, eax
jmp loc2
loc1:
xor eax, eax
inc eax
loc2:

(xor eax, eax意為:eax=0)

讓我們來看看這些代碼:

mov eax, edx;把edx放入eax中
sub eax, ecx;eax-ecx
cmp eax, 2

這有一條新指令:cmp。Cmp意為compare(比較)。它能比較兩個值(寄存器,內存,直接數值)並設置Z-flag(零標志)。零標志很像carry,也是內部標志寄存器的一位。

Jz loc1

這也是條新的。它是條件跳轉指令。Jz=jump if zero(如果設置了零標志就跳轉)。Loc1是一個標記指令“xor eax,eax|inc eax”內存開始處offset的標簽。因而jz loc1=如果設置了零標志,跳往位於loc1的指令。

Cmp eax, 2;如果eax=2設置零標志
Jz loc1;如果設置了零標志就跳轉
=
如果eax等於2,跳往位於loc1的指令

然後有jmp loc2.這也好似一個跳轉,但是是一個無條件跳轉:它總是執行。上面的代碼就是:

if ((edx-ecx)==2)
{
eax = 1;
}
else
{
eax = 0;
}

或者Basic版:

IF (edx-ecx)=2 THEN
EAX = 1
ELSE
EAX = 0
END IF

7.1-標志寄存器

標志寄存器有一套標志。它們設不設置取決於計算或其他時間。我不會討論它們的全部。只揀幾個重要的說:

ZF(零標志) 當計算結果是零時該標志被設置(compare實際上是只設置標志不保存結構的減法)
SF(符號標志) 結果為負就設置
CF(carry標志) Carry標志中存放計算後最右的位。
OF(溢出標志) 標明一個溢出了的計算。如,結構和目標不匹配。

還有更多的標志(Parity, Auxiliary, Trap, Interrupt, Direction, IOPL, Nested Task, Resume, & Virtual Mode)但因為我們不用它們,所以我不解釋。

7.2-跳轉系列

有一整套的條件跳轉,而且它們跳轉與否均取決於標志的狀態。但由於大部分跳轉指令有明白的名字,你甚至無需知道哪個標志要設置,例如:“如果大於等於就跳轉”(jge)和“符號標志=溢出標志”一樣,而“如果零就跳轉”和“如果零標志=1就跳轉”一樣。

在下表中,“意思”指的是什麼樣的計算結果該跳轉。“如果大於就跳轉”意為:

cmp x, y
jmp 如果 x 比 y大

所有的跳轉指令需要一個參數:要跳往的offset。

8.0-關於數的一些事情

在大多數的編程語言中使用整數還是浮點數只取決於變量的聲明。在匯編語言中,完全的不同。浮點數的計算是由特別的偽代碼和FPU協處理器(浮點單元)完成的。浮點指令將會在後面討論。先來看看一些關於整數的事情。在c語言中有signed(有符號)整數和unsigned(無符號)整數。Signed是意為數有符號(+或-)。Unsigned總是正。找出下表中的不同(再一次的,這是一個byte的例子,它在其他大小時也同樣工作)。

因此,在有符號數中,一個byte被分為兩段:0~7F用於正值。80~FF用於負值。對於dword值,它也一樣:0~7FFFFFFFh為正,80000000~FFFFFFFFh為負,正如你可能已經注意到的一樣,負值的最高位有一個集合,因為它們比80000000h大。這位被稱為符號位。

3.1-有符號或無符號?

你和處理器都不能看出一個值是signed還是unsigned。好消息是對於加法和減法來說,一個數是signed還是unsigned沒有關系。

計算:-4+9

FFFFFFFC+00000009=00000005(這是對的)

計算:5-(-9)

00000005-FFFFFFF7=0000000E(這也是對的,5――9=4)

壞消息是對於乘法,除法和比較(compare)並不是這樣。因此,對於signed數有特殊的乘除偽代碼:imul和idiv

Imul也有一個比mul好的地方在於它可以接受直接數值:

imul src
imul src, immed
imul dest,src, 8-bit immed
imul dest,src
idiv src

它們幾乎和mul,div一樣,只是它們可以計算signed值。比較(compare)可以和unsigned一樣用。但標志作不同的設置。因此,對於符號和無符號數字有不同的jump指令:

cmp ax, bx
ja somewhere

ja是一個無符號跳轉指令。如果大於就跳轉。考慮這個ax=FFFFh(無符號時為FFFFh,有符號時為-1)和bx=0005h(無符號時為5,有符號時為5)。由於FFFFh在無符號時比0005大,ja指令會跳轉,但如果用的是jg(指一個有符號跳轉):

cmp ax, bx
jg somewhere

jg指令不會跳轉,因為-1不比5大。

只要記住這點:

一個數字是有符號還是無符號取決於你怎樣對待這個數。

9.0-更多的偽代碼

這兒有更多的偽代碼

TEST

Test對兩個參數(目標,源)執行AND邏輯操作,並根據結果設置標志寄存器。結果本身不會保存。Test用來測試一個位,例如寄存器:

test eax, 100b;b後綴意為二進制
jnz bitset

如果eax右數第三個位被設置了,jnz將會跳轉。Test的一個非常普遍的用法是用來測試一方寄存器是否為空:

test ecx, ecx
jz somewhere

如果ecx為零,Jz跳轉

關於棧的偽代碼

在我講棧的偽代碼之前,我會先解釋什麼是棧。棧是內存的一個地方,esp為指向棧的指針。棧是用來保存臨時數值的地方,有兩個指令來放入一個指和再把它取出來:push和pop。Push把一個指壓入棧。Pop再把它彈出來。最後一個放入的值最先出來。一個值被放入棧中,棧指針步減,當它移出來的時候,棧指針步增。看這個例子:

(1) mov ecx, 100
(2) mov eax, 200
(3) push ecx ; save ecx
(4) push eax
(5) xor ecx, eax
(6) add ecx, 400
(7) mov edx, ecx
(8) pop ebx
(9) pop ecx

解釋

1、 把100放入ecx中

2、 把200放入eax中

3、 把ecx(等於100)壓入棧中(第一個壓入)

4、 把eax(等於200)壓入棧中(最後壓入)

5、 /6/7:對ecx執行操作,使ecx的值改變

8、 彈出ebx:ebx成為200(最後壓入,最先彈出)

9、 彈出ecx:ecx又成為100(最先壓入,最後彈出)

為了說明再壓棧和彈棧時,內存中發生了什麼,看下圖:

(棧在這裡是初始化為0,但實際上並不是這樣。ESP表示ESP指向的offset)

mov ax, 4560h
push ax

mov cx, FFFFh
push cx

pop edx

edx現在是 4560FFFFh 了.

CALL和RET

Call跳轉到某段代碼而且一發現RET指令就返回。你可以把它們看成在其他編程語言中的函數或子程序。例如:

……代碼……
call 0455659
……更多代碼……

455659處的代碼:

add eax, 500
mul eax, edx
ret

當執行這條指令時,處理器跳到455659處的代碼,執行指令一直到ret為止,並返回到調用處的下一條。Call跳轉到的代碼被成為過程(procedure)。你可以把你反復使用的代碼寫進一個過程並在你每次需要它的時候調用。

更深入的細節:call把EIP(指向將要執行指令的指針)壓入棧,而ret指令在它返回的時候把它彈出來。你也可以給一個call指定的參數。這是由壓棧來完成的:

push something
push something2
call procedure

在一個調用的內部,參數從棧中讀出並使用。注意,只在過程中需要的局部變量也儲存在棧中。我不會在此深入下去,因為它可以在masm和tasm中很輕易的完稱。只要記住你可以寫過程,而且它們可以由參數。一個重要的地方:

eax幾乎總是用來裝一個過程的返回值。

對於windows函數也是如此。但然,你可以在你的過程使用其他的寄存器,但這是標准。

10.0-masm的優點

如果你不在使用masm,你可以跳過這章並嘗試著轉換所有的例子,或不論如何地讀一下,並試著說服自己使用masm。當然,這是你的選擇。但masm真的使匯編語言更容易了。

10.1-條件和循環結構

Masm有一些偽高階的語法來簡便地創建條件和循環結構:

.IF, .ELSE, .ELSEIF, .ENDIF
.REPEAT, .UNTIL
.WHILE, .ENDW, .BREAK
.CONTINUE

If

如果你有使用編程語言的經驗(你應該有),你可能已經看到了一些像if/else的結構:

.IF eax==1
;eax等於1
.ELSEIF eax=3
; eax等於3
.ELSE
; eax既不是1也不是3
.ENDIF

這種結構非常有用。你不需要和一對跳轉攪在一起了,只要一個.IF語句(也不要忘記.IF和.ELSE之前的時期)。嵌套的if是允許的:

.IF eax==1
.IF ecx!=2
; eax= 1 而且 ecx 不是 2
.ENDIF
.ENDIF

但可以更簡潔些:

.IF (eax==1 && ecx!=2)
; eax = 1 而且 ecx 不是 2
.ENDIF

這些是你可以使用的操作符:

==        等於 
!= 不等於
> 大於
< 小於
>= 大於等於
<= 小於等於
& 位測試
! 邏輯非
&& 邏輯與
|| 邏輯或
CARRY? carry bit set
OVERFLOW? overflow bit set
PARITY? parity bit set
SIGN? sign bit set
ZERO? zero bit set

Repeat

這個語句執行一塊指令知道條件為真為止:

.REPEAT ;代碼在此 .UNTIL eax==1

這塊代碼反復執行repeat和until之間的代碼,知道eax=1。

While

While是repeat語句的反轉。它在條件為真時執行代碼塊:

.WHILE eax==1
;代碼在此
.ENDW

你可以使用.BREAK語句來跳出循環

.WHILE edx==1
inc eax
.IF eax==7
.BREAK
.ENDIF
.ENDW

如果Eax==7,while循環將停止

continue指令使repeat或While跳過下面的代碼塊,重新執行循環。

10.2-invoke

這是勝過tasm和nasm最大的優點。Invoke簡化了過程和call的使用。

一般的格式:

push parameter3
push parameter2
push parameter1
call procedure

Invoke 格式:

invoke procedure, parameter1, parameter2, parameter3

匯編後的代碼是一摸一樣的,但invoke格式更簡單而且更可靠。對一個過程使用invoke,你要這樣定義prototype:

PROTO STDCALL testproc:DWORD, :DWORD, :DWORD

聲明了名為testproc,需三個DWORD大小的參數的過程。現在,如果你這麼做……

invoke testproc, 1, 2, 3, 4

……masm會給你一個testproc過程需要三個參數而不是四個的錯誤。Masm還會做類型檢查。它檢查參數是否為正確的類型(即大小)

在一個invoke語句中,你可以用ADDR代替offset。這會使地址在匯編時是正確的。

過程這樣定義:

testproc PROTO STDCALL :DWORD, :DWORD, :DWORD
.code
testproc proc param1:DWORD, param2:DWORD, param3:DWORD
ret
testproc endp

這會創建一個名為testproc,帶三個參數的過程。Prototype是用來調用過程的。

testproc PROTO STDCALL :DWORD, :DWORD, :DWORD
.code
testproc proc param1:DWORD, param2:DWORD, param3:DWORD
mov ecx, param1
mov edx, param2
mov eax, param3
add edx, eax
mul eax, ecx
ret
testproc endp

現在,過程做了一下計算,(param1, param2, param3) = param1 * (param2 + param3).結果(返回值)存放在eax中,局部變量這樣定義:

testproc proc param1:DWORD, param2:DWORD, param3:DWORD
LOCAL var1:DWORD
LOCAL var2:BYTE
mov ecx, param1
mov var2, cl
mov edx, param2
mov eax, param3
mov var1, eax
add edx, eax
mul eax, ecx
mov ebx, var1
.IF bl==var2
xor eax, eax
.ENDIF
ret
testproc endp

你不可以在過程外使用這些變量。它們儲存在棧中而且當過程返回時移出。

10.3-宏

現在不解釋宏。可能在以後的教程中,但現在它們對我們不重要。

11.0-Windows中的匯編基礎

現在你已經有了一些匯編語言的基礎知識,你將要學習在Windows中怎樣學習匯編。

11.1-API

Windows編程的根本在於Windows API,應用程序接口。這是由操作系統提供的一套函數。每個Windows程序員都要用這些函數。這些函數在像kernel, user, gdi, shell, advapi等系統dll中。函數有兩類:ANSI和Unicode。這和字符串的存儲方法有關。Ansi中,每個字節代表一個符號(ASCI碼),並用字節0代表一個字符串的結束(null-terminated)。Unicode使用寬字符格式。它的每個字節用2個字節。這允許像中文等多字符的語言的使用。寬字符串由兩個0字節結束。Windows通過使用不同的函數名,同時支持Ansi和Unicode。

例如:

MessageBoxA(後綴A意為ansi)
MessageBoxW(後綴W意為寬字符-unicode)

我們只使用ansi型

11.2-導入dll

為了使用來自WindowsAPI的函數,你需要導入dll。這是由導入庫(.lib)來完成的。這些庫是必需的。因為它們使系統(Windows)能在內存的動態基地址處動態的載入dll。在Win32asm包中(win32asm.cjb.net)提供了大多數標准dll的庫。你可以用masm的includelib語句裝載一個庫。

譯者注:注意,win32asm.cjb.net被中國電信封了ip。訪問請使用代理。

Includelib C:\masm32\lib\kernel32.lib

這將載入庫kernel32.lib。在例子中,用這種格式:

Includelib \masm32\lib\kernel32.lib

現在你可以看到為什麼匯編源文件要和masm在同一個區的原因了。你可以不改動路徑為正確的區就能在其他的電腦上編譯你的程序。

但你不只是需要包含庫。包含文件(.inc)也是必須的。這些可以用l2inc工具由庫文件自動生成。包含文件這樣裝載:

include \masm32\include\kernel32.inc

在包含文件中,定義了dll中函數的原型(prototypes),因而你能使用invoke。

kernel32.inc:
...
MessageBoxA proto stdcall :DWORD, :DWORD, :DWORD, :DWORD
MessageBox textequ
...

你能看到包含文件內有for Ansi的函數而且沒有‘A’的函數名字定義為與真實函數名一樣:你可以用MessageBox代替MessageBoxA使用。在你包含了庫和包含文件後,你可以使用函數了:

invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, NULL

11.3-Windows包含文件

這裡有一個特別的包含文件。大多數的時候統稱為Windows.inc,其中包含了用於Windows API的所有常量和結構的定義。例如,消息框有不同的樣式。函數的第四個參數是樣式。NULL指的是MB_OK,它只有一個OK按鈕。Windows包含文件有這些樣式的定義:

> MB_OK equ 0
MB_OKCANCEL equ ...
MB_YESNO equ ...

因此你可以把這些名字當常數來用:

invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_YESNO

例子將使用masm包中的包含文件:

include \masm32\include\windows.inc

11.4-框架

.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc
include \masm32\include\windows.inc
.data
blahblah
.code
start:
blahblah
end start

這是Windows匯編源文件(.asm)的基本框架

.486

告訴匯編器應該生成486處理器(或更高)的偽代碼。你可以使用.386,但大多數情況下用.486

.model flat, stdcall

使用平坦內存模式(在前面章節中討論了)並使用stdcall調用習慣。它的意思是函數的參數從右往左壓入(最後的參數最先壓入)而且函數在結束時自己清棧。這對於幾乎所有的Windows API函數和dll是標准

option casemap:none

控制字符的映射為大寫。為了Windows.inc文件能正常工作,這個應該為”none”

includelib

前面討論了

include

前面也討論了

.data

開始data部分(看前面章節)

.code

開始code部分(看前面章節)

start:
end start

表示一個程序的開始的標簽。它不是非得叫“start”。你可以使用任何和“end”語句後相同的標簽:

startofprog:
end startofprog

12.0-第一個程序

是創建你的第一個程序的時候了。本章中的指導將這樣組織:

12.1-第一步

如果萬事具備,你應該在你的masm同一個區上有一個win32(或win32asm)目錄。為每個工程,你應該創建一個子目錄。

在win32目錄中創建一個名為“Firstprogram“的子目錄。創建一個新的文本文件並重命名為“first.asm”。

12.2-第二步

在first.asm中輸入一下代碼:

.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\windows.inc

因為現在,我們僅需要kernel32和user32兩個dll。

12.3-第三步

我們將要創建著名的“Hello World”程序。要顯示“hello World”字符串,我們要用消息對話框。消息對話框由MessageBox函數創建。你可以在《win32 程序員參考》(看第二章)中查找這個函數。這是書上說的:

MessageBox函數創建,顯示並操作消息對話框。消息對話框包含應用程序定義的消息和標題,加上任何預定義的圖標與按鈕的組合。

int MessageBox(
HWND hWnd, // handle of owner window
LPCTSTR lpText, // address of text in message box
LPCTSTR lpCaption, // address of title of message box
UINT uType // style of message box
);
Parameters
hWnd
Identifies the owner window of the message box to be created. If this parameter is NULL, the message box has no owner window.
lpText
Points to a null-terminated string containing the message to be displayed.
lpCaption
Points to a null-terminated string used for the dialog box title. If this parameter is NULL, the default title Error is used.
uType
Specifies a set of bit flags that determine the contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups of flags.
[--SNIP--]

在這段文字後有所有常數和標志的列表(他們定義在windows.inc中)。因為它太長了,我沒有在這裡列出來。通過查看參考,你就知道MessageBox函數要4個參數:父窗口(owner),指向消息串的指針,指向標題串的指針和消息框的類型。

HWnd可以是Null。因為我們的程序沒有窗口。

LpText必須是指向我們文本的指針。這僅僅意為參數是文本所在內存地址的offset。

LpCaption 是標題串的offset。

UType 是參考中解釋的像MB_OK,MB_OKCANCEL,MB_ICONERROR等值的組合。

讓我們先定義兩個用於MessageBox的字符串:

在first.asm中加入:

.data
MsgText db "Hello world!",0
MsgTitle db "This is a messagebox",0

.data 指示data部分的開始。用db,字節直接被插入,而且字符串又只是字節的集合,data部分會在包含上面的字符串,附加上結尾的0。MsgText裝有第一個字符串的offset。MsgTitle有第二個字符串的offset。現在我們可以使用函數:

invoke MessageBox, NULL, offset MsgText, offset MsgTitle, Null

但因為用的是invoke,你可以使用(更安全)ADDR代替offset:

invoke MessageBox, Null, ADDR MsgText, ADDR MsgTitle, Null

我們還沒有看最後一個參數,但這不會有什麼問題。因為MB_OK(有一個ok按鈕的消息對話框的樣式)等於0(NULL)。但你也可以使用其他的任何樣式。Utype(第4個參數)的定義是:

指定一系列決定對話框內容與行為的位標志。這個參數可以是下面標志組中標志的組合。

現在以我們要一個有OK按鈕與“information”圖標的簡單消息對話框為例。MB_OK是OK按鈕的樣式,MB_ICONINFORMATION是information圖標的樣式。樣式是用“or”操作符聯合的。這不是or偽代碼。Masm會在匯編前處理or操作。不用or,你可以用+號(加號)代替,但有時對層疊樣式有問題(一個樣式包含其他一些樣式)。但在本例中你也可以用+號。

.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK + MB_ICONINFORMATION
end start

把以上的代碼加入到你的first.asm文件中。

我們還加入了一個start標簽。如果你現在匯編你的程序並運行它,它將顯示一個消息對話框但很有可能在你點OK之後就崩潰了。這是因為程序沒有結束,而處理器開始執行MessageBox代碼後的任何東西。Windows中程序是用ExitProcess函數結束的:

VOID ExitProcess(
UINT uExitCode //對於所有線程的退出代碼 );

我們可以把0用作退出碼。

把你的代碼改成這樣:

.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK + MB_ICONINFORMATION
invoke ExitProcess, NULL
end start

12.4-第4步

因此我們最終的程序是:

.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\windows.inc
.data
MsgText db "Hello world!",0
MsgTitle db "This is a messagebox",0
.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK or MB_ICONINFORMATION
invoke ExitProcess, NULL
end start

12.5-第5步

現在我們將從源代碼產生可執行文件。

用一下內容新建一個文本文件並命名為make.bat:

@echo off
ml /c /coff first.asm
link /subsystem:windows first.obj
pause>nul

解釋:

ml /c /coff first.asm

Ml是宏匯編器(masm)。Masm將從程序創建原始代碼。參數的意思是: /c =匯編不鏈接(因為我們用link.exe來做這項工作) /coff = 產生COFF格式的object(對象)文件,這是Windows可執行文件的標准格式。 first.asm = a匯編first.asm文件

link /subsystem:windows first.obj

鏈接器把object文件和所有導入的dll與庫鏈接起來: /subsystem:windows = 創建Windows的可執行文件。 first.obj = 鏈接 first.obj

如果你把所有的事情都正確的完成了,並運行批處理文件。將產生first.exe。運行它,看看有什麼結果。

13.0-Windows中的窗口

在本章中,我們將創建一個有窗口的程序

13.1-窗口

你可能已經猜到了Windows之所以稱為Windows的原因了。在Windows中,有兩種程序:GUI程序和控制台程序。控制台模式的程序看上去就像Dos程序,它們在一個似-dos的窗口中運行。你使用的大多數程序是GUI(圖形用戶界面)程序,它們有一個用於和用戶交互的圖形界面。這是由創建窗口來完成的。幾乎你在Windows中看見的每一件東西都是窗口。首先,你創建一個父窗口,然後是像編輯框,靜態控件(文本標簽-譯者注),按鈕等的自窗口(控件)。

13.2-窗口類

每一個窗口都有名字。你為你的父窗口定義你自有的類。對於控件,你可以使用Windows的標准類名(例如,“Edit”,“Static”,“Button”)

13.3-結構

你程序中的窗口類是用“RegisterClassEx“函數注冊的。(RegisterClassEx是RegisterClass的擴展版本,後者已經不太使用了)這個函數的聲明是:

ATOM RegisterClassEx(
CONST WNDLCASSEX *lpwcx//有類數據的結構之地址
);

lpwcx:指向WNDCLASSEX結構。在把它傳遞給函數之前,你必須用適當的類屬性填寫結構。

唯一的參數是指向結構的指針。先來看看一些結構的基本知識:

一個結構是一些變量(數據)的集合。它用STRUCT定義:

SOMESTRUCTURE STRUCT
dword1 dd ?
dword2 dd ?
some_word dw ?
abyte db ?
anotherbyte db ?
SOMESTRUCTURE ENDS
(結構名不一定要大寫)

你可以用問號把你的變量定義在未初始化data部分。現在你可以根據定義創建一個結構:

Initialized
Initializedstructure SOMESTRUCTURE <100,200,10,'A',90h>
Uninitialized
UnInitializedstructure SOMESTRUCTURE <>

在第一個例子中,創建了一個新的結構(用初始化了的結構保存它的offset),而且結構的每一個元素用初始化數值填寫了。第二個例子只是告訴masm為結構名分配內存,而且每個數據元素用0初始化。在創建了結構之後,你可以在代碼中使用它:

mov eax, Initializedstructure.some_word
; eax現在是 10
inc UnInitializedstructure.dword1
; 結構的dword1步增

結構是這樣存在內存中的:

內存地址 內容

offset of Initializedstructure 100 (dword, 4 bytes)
offset of Initializedstructure + 4 200 (dword, 4 bytes)
offset of Initializedstructure + 8 10 (word, 2 bytes)
offset of Initializedstructure + 10 65 or 'A' (1 byte)
offset of Initializedstructure + 11 90h (1 byte)

12.3-WNDCLASSEX

現在已經了解了足夠多的結構知識,讓我們處理RegisterClassEx吧。在《win32程序員參考》中,你可以查找WNDCLASSEX結構的定義。

typedef struct _WNDCLASSEX { //
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;

解釋

cbSize
WNDCLASSEX結構體的大小。用於Windows的認證。你可以用SIZEOF得到它的大小: mov wc.cbsize, SIZEOF WNDCLASSEX

style

為類指定一個樣式(如果窗口要有滾動條,加上重畫標志。等等)

lpfnWndProc

指向Windows Procedure的指針(本章後面有更多內容)

cbClsExtra

在Windows類結構後本配多少額外內存。對我們不重要

cbWndExtra

在Windows實例後分配多少額外內存。對我們也不重要

hInstance

你程序的實力句柄。你可以用GetMoudleHandle函數得到這個句柄

hIcon

窗口圖標資源的句柄

hCursor

窗口光標資源的句柄

hbrBackground

用於填充背景的畫刷句柄,或是標准刷子類型中的一個,如 COLOR_WINDOW, COLOR_BTNFACE , COLOR_BACKGROUND.

lpszMenuName

指向一個指定菜單類名的零結尾字符串

lpszClassName

指向一個指定窗口類名的零結尾字符串

hIconSm

一個和窗口類關聯的小圖標句柄

在你的Win32文件夾中創建一個名為firstWindow的文件夾並在這個文件夾中創建一個名為window.asm的新文件,輸入一下內容:

.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc

然後創建一個名為make.bat的.bat文件。把這些文本粘貼進去:

@echo off
ml /c /coff window.asm
link /subsystem:windows window.obj
pause>nul

從現在開始,為了節省空間,僅顯示小段的代碼。你可以通過點來顯示教程此處的全部代碼。完整的代碼在新窗口中顯示。

譯者注:為了方便,我又把這些放回來了。

13.4-注冊類

現在我們在名為WinMain的過程中注冊類。該過程中完成窗口的初始化。

把這些加入你的匯編文件:

WinMain PROTO STDCALL :DWORD, :DWORD, :DWORD
.data?
hInstance dd ?
.code
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke WinMain, hInstance, NULL, NULL, SW_SHOWNORMAL
end start

這些代碼通過GetModuleHandle得到模塊句柄,並把模塊句柄放入hInstance變量中。這個句柄在Windows API中頻繁使用。然後它調用WinMain過程。這不是一個API函數,而是一個我們將要定義的過程。原型是:WinMain PROTO STDCALL :DWORD, :DWORD, :DWORD, :DWORD,因而是一個帶4個參數的函數:

現在把這些代碼放在end start:前

WinMain proc hInst:DWORD, hPrevInst:DWORD, CmdLine:DWORD, CmdShow:DWORD
ret
WinMain endp

你根本就不需要用這個winmain過程,但這是一種十分普遍的處世化你的程序的方法。Visual C自動初始化這個函數的參數,但我們必須自己來做。現在不要管hPrevInst和CmdLine。集中注意在hInst和CmdShow上。Hinst是實例句柄(=模塊句柄),CmdShow是定義窗口該如何顯示的標志。(你可以在API參考關於ShowWindows部分發現更多)

在前面代碼中的"invoke WinMain, hInstance, NULL, NULL, SW_SHOWNORMAL"用正確的實例句柄和顯示標志調用這個函數。現在我們可以在WinMain中寫我們的初始化代碼了。

WinMain proc hInst:DWORD, hPrevInst:DWORD, CmdLine:DWORD, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL hwnd:DWORD
ret
WinMain endp

這有我們將在過程中要用的兩個局部變量

.data
ClassName db "FirstWindowClass",0
.code
WinMain proc hInst:DWORD, hPrevInst:DWORD, CmdLine:DWORD, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL hwnd:DWORD
; now set all the structure members of the WNDCLASSEX structure wc:
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon, eax
mov wc.hIconSm, eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, ADDR wc
ret
WinMain endp

讓我們來看看發生了什麼:

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL

初始化了結構的大小(這是RegisterClassEx要求的)。設置類的樣式為”CS_HREDRAW or CS_VREDRAW”,然後設置了窗口過程的offset。你在後面會知道什麼是窗口過程,現在你僅需要記住你需要WndProc過程的地址。該地址可以通過“offset WndProc”獲得。Cb.ClsExtra和cb.WndExtra我們沒有使用因而設它們為Null。

Push hInst
Pop wc.hInstance

Wc.hInstance設為WinMain的hInst參數。為什麼我們不用:mov wc.hInstance, hInst?因為mov指令不允許從一個地址移到另一個地址。通過push/pop,值被壓入棧,然後又彈入目標中。

mov wc.hbrBackground, COLOR_WINDOW
mov wc.lpszMenuName, NULL
mov wc.lpszClassName, OFFSET ClassName

類的背景色被設為COLOR_WINDOW,沒有定義菜單(null)而且lpszClassName設為一個指向零結尾的類名字符串:“FirstWindowClass”它應該是一個在你的程序中定義的唯一名字。

invoke LoadIcon, NULL, IDI_APPLICATION
mov wc.hIcon, eax
mov wc.hIconSm, eax

窗口需要一個圖標。但又因為我們要一個指向圖標的句柄,我們使用LoadIcon來載入圖標並獲得句柄。LoadIcon有兩個參數:hInstance和lpIconName。HInstance是包含圖標的可執行文件的模塊句柄。LpIconName是一個指向圖標資源和圖標ID的字符串的指針。如果你用NULL為hInstance,你可以從一些標准圖表中選這一個(這卻是是因為我們在這裡還沒有圖標資源)hIconSm是小圖標,你可以對它使用相同的句柄。

invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax

對光標也一樣。NULL作hInstance,並用一個標准光標類型:IDC_ARROW,標准Windows箭頭型光標。

invoke RegisterClassEx, ADDR wc

現在,最終用RegisterClassEx來注冊類,通過一個指向WNDCLASSEX結構的指針作參數。

13.5-創建窗口

現在,你已經注冊了一個類,你可以使用它創建一個窗口:

HWND CreateWindowEx(
DWORD dwExStyle, // extended window style
LPCTSTR lpClassName, // pointer to registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // handle to menu, or child-window identifier
HINSTANCE hInstance, // handle to application instance
LPVOID lpParam // pointer to window-creation data
);

DwExstyle和dwStyle是兩個決定窗口樣式的參數。

LpClassName 是一個指向你注冊了的類名的指針。

LpWindowName 是你窗口的名字(如果有的話,這將成為你窗口的標題)

X, Y, nWidth, nHeight 決定你窗口的位置和大小

HMenu 是菜單窗口的句柄(在後面討論,現在為空)

HInstance 是程序實例的句柄

LpPararm 是你能在你的程序中使用的擴展值

.data
AppName "FirstWindow",0
.code
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,400,300,NULL,NULL,\
hInst,NULL
mov hwnd, eax
invoke ShowWindow, hwnd, SW_SHOWNORMAL
invoke UpdateWindow, hwnd

(注意\使匯編器讀下一行的時候好像還在同一行)

我們的代碼將用我們剛剛注冊的類名創建一個新的窗口。標題是“FirstWindow”(程序名,AppName),樣式是WS_OVERLAPPEDWINDOW,這是一個創建有標題,系統菜單,可縮放邊框和最大化/最小化按鈕的窗口樣式。CW_USERDEFAULT作為x和y的位置會使Windows為新窗口使用缺省位置。窗口的(初始)大小是400×300象素。

函數的返回值是窗口句柄,HWND。它儲存在局部變量hwnd中。然後窗口用ShowWindow顯示。UpdateWindow確保窗口被畫出。

13.6-消息循環

窗口可以通過消息和你的程序以及其他窗口通訊。無論何時,一條消息被發送給指定的窗口。它的窗口過程都要被調用。每個窗口都有一個消息循環或消息泵(pump)。這是一個無止盡的檢查是否給有你的窗口的消息的循環。而且如果有,把消息傳遞給dispatchMessage函數。這個函數會調用你的窗口過程。消息循環和窗口過程是兩個完全不同的東西!!!

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL hwnd:DWORD
LOCAL msg:MSG ;<<
........
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW

這是消息循環看上去的樣子。.WHILE TRUE, .ENDW循環到eax為0之前都會繼續。如果它接到了WM_QUIT消息,GetMessage返回0,這將關閉窗口因而程序應該在不論GetMessage返回0時退出。如果不是這樣(0),消息被傳遞給TranslateMessage(這個函數把按鍵翻譯為消息)而且消息被Windows用DispatchMessage函數解包。消息本身在一個消息循環的組成部分MSG結構中(LOCAL msg: MSG被加入過程,增加了一個稱為msg的局部消息結構)你可以在你的所有程序中用這個消息循環。

13.7-窗口過程

消息會被發送往窗口過程。一個窗口過程看上去總是這樣:

WndProc PROTO STDCALL :DWORD, :DWORD, :DWORD, :DWORD
.code
WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
mov eax, uMsg
.IF eax==XXXX
.ELSEIF eax==XXXX
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.ENDIF
ret
WndProc endp

窗口過程總是有4個參數

hWnd 包含窗口句柄
uMsg 消息
wParam 消息的第一個參數(由消息定義)
lParam 消息的第二個參數(由消息定義)

窗口不處理的消息應該傳遞給DefWindowProc,它會處理這些。一個窗口過程的例子:

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
mov eax, uMsg
.IF eax==WM_CREATE
invoke MessageBox, NULL, ADDR AppName, ADDR AppName, NULL
.ELSEIF eax==WM_DESTROY
invoke PostQuitMessage, NULL
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.ENDIF
ret
WndProc endp

這段代碼在窗口初始化時顯示程序名稱。也要注意我加入了WM_DESTROY消息的處理。這條消息在窗口將要關閉的時候發送。程序要用PostQuitMessage作出反應。

現在看看最終的代碼:

.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc
WinMain PROTO STDCALL :DWORD, :DWORD, :DWORD, :DWORD
WndProc PROTO STDCALL :DWORD, :DWORD, :DWORD, :DWORD
.data?
hInstance dd ?
.data
ClassName db "FirstWindowClass",0
AppName db "FirstWindow",0
.code
start:
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke WinMain, hInstance, NULL, NULL, SW_SHOWNORMAL
invoke ExitProcess, NULL
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL hwnd:DWORD
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon, eax
mov wc.hIconSm, eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW-WS_SIZEBOX-WS_MAXIMIZEBOX,CW_USEDEFAULT,\
CW_USEDEFAULT,400,300,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
mov eax, uMsg
.IF eax==WM_CREATE
invoke MessageBox, NULL, ADDR AppName, ADDR AppName, NULL
.ELSEIF eax==WM_DESTROY
invoke PostQuitMessage, NULL
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.ENDIF
ret
WndProc endp
end start

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved