Win32Asm 教程,來自易語言俱樂部論壇,譯者taowen。 (5千字)

看雪資料發表於2002-03-10

Win32Asm 教程
這是我的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或者別的什麼??-彙編產生的程式更小而且更快。在有人工智慧的高階程式語言中,編譯器要產生輸出程式碼變得(比彙編)更困難。編譯器必須指出最快(或最小)的方式產生彙編程式碼,而且雖然編譯器變得越來越好,你自己來寫(彙編)程式碼(包括可選的程式碼最佳化)能生成更小更快的程式碼。但是,當然,這比高階語言難多了。還有另一個與某些使用執行時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指標標明在段本身的位置。看下圖:
記憶體
段 1 (64kb)段 2 (64kb)段 3 (64kb)段 4(64kb)更多
注意下面關於16位程式的解釋,後面有更多關於32位的(但不要跳過這部分,要理解32位的記憶體管理,這部分很重要)上表是全部的那記憶體,被劃分成了64kb的多個段。最多有65536個段。現在取出一段:
段 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
譯者taowen

相關文章