這是我的Win32Asm教程。它總是創建中,但我會不停地添加內容。通過上面的next和prev鏈接,你可以轉到后面和前面一頁。
介紹
先來對這個教程做個小介紹。Win32Asm不是一個非常流行的編程語言,而且只有為數不多(但很好)的教程。大多數教程都集中在編程的win32部分(例如,winAPI,使用標準Windows編程技術等),而不是匯編語言本身,例如偽代碼(opcodes),寄存器(registers)的使用等。雖然你能在其他教程中找到這些,但那些教程通常是解釋Dos編程的。它當然可以幫你學匯編語言,但在Windows中編程,你不再需要了解Dos中斷(interrupt)和斷口(port)In/our函數。在Window中,WindowsAPI提供了你可在你的程序中使用的標準功能(function),后面還會對此有更多內容。這份教程的目標是在解釋用匯編編Win32程序的同時學習匯編語言本身。
1.0匯編語言
匯編是創造出來代替原始的由處理器理解的二進制代碼的。很久以前,但是尚沒有任何高級語言,程序是用匯編寫的。匯編代碼直接描述處理器可以執行的代碼,例如:
add eax,edx
這條指令-add-把兩個值加到一起。Eax和edx被稱為寄存器,它們可以保存值在處理器內部。這條代碼被轉換為66 03 c2(16進制)。處理器讀這行代碼,并執行它所代表的指令,像C等高級語言把它們自己的語言翻譯為匯編語言,而匯編程序又把它轉換為二進制代碼:
C 代碼>> C編譯器 > >匯編語言>>匯編器>>原始輸出(十六進制)
a = a + b;add eax, edx66 03 C2
(注意該處的匯編語言的代碼被簡化了,實際輸出決定于C代碼的上下文)
1.1-為什么?(Why?)
既然用Asm寫程序更困難,為什么你用Asm而不是C或者別的什么??-匯編產生的程序更小而且更快。在有人工智能的高級編程語言中,編譯器要產生輸出代碼變得(比匯編)更困難。編譯器必須指出最快(或最?。┑姆绞疆a生匯編代碼,而且雖然編譯器變得越來越好,你自己來寫(匯編)代碼(包括可選的代碼優化)能生成更小更快的代碼。但是,當然,這比高級語言難多了。還有另一個與某些使用運行時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中。而且,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]
鏈接器
我的選擇:微軟附加鏈接器(link.exe)
網址:win32asm.cjb.net(在win32asm包中)
描述:鏈接器把對象(object)文件和庫文件(用與DLL導入)“鏈接”到一起輸出最終的可執行文件。
關于:我會用Iczelion的Win32asm包中的link.exe。但大多數的鏈接器都可以用。
供選擇:Tasm linker[dl]
資源編輯器
我的選擇:Borland資源編輯器
網址:www.crackstore.com
描述:用于創建資源(圖形,對話框,位圖,菜單等)的資源編輯器。
關于:大多數的編輯器都可以。我個人愛好是resource workshop但你可以用你喜歡的。注意由于resource workshop創建的資源文件有時給資源編譯帶來麻煩,如果你想使用這個編輯器,你應當把tasm一起下下來,他里面包含了用于編譯borland風格資源的brc32.exe。
供選擇:Symantec資源編輯器,資源創建者(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(函數列表視圖)。
在某個地方創建一個win32文件夾(或其他你喜歡的名字),并為你創建的每一個工程創建一個子文件夾。
3.0 asm基礎知識
這章將教你匯編語言的基礎知識
3.1偽代碼(opcodes)
匯編程序是用偽代碼創建的。一個偽代碼是一條處理器可以理解的指令。例如:
ADD
Add指令把兩個數加到一起。大部分偽代碼有參數
ADD eax, edx
ADD有兩個參數。在加法的情況下,一個源一個目標。它把源值加到目標值中,并把結果保存在目標中。參數有很多不同的類型:寄存器,內存地址,直接數值(immediate values)如下:
3.2寄存器
有幾種大小的寄存器:8位,16位,32位(在MMX處理器中有更多)。在16位程序中,你僅能使用16位和8位的寄存器。在32位的程序中,你可以使用32位的寄存器。
一些寄存器是別的寄存器的一部分:例如,如果EAX保存了值EA7823BBh這里是其他寄存器的值。
EAXEA7823BB
AXEA7823BB
AHEA7823BB
ALEA7823BB
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, 12345678hMov把一個值載入寄存器(注意: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)指針
譯者taowen
4.0內存
這部分將解釋在Windows中內存是如何管理的。
4.1Dos和win3.xx
在像用于Dos和Win3.xx的16位程序中,內存被分成許多個段。這些段的大小為64kb。為了存儲內存,需要一個段指針和一個偏移址指針。段指針標明要使用哪個段,offset指針標明在段本身的位置??聪聢D:
內存
段 1 (64kb)段 2 (64kb)段 3 (64kb)段 4(64kb)更多
注意下面關于16位程序的解釋,后面有更多關于32位的(但不要跳過這部分,要理解32位的內存管理,這部分很重要)上表是全部的那內存,被劃分成了64kb的多個段。最多有65536個段?,F在取出一段:
段 1(64kb)
Offset 1Offset 2Offset 3Offset 4Offset 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保存了當前棧的offset1,在ss(棧段中)。
4.2 32位Windows
你可能已經注意到了關于段的一切是乏味的。在16位編程中,段是必不可少的。幸運的是,這個問題已經在32位Windows(95及以上)中得到解決。你仍然有段,但不用管他們了因為它們不再是64kb,而是4GB。你如果嘗試著改變段寄存器中的一個,windows甚至會崩潰。這稱為平坦(flat)內存模式。只有offset而且是32位的,因而范圍從0到4,294,967,295。內存中的每一個地址都是用offset表示的。這真是32位勝于16位的最大優點。所以,你現在可以忘了段寄存器并把精神集中在其他的寄存器上。
譯者taowen
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中,前一章中有更多信息)你也能從地址的某一個地方獲得一個值并把它放入一個寄存器中。下面有一個例子:
offset3435363738393A3B3C3D3E3F404142
data0D0A50324457257A5E72EF7DFFADC7
每一個塊代表一個字節
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 目標,源
執行的運算是 目標=目標+源。下面的格式是允許的。
目標源例子
RegisterRegisteradd ecx, edx
RegisterMemoryadd ecx, dword ptr [104h] / add ecx, [edx]
RegisterImmediate valueadd eax, 102
MemoryImmediate valueadd dword ptr [401231h], 80
MemoryRegisteradd 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 / sourceALAH
WORD (16-bits)dx:ax* / sourceAXDX
DWORD (32-bits)edx:eax* / sourceEAXEDX
*:例如,如果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放入目標位中。
指令ANDORXORNOT
源位00110011001101
目標位010101010101XX
輸出位00010111011010
如果源和目標均為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(循環右移)
Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1 Bit 0
原來 10011011
循環移動,計數=3 100110 1 1 (移出)
結果 01110011
如你在上圖所見,位循環了。注意,每個被推出的位又移到了另一邊。和Shifting一樣,carry位裝有最后被移出的位。Rcl和Rcr實際上和Rol,Rcr一樣。它們的名字暗示了它們用carry位來表明最后移出的位,但和Rol和Ror干同樣的事情。它們沒有什么不同。
交換
XCHG指令也非常簡單。它同在兩個寄存器和內存地址之間交換:
eax = 237h
ecx = 978h
xchg eax, ecx
eax = 978h
ecx = 237h