一、關於寄存器
寄存器有EAX,EBX,ECX,EDX,EDI,ESI,ESP,EBP等,似乎IP也是寄存器,但只有在CALL/RET在中會默認使用它,其它情況很少使用到,暫時可以不用理會。 EAX是WIN32 API 默認的返回值存放處。 ECX是LOOP指令自動減一的寄存器。 ESP是堆棧指針。 EBP經常用來在堆棧中尋址。 ESI好像常常用在指針尋址中,EDI不大清楚。
二、關於內存尋址
WIN32中內存是平坦的,對於每個程序來說都可以使用2G範圍的地址,但各個程序之間並不會干擾,這是因爲各個程序所使用到的物理內存被Windows自行安排,不會互相覆蓋,而且一個程序不會隨意地訪問到另一個程序的地址空間。
三、關於堆棧
Windows爲每個程序安排了堆棧段,它是從高地址向低地址延伸的,之所以採用這種方式,是因爲這樣可以使堆棧指針始終指向最近入棧的元素的起始地址,這樣的話,爲訪問這個元素提供了非常便利的方式。
ESP作爲堆棧指針始終指向棧頂,如果看一下PUSH和POP的操作就可以明白這句話: PUSH: ESP <-- ESP-4 (ESP+3,ESP) <-- 入棧元素 POP: 出棧元素 <-- (ESP+3,ESP) ESP <-- ESP+4
因爲PUSH和POP自動修改了ESP的值,使它始終指向棧頂了。當然也可以自己來修改ESP的值,例如我們可以: sub esp,4 ;這樣就把棧頂指針向下移動了。 這種操作常常用在局部變量的分配中,在子程序中使用到局部變量時,就在堆棧中爲它們提供空間,這樣可以使子程序退出時收回局部變量佔用的空間,有利於子程序的模塊化。
我們可以用ESP來尋址堆棧中的元素,比如ESP指向當前棧頂元素的起始地址,ESP-4指向前一個元素的起始地址,不過因爲ESP常常在變化,這樣用ESP在堆棧中尋址的話不方便,所以我們就用EBP來代替ESP尋址,首先把EBP入棧保存,然後把ESP賦值給EBP,這樣就可以用EBP來尋址堆棧中的數據了。我用一個例子來說明堆棧的變化。
push 0x00000001 ;1 push ebp ;2 mov ebp,esp ;3 push 0x12345678 ;4 mov eax,dword ptr[ebp+4] ;5 mov ebx,dword ptr[ebp-4] ;6 mov ax,word ptr[ebp-2] ;7 mov al,byte ptr[ebp-1] ;8 mov al,byte ptr[ebp-3] ;9 mov ax,word ptr[ebp-3] ;10
5 eax=0x00000001 6 ebx=0x12345678 7 ax=0x1234 8 al=0x12 9 al=0x56 10 ax=0x3456
堆棧使用在子程序的實現中,當調用子程序時,首先把參數入棧,然後把返回IP入棧,然後轉移到子程序處,如果有局部變量,則下移ESP,然後初始化該局部變量,這樣用到EBP來尋址局部變量,參數的尋址同樣要用到EBP。
四、簡單的幾個關鍵字
ptr 顯式指定後面的數據的類型 offset 全局變量的地址 addr 局部變量的地址,也可以用在全局變量上 local 定義局部變量 proc 定義子程序 proto 聲明子程序
五、例子
Hello.asm文件的內容如下: ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 第一部分:模式和源程序格式的定義語句 .386 ; 指令集 .model flat,stdcall ; 工作模式 option casemap:none ; 格式 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; Include 文件定義 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 數據段 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .data szCaption db 'A MessageBox !',0 szText db 'Hello, World !',0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 代碼段 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .code start: invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK invoke ExitProcess,NULL ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> end start ; 指定程序的入口
1. 第一部分模式和源程序格式的定義語句 第一行 指定使用的指令集(編譯器使用) Win32環境工作在80386及以上的處理器中,所以必須定義.386。如果程序(VxD等驅動程序)中要用到特權指令,那麼必須定義.386p。 第二行 定義程序工作的模式(包括內存模式、語言模式、其它模式) 對Win32程序來說,只有一種內存模式,即flat(平坦)模式。 Win32 API調用使用的是stdcall格式,所以Win32彙編中必須在.model中加上stdcall參數。 第三行 option語句 由於Win32 API中的API名稱區分大小寫,所以必須定義option casemap:none,來表明程序中的變量和子程序名對大小寫敏感。
2. 包含全部段的源程序結構: .386 .model flat,stdcall option casemap:none <一些include語句> .stack [堆棧段的大小] .data <一些初始化過的變量定義> .data? <一些沒有初始化過的變量定義> .const <一些常量定義> .code <代碼> <開始標記> <其他語句> end 開始標記
3. 段的定義 數據段 .data 已初始化數據段,可讀可寫的已定義變量; 當程序裝入完成時,這些值就已經在內存中; 數據定義在.data段中會增加可執行文件的大小; .data段一般存放在可執行文件的_DATA節區(Section)內; .data? 未初始化數據段,可讀可寫的未定義變量,在可執行文件中不佔空間; 這些變量一般作爲緩衝區或者在程序執行後纔開始使用。 數據定義在.data?數據段中不會增加可執行文件的大小; .data?段一般存放在可執行文件的_BSS節區內; .const 常量,可讀不可寫的變量;
代碼段 .code 所有的指令都必須寫在代碼段中; Win32中,數據段是不可執行的,只有代碼段有可執行的屬性; 對於運行在特權級3的應用程序,.code段不可寫。除非把可執行文件PE頭部中的屬性位改成可寫; 對於運行在特權級0的程序,所有的段都有讀寫權限,包括代碼段; .code代碼段一般存放在可執行文件的_TEXT節區內;
堆棧段 .stack 與DOS彙編不同,Win32彙編不必考慮堆棧。系統會自動分配堆棧空間; 堆棧段的內存屬性是可讀寫並且可執行; 靠動態修改代碼的反跟蹤模塊可以拷貝到堆棧中去邊修改邊執行; 緩衝區溢出技術也會用到這個特性;
4. 調用操作系統功能的方法: DOS下 操作系統的功能通過各種軟中斷來實現。 應用程序調用操作系統功能將經歷如下三個過程: 把相應的參數放在各個寄存器中再調用相應的中斷; 程序控制權轉到中斷中去執行; 完成以後通過iret中斷返回指令回到應用程序中; DOS下調用系統功能方法的缺點: 所有的功能號定義是難以記憶的數字; 80x86系列處理器能處理的中斷最多只能有256個; 通過寄存器來傳遞參數,對於參數較多的函數很不方便; Win32下 系統功能模塊放在Windows的動態鏈接庫(DLL)中 作爲Win32 API核心的3個DLL: KERNEL32.DLL 系統服務功能。 GDI32.DLL 圖形設備接口。 USER32.DLL 用戶接口服務。
常用API的參數和函數聲明,查看文檔《Microsoft Win32 Programmer's Reference》
5. Win32 API的函數原型聲明 函數原型聲明的彙編格式如下: 函數名 proto [距離] [語言] [參數1]:數據類型, [參數2]:數據類型,...... proto是函數聲明的僞指令 距離可以設置爲NEAR、FAR、NEAR16、NEAR32、FAR16或FAR32,由於Win32中只有一個平坦的段,無所謂距離,所以在定義時可以忽略距離。 語言類型可是使用.model所定義的默認值。
以消息對話框函數MessageBox爲例 C格式如下: int MessageBox( HWND hWnd, // Handle to owner window LPCTSTR lpText, // text in message box LPCTSTR lpCaption, // message box title UINT uType // message box style );
彙編格式如下: MessageBox Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword 或者寫爲 MessageBox Proto :dword,:dword,:dword,:dword 編譯器只對參數的數量和類型感興趣,參數的名稱只是增加可讀性,所以可以省略。 對於彙編語言來說,Win32環境中的參數實際上只有一種類型,就是一個32位的整數(dword,double word),雙字,四字節。
6. 調用Win32 API 調用API有如下兩種方法: 1) invoke invoke是MASM提供的僞指令; invoke僞指令的好處就是能夠提高代碼的可讀性,減少錯誤; invoke做了下面三件事: 在編譯的時候,由編譯器把invoke僞指令展開成相應的push指令和call指令; 進行參數數量的檢查工作; 如果帶的參數數量和聲明時的數量不符,編譯器會報錯; 2) push和call的組合 80386處理器的指令
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK 也可寫爲 push NULL push offset szText push offset szCaption push MB_OK call MessageBox
7. Win32 API函數返回值的處理方法 對於彙編語言來說,Win32 API函數返回值的類型只有dword一種類型,它永遠放在eax中。 如果要返回的內容在一個eax中放不下,Win32 API採用如下方法來解決: a) 一般是eax中返回一個指向返回數據的指針; b) 在調用參數中提供一個緩衝區地址,數據直接返回到這個緩衝區中去。類似變參的概念;
[color=#0000FF]8. 與字符串相關Win32 API的分類 在Win32環境中,根據兩個不同的字符集(ANSI字符集和Unicode字符集),可以把和字符串相關的API分成兩類: a) 處理ANSI字符集的Win32 API函數 函數名稱的尾部帶一個“A”字符; ANSI字符串是以NULL結尾的一串字符數組,每一個ANSI字符佔一個字節的寬度; 例如:MessageBoxA Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword b) 處理Unicode字符集的Win32 API函數 函數名稱的尾部帶一個“W”字符; 每一個Unicode字符佔兩個字節的寬度,所以可以同時定義65536個不同的字符; 例如:MessageBoxW Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword
Windows 9x系列不支持Unicode版本的API,絕大多數的API只有ANSI版本。 只有Windows NT系列才完全支持Unicode版本的API。 爲了編寫在幾個平臺中都能通用的程序,一般應用程序都使用ANSI版本的API函數集。
提高程序可移植性的一個方法: 一般在源程序中不直接指明使用Unicode還是ANSI版本,而是使用宏彙編中的條件彙編功能來統一替換。 比如,在頭文件中做如下定義: if UNICODE MessageBox equ <MessageBoxW> else MessageBox equ <MessageBoxA> endif 然後在源程序的頭部指定UNICODE=1或UNICODE=0,重新編譯後就能產生不同的版本。
9. include語句 include語句的語法是: include 文件名 或 include <文件名> 用“<>”將文件名括起來,可以避免黨文件名和MASM的關鍵字同名時引起編譯器混淆。
include語句的作用: 解決了所用到的Win32 API函數都必須預先聲明的麻煩。 把所有用到的Win32 API函數聲明預先放在一個頭文件中,然後用include語句包含進源程序。
編譯器對include語句的處理方法,僅是簡單地用指定的文件內容把這行include語句替換掉而已。
和C語言中的#include作用類似。
Re:外掛源代碼&教學&工具下載[包含商業源代碼]
10. includelib語句 includelib語句的語法是: includelib 庫文件名 或 includelib <庫文件名> 用“<>”將文件名括起來,同樣可以避免當文件名和MASM的關鍵字同名時引發編譯器混淆。
includelib語句的作用是: 告訴鏈接器使用哪些導入庫。
導入庫 WIN32中,API函數的實現代碼放在DLL中,導入庫中只留有API函數的定位信息和參數數目等簡單信息。
DOS下的函數庫是靜態庫 C語言的函數庫是典型的靜態庫 靜態庫的好處是節省大量的開發時間。 靜態庫的缺點是每個可執行文件中都包含了要用到的相同函數的代碼,即佔用了大量的磁盤空間,執行的時候,這些代碼也會重複佔用內存。
includelib語句和include語句的處理不同,includelib不會把.lib文件的內容插入到源程序中,它只是告訴鏈接器在鏈接的時候到指定的庫文件中去找Win32 API函數的位置信息而已。
11. MASM中標號和變量的命名規範 MASM中標號和變量的命名規範是相同的,如下: 1) 可以用字母、數字、下劃線及符號@、$和?。 2) 第一個符號不能是數字。 3) 長度不能超過240個字符。 4) 不能使用指令名等關鍵字。 5) 在作用域內必須是唯一的。
12. 標號 標號有如下兩種定義方法: 標號名: 目的指令 ;方法1 或 標號名:: 目的指令 ;方法2 方法1和方法2是不同的 方法1 標號名的後面跟一個冒號,表示標號的作用域是當前的子程序。 在單個子程序中的標號不能同名,不能從一個子程序中用跳轉指令跳到另一個子程序中。 方法2 標號名的後面跟兩個冒號,表示標號的作用域是整個程序。 對任何其它子程序都是可見的。
在低版本MASM中,默認標號的作用域是整個程序。 在高版本MASM中,默認標號的作用域是當前的子程序。
高版本MASM中的@@標號 當用@@做標號時,可以用@F和@B來引用; @F表示本條指令後的第一個@@標號; @B表示本條指令前的第一個@@標號;
不要在間隔太遠的代碼中使用@@標號,源程序中@@標號和跳轉指令之間的距離最好限制在編輯器能夠顯示的同一屏幕的範圍內。
13. 全局變量 全局變量的作用域是整個程序 Win32彙編的全部變量定義在.data或.data?段內,這兩個段都是可寫的。可以同時定義變量的類型和長度。
全局變量的定義格式如下: 變量名 類型 初始值1,初始值2,...... 變量名 類型 重複數量 dup (初始值1,初始值2,......)
MASM支持的變量類型如下表: 名稱 表示方式 縮寫 長度(字節) 字節 Byte db 1 字 word dw 2 雙字(double word) dword dd 4 三字(far word) fword df 6 四字(quad word) qword dq 8 10字節BCD碼(ten byte) tbyte dt 10 有符號字節(sign byte) sbyte 1 有符號字(sign word) sword 2 有符號雙字(sign dword) sdword 4 單精度浮點數 Real4 4 雙精度浮點數 Real8 8 10字節浮點數 Real10 10
注意:只有定義全局變量的時候,類型纔可以用縮寫。
在byte類型變量的定義中,可以用引號定義字符串和數值定義的方法混用。 例如:szText db ‘Hello,world!’,0dh,0ah,’Hello again’,0dh,0ah,0
全局變量的初始化: 全局變量在定義中既可以指定初值,也可以只用問號預留空間。 全局變量定義在.data?段中時,只能用問號預留空間,因爲.data?段不能指定初始值。 定義時用問號指定的全局變量的初始值是0。
14. 局部變量 局部變量的好處是使程序的模塊結構更加分明。 局部變量的缺點是因爲空間是臨時分配的,所以無法定義含有初始化值的變量,對局部變量的初始化一般在子程序中由指令完成。 局部變量的作用域是單個子程序。 局部變量定義在堆棧中。
局部變量的定義格式如下: local 變量名1[[重複數量]][:類型],變量名2[[重複數量]][:類型] ...... local是MASM提供的僞指令,用於支持局部變量的定義。有了local僞指令降低不少難度。
定義局部變量需注意以下幾點: a) local僞指令必須緊接在子程序定義的僞指令proc後、其它指令開始之前,因爲局部變量的數目必須在子程序開始的時候就確定下來; b) 定義局部變量時數據類型不能用縮寫。如果要定義數據結構,可以用數據結構的名稱當作類型; c) Win32彙編中,參數的默認類型是dword,如果定義dword類型的局部變量,類型可以省略; d) 當定義數組類型的局部變量時,重複數量可以用“[]”括起來,不能使用定義全局變量的dup僞指令。 e) 局部變量不能和已定義的全局變量同名。 f) 局部變量的作用域是當前的子程序,所以在不同的子程序中可以有同名的局部變量。
局部變量的初始化: 局部變量無法在定義的時候指定初始化值,因爲local僞指令只是爲局部變量留出空間。 局部變量的初始值是隨機的,所以,對局部變量的值一定要初始化。 一般在子程序中使用指令來初始化局部變量。
RtlZeroMemory這個Win32 API函數實現將整個數據結構填0的功能,類似C語言的memset。
在原來的DOS環境下,低版本的MASM中,所有的變量都相當於現在所說的全局變量,都定義在數據段裏面。 用彙編語言在堆棧中定義局部變量非常麻煩,需要作一張表,表上的內容是局部變量名和 ebp指針的位置關係。
15. 使用局部變量的一個典型例子與反彙編得到指令的比較: TestProc proc local @loc1:dword,@loc2:word local @loc3:byte
mov eax,@loc1 mov ax,@loc2 mov al,@loc3
TestProc endp
反編譯後得到以下指令: :00401000 55 push ebp :00401001 8BEC mov ebp,esp :00401003 83C4F8 add esp,FFFFFFF8 :00401006 8B45FC mov eax,dword ptr [ebp-04] :00401009 668B45FA mov ax,word ptr [ebp-06] :0040100D 8A45F9 mov al,byte ptr [ebp-07] :00401010 C9 leave :00401011 C3 ret
其中的 push ebp ; 把原來ebp寄存器的值保存起來; mov ebp,esp ; 把esp寄存器的值複製到ebp寄存器中,供存取局部變量時做指針用; add esp,FFFFFFF8 ; 在堆棧中預留出空間(即重新設置堆棧指針),由於堆棧是向下增長,所以要把esp加上一個負值。 三條指令用於局部變量的準備工作。
在堆棧中預留出空間時,把esp加上(-8),而不是加上(-7),是因爲在80386處理器中,以dword爲界對齊時存取內存的速度最快。以空間換時間。
leave是80386指令集中的一條指令,用於局部變量的掃尾工作。 一條leave指令就實現了mov esp,ebp和pop ebp兩條指令的功能。 mov esp,ebp ; ebp寄存器中保存了正確的初始esp值,所以把正確的esp設置回去後,ret指令就能從堆棧中取出正確的地址返回。 pop ebp ; 執行這條語句之後,堆棧就是正確的。
由於esp寄存器在程序的執行過程中可能隨時會被用到,所以不可能用esp寄存器做指針來存取堆棧中的局部變量。 ebp寄存器也是以堆棧段爲默認數據段的,所以可以用ebp做指針來存取堆棧中的局部變量。
局部變量在堆棧中排列的順序如下表: ebp偏移 內容 ebp+4 由call指令推入的返回地址。 ebp push ebp指令推入的原ebp值,然後新的ebp就等於當前的esp寄存器的值。 ebp-4 第一個局部變量@loc1:dword (4個字節) ebp-6 第二個局部變量@loc2:word (2個字節) ebp-7 第三個局部變量@loc3:byte (1個字節)
使用局部變量時的注意點: a) ebp寄存器是關鍵,它起到保存原始esp寄存器值的作用; b) 另外,ebp寄存器隨時用做存取局部變量的指針基址,所以絕不能把ebp寄存器用於別的用途; c) ebp寄存器的值絕對不能被改變,把ebp寄存器的值改掉,程序就玩完;
16. 數據結構 數據結構相當於一種自定義的數據類型,類似C語言中的struct定義。 彙編中,數據結構的定義方法如下: 結構名 struct 字段1 類型 ? 字段2 類型 ? ...... 結構名 ends
定義數據結構並不會在某個段中產生數據,只有使用數據結構在數據段中定義數據後,纔會產生數據。
使用數據結構在數據段中定義數據的兩種方法如下: 第一種定義方法是未初始化的定義方法: .data? stWndClass WNDCLASS <> ......
第二種定義方法是定義的同時指定結構中個字段的初始值: .data stWndClass WNDCLASS <1,1,1,1,1,1,1,1,1,1> ......
彙編中,對數據結構變量的幾種引用方法如下: a) 最直接的方法: mov eax,stWndClass.lpfnWndProc 如果stWndClass結構變量在內存中的起始地址是403000h,那麼這句指令會被編譯成mov eax,[403004h] b) 在實際使用中,常有使用指針存取數據結構變量的情況: 如果使用esi寄存器做指針尋址 mov esi,offset stWndClass mov eax,[esi + WNDCLASS.lpfnWndProc] 第二句指令將被編譯成mov eax,[esi+4] c) 使用assume僞指令把寄存器預先定義爲結構指針,在進行操作: mov esi,offset stWndClass assume esi:ptr WNDCLASS mov eax,[esi].lpfnWndClass ...... assume esi:nothing 編譯後產生同樣的代碼,不過程序的可讀性比較好。 注意:在不使用esi寄存器做指針的時候要用assume esi:nothing取消定義。
結構的嵌套定義如下: NEW_WNDCLASS struct dwOption dword ? oldWndClass WNDCLASS <> NEW_WNDCLASS ends
引用嵌套的oldWndClass結構變量的lpfnWndProc字段的方法: assume esi:ptr NEW_WNDCLASS mov eax,[esi].oldWndClass.lpfnWndProc ...... assume esi:nothing
windows.inc文件定義了大部分Win32 API所涉及的常量和數據結構。
17. 以不同的類型訪問變量 MASM中以不同的類型訪問不會對變量造成影響。而C語言中的數據類型強制轉換過程中,數據的內容已經發生變化。 MASM中,如果要用指定類型之外的長度訪問變量,必須顯式地指出要訪問的長度,這樣,編譯器忽略語法上的長度校驗,僅使用變量的地址。 訪問變量是顯式地指出要訪問長度的方法是: 類型 ptr 變量名 例如: mov ax,word ptr szBuffer mov eax,dword ptr szBuffer 類型可以設置爲byte、word、dword、fword、qword、real8和real10。 類型必須和操作的寄存器長度匹配,否則無法通過編譯。
需要注意的是: 指定類型的訪問變量並不會去檢測長度是否溢出。
80386的字節序是: 低位數據在低地址,高位數據在高地址 舉例: 下面這段代碼存在長度溢出的問題。長度溢出即越界存取到相鄰的其它變量。 .data bTest1 db 12h wTest2 dw 1234h dwTest3 dd 12345678h ...... .code ...... mov al,bTest1 mov ax,word ptr bTest1 mov eax,dword ptr bTest1 ...... 通過反彙編後的內容如下: ; .data段中的變量 :00403000 12 ; 從這裏開始的1個字節是變量bTest1 :00403001 34 ; 從這裏開始的2個字節是變量wTest2 :00403002 12 :00403003 78 ; 從這裏開始的4個字節是變量dwTest3 :00403004 56 :00403005 34 :00403006 12
; .code段中的代碼 :00401000 A000304000 mov al,byte ptr [00403000] :00401005 66A100304000 mov ax,word ptr [00403000] :0040100B A100304000 mov eax,dword ptr [00403000]
運行結果: al等於12h ax等於 3412h eax 等於 78123412h
從例子可以看出,彙編中用ptr強制覆蓋變量長度的時候,實質上只用了變量的地址,編譯器並不會考慮定界的問題。
movzx指令用於數據長度的擴展 movzx指令是80386處理器提供的擴展指令,該指令總是將擴展的數據位用0代替。 movzx指令是安全的強制類型轉換方式。 能夠像C語言的強制類型轉換一樣,把一個字節擴展到一個字或一個雙字再放到ax或eax中,高位保持0而不是越界存取到其它的變量中。
movsx指令可以完成帶符號位的擴展 movsx指令是80386處理器提供的擴展指令; 當被擴展數據的最高位爲0時,效果和movzx指令相同;當最高位爲1時,則擴展部分的數據位全部用1填充。
18. 變量的尺寸和數量 sizeof僞操作符可以取得變量、數據類型或數據結構以字節爲單位的長度(尺寸)。 格式: sizeof 變量、數據類型或數據結構名
lengthof僞操作符可以取得變量、數據類型或數據結構中數據的項數(數量) 格式: length 變量、數據類型或數據結構名
對字符串使用sizeof僞操作符,取得的長度包括結束符0。
需要注意的是: sizeof僞操作符和length僞操作符取得的數值是編譯期產生的,由編譯器直接替換到指令中去。所以,在反彙編得到的代碼中沒有sizeof或lengthof,而只有它們取得的數值。
取得字符串長度的一種特殊情況: 如果szHello的定義分成兩行: szHello db ‘Hello’,0dh,0ah db ‘World’,0 sizeof szHello得到的數值是7而不是13。 這種定義方式實質爲越界使用字符串變量。 MASM中的變量定義只認一行,後一行db ‘World’,0實際上是另一個沒有名稱的數據定義。 要取得這種字符串的長度時,千萬不能用sizeof僞指令,最好是在程序中用lstrlen函數去計算。
19. 獲取變量地址 獲取全局變量地址和獲取局部變量地址的操作是不同的。 因爲全局變量定義在數據段中,而局部變量在堆棧中。全局變量的地址可以在編譯期確定,而局部變量的地址只能在運行期確定。
全局變量的地址在編譯期已經由編譯器確定了。 獲取全局變量的地址使用offset僞操作符,這個操作在編譯期而不是運行期完成。 mov 寄存器,offset 變量名
不可能用offset僞操作符來獲取局部變量地址的原因是: 局部變量是用ebp來做指針訪問的,由於ebp的值隨着程序的執行環境不同可能是不同的,所以局部變量的地址值在編譯期也是不確定的。
獲取局部變量的地址使用lea指令 lea指令是80386處理器指令集中的一條指令。 lea eax,[ebp-4]
在invoke僞指令的參數中用到某個局部變量的地址,使用MASM提供的僞操作符addr。 格式爲: addr 局部變量名和全局變量名 addr僞操作符即可用於局部變量,也可用於全局變量
使用addr僞操作符需要注意以下幾點: a) 對局部變量取地址的時候,addr僞操作符只能用在invoke的參數中,不能用在如下的mov指令中。 mov eax,addr 局部變量名 ;這是錯誤的用法 因爲在這句mov指令中,編譯器無法把addr僞操作符替換成lea指令。 b) 當在invoke中使用addr僞操作符時,在addr僞操作符的左邊不能使用eax寄存器,否則eax寄存器的值會被覆蓋掉,當然eax寄存器用在addr僞操作符的右邊的參數中是可以的。 MASM對於這種情況會報編譯期錯誤。
20. 使用子程序 Win32彙編中的子程序也採用堆棧來傳遞參數,所以可以用invoke僞指令來調用子程序,並進行語法檢查工作。
子程序的定義方式如下: 子程序名 proc [距離] [語言類型] [可視區域] [USES 寄存器列表] [,參數:類型]...[VARARG] local 局部變量列表 指令
子程序名 endp proc僞指令和endp僞指令用於定義子程序開始和結束的位置。 子程序有如下屬性: a) 距離 Win32中只有一個平坦的段,無所謂距離,所以對距離的定義往往忽略。 b) 語言類型 表示參數的使用方式和堆棧平衡的方式,如果忽略,則使用程序頭部.model定義的值。 c) 可視區域 可以設爲PRIVATE、PUBLIC和EXPORT,默認是PUBLIC。 PRIVATE表示子程序只對本模塊可見; PUBLIC表示子程序對所有模塊可見(在最後編譯鏈接完成的可執行文件中); EXPORT表示子程序是DLL的導出函數; d) USES寄存器列表 表示由編譯器在子程序指令開始前自動安排push這些寄存器的指令,並且在ret前自動安排pop指令,用於保護執行環境。 一種更方便的做法是,在子程序的開頭和結尾用pushad指令和popad指令一次保存和恢復所有寄存器。 e) 參數和類型 參數指參數的名稱,在定義參數名的時候不能跟全局變量和子程序中的局部變量重名。 對於類型,由於Win32中的參數類型只有32位(dword)一種類型,所以可以省略。在參數定義的最後還可以跟VARARG,表示在已確定的參數後還可以跟多個數量不確定的參數。
在寫源程序的時候有意識地把子程序的位置提到invoke語句的前面,省略掉proto語句,可以簡化程序和避免出錯。
參數傳遞和堆棧平衡 在調用子程序時,參數的傳遞是通過堆棧進行的。 調用者把傳遞給子程序的參數壓入堆棧,子程序從堆棧中取出相應的值來使用。
調用約定,約定了參數入棧的順序和由誰(調用者或子程序)來平衡堆棧。 由於各種語言默認的調用約定是不同的,所以在proc以及proto語句的語言屬性中確定語言類型後,編譯器才能將invoke爲指令翻譯成正確的樣子。
不同語言調用方式的差別如下表: 語言類型 最先入棧參數 平衡堆棧者 允許使用VARARG C 右 調用者 是 SysCall 右 子程序 是 StdCall 右 子程序 是 BASIC 左 子程序 否 FORTRAN 左 子程序 否 PASCAL 左 子程序 否
注:VARARG表示參數的個數可以是不確定的,如wsprinitf,StdCall的堆棧清除平時是由子程序完成的,但使用VARARG時是由調用者清除的。 從上表可以看出只有C語言是調用者平衡堆棧,其他語言類型都是被調用者來平衡堆棧。
因爲Win32約定的類型是StdCall,所以在程序中調用子程序或系統API後,不必自己來平衡堆棧,免去了很多麻煩。 存取參數和局部變量都是通過堆棧來實現的,和存取局部變量類似,參數的存取也是通過ebp做指針來完成的。 所有對局部變量使用的限制幾乎都可以適用於參數。
21. 條件測試語句 MASM的條件測試的語法和C語言相同。 同樣,對於不含比較符的單個變量或寄存器,MASM也是將所有非零值認爲是“真”,零值認爲是“假”。 與C語言的條件測試相同,MASM的條件測試僞操作符並不會改變被測試的變量或寄存器的值。 MASM的條件測試僞操作符經過編譯器編譯會翻譯成類似cmp或test之類的比較或位測試的指令。
MASM條件測試的基本表達式如下: 寄存器或變量 操作符 操作數 兩個以上的表達式可以用邏輯運算符連接: (表達式1)邏輯運算符(表達式2)邏輯運算符(表達式3)...
條件測試中的操作符和邏輯運算符如下表 操作符和邏輯運算符 操作 用途 == 等於 變量和操作數之間的比較 != 不等於 變量和操作數之間的比較 > 大於 變量和操作數之間的比較 >= 大於等於 變量和操作數之間的比較 < 小於 變量和操作數之間的比較 <= 小於等於 變量和操作數之間的比較 & 位測試 將變量和操作數做“與”操作 ! 邏輯取反 對變量取反或對表達式的結果取反 && 邏輯與 對兩個表達式的結果進行邏輯“與”操作 || 邏輯或 對兩個表達式的結果進行邏輯“或”操作
MASM的條件測試語句有如下幾點限制: a) 表達式的左邊只能是變量或寄存器,不能爲常數; b) 表達式的兩邊不能同時爲變量,但可以同時爲寄存器; 這些限制來自於80x86的指令。
以下一些系統標誌寄存器中的各種標誌位的狀態指示,本身相當於一個表達式: CARRY? 表示Carry位是否置位 OVERFLOW? 表示Overflow位是否置位 PARITY? 表示Parity位是否置位 SIGN? 表示Sign位是否置位 ZERO? 表示Zero位是否置位
22. 分支語句 MASM中的分支僞指令的語法如下: .if 條件表達式1 表達式1爲“真”時執行的指令 [.elseif 條件表達式2] 表達式2爲“真”時執行的指令 [.elseif 條件表達式3] 表達式3爲“真”時執行的指令 ... [.else] 所有表達式爲“否”時執行的指令 .endif
注意: 關鍵字if/elseif/else/endif的前面有個小數點,如果不加小數點,就變成宏彙編中的條件彙編僞操作。功能完全不一樣。 if/else/endif是宏彙編中條件彙編宏操作的僞操作指令,作用是根據條件決定在最後的可執行文件中包不包括某一段代碼。
由.if/.elseif/.else/.endif條件分支僞指令構成的分支結構只能有一個條件被滿足。 如果需要構成的分支結構對於所有的表達式爲“真”都要執行相應的代碼,可以利用多個.if/endif來完成,如下: .if 表達式1 表達式1爲“真”要執行的指令 .endif .if 表達式2 表達式2爲“真”要執行的指令 .endif
23. 循環語句 循環語句的語法如下: .while 條件測試表達式 指令 [.break[.if 退出條件]] ;如果.break僞指令後面跟一個.if測試僞指令的話,那麼當退出條件爲“真”時才執行.break僞指令。 [.continue] .endw 或 .repeat 指令 [.break[.if 退出條件]] ;如果.break僞指令後面跟一個.if測試僞指令的話,那麼當退出條件爲“真”時才執行.break僞指令。 [.continue] .until 條件測試表達式(或.untilcxz [條件測試表達式])
其中,.while/.break/.continue/.endw/.repeat/.until/.untilcxz都是僞指令。
循環體中可以使用.break僞指令強制退出循環。 循環體中可以使用.continue僞指令忽略以後的指令。
.while/.endw和.repeat/.until的區別如下: a) 前者可能一次也不會執行循環體內的指令,而後者至少會執行一次循環體內的指令。 b) 前者當判斷條件爲FALSE時退出循環,而後者當判斷條件爲TRUE時退出循環。
MASM的條件測試總是把操作數當作無符號數看待。 這就是說,在分支和循環的僞指令反彙編後可以發現,在使用>,>=,<和<=比較符時,MASM的僞指令總是將比較以後的跳轉指令使用爲jb和jnb等無符號數比較跳轉的指令。 所以,如果程序中需要構造有符號數的比較分支或循環結構,那麼必須另外用jl和jg等有符號數比較跳轉的指令來完成,使用條件測試配合分支或循環僞指令可能會得到錯誤的結果。
|
|