1. 程式人生 > >win32 彙編基礎概念整理

win32 彙編基礎概念整理

一、關於暫存器

暫存器有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) 在呼叫引數中提供一個緩衝區地址,資料直接返回到這個緩衝區中去。類似變參的概念;

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作用類似。

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等有符號數比較跳轉的指令來完成,使用條件測試配合分支或迴圈偽指令可能會得到錯誤的結果

Win32彙編 暫存器

1、資料暫存器
資料暫存器主要用來儲存運算元和運算結果等資訊,從而節省讀取運算元所需佔用匯流排和訪問儲存器的時間。32位CPU有4個32位的 通用暫存器EAX、EBX、ECX和EDX。對低16位資料的存取,不會影響高16位的資料。這些低16位暫存器分別命名為:AX、BX、CX和DX,它 和先前的CPU中的暫存器相一致。

4個16位暫存器又可分割成8個獨立的8位暫存器(AX:AH-AL、BX:BH-BL、CX:CH-CL、DX:DH-DL),每個暫存器都有 自己的名稱,可獨立存取。程式設計師可利用資料暫存器的這種”可分可合”的特性,靈活地處理字/位元組的資訊。
暫存器AX和AL通常稱為累加器 (Accumulator),用累加器進行的操作可能需要更少時間。累加器可用於乘、 除、輸入/輸出等操作,它們的使用頻率很高; 暫存器BX稱為基地址暫存器(Base Register)。它可作為儲存器指標來使用; 暫存器CX稱為計數暫存器(Count Register)。在迴圈和字串操作時,要用它來控制迴圈次數;在位操作 中,當移多位時,要用CL來指明移位的位數;
暫存器DX稱為資料寄 存器(Data Register)。在進行乘、除運算時,它可作為預設的運算元參與運算,也 可用於存放I/O的埠地址。在16位CPU中,AX、BX、CX和DX不能作為基址和變址暫存器來存放儲存單元的地址,但在32位CPU中,其32位寄 存器EAX、EBX、ECX和EDX不僅可傳送資料、暫存資料儲存算術邏輯運算結果,而且也可作為指標暫存器,所以,這些32位暫存器更具有通用性。
2、 變址暫存器
32位CPU有2個32位通用暫存器ESI和EDI。其低16位對應先前CPU中的SI和DI,對低16位資料的存取,不影響高16位 的資料。
暫存器ESI、EDI、SI和DI稱為變址暫存器(Index Register),它們主要用於存放儲存單元在段內的偏移量,用它們可實現多種儲存器運算元的定址方式,為以不同的地址形式訪問儲存單元提供方便。變址 暫存器不可分割成8位暫存器。作為通用暫存器,也可儲存算術邏輯運算的運算元和運算結果。它們可作一般的儲存器指標使用。在字串操作指令的執行過程中, 對它們有特定的要求,而且還具有特殊的功能。
3、指標暫存器
32位CPU有2個32位通用暫存器EBP和ESP。其低16位對應先前 CPU中的BP和SP,對低16位資料的存取,不影響高16位的資料。
暫存器EBP、ESP、BP和SP稱為指標暫存器(Pointer Register),主要用於存放堆疊記憶體儲單元的偏移量,用它們可實現多種儲存器運算元的定址方式,為以不同的地址形式訪問儲存單元提供方便。指標寄存 器不可分割成8位暫存器。作為通用暫存器,也可儲存算術邏輯運算的運算元和運算結果。
它們主要用於訪問堆疊內的儲存單元,並且規定:
BP 為基指標(Base Pointer)暫存器,用它可直接存取堆疊中的資料;
SP為堆疊指標(Stack Pointer)暫存器,用它只可訪問棧頂。
4、段暫存器
段暫存器是根據記憶體分段的管理模式而設定的。記憶體單元的實體地址由段暫存器的值 和一個偏移量組合而成
的,這樣可用兩個較少位數的值組合成一個可訪問較大物理空間的記憶體地址。
CPU內部的段暫存器:
CS--代 碼段暫存器(Code Segment Register),其值為程式碼段的段值;
DS--資料段暫存器(Data Segment Register),其值為資料段的段值;
ES--附加段暫存器(Extra Segment Register),其值為附加資料段的段值;
SS-- 堆疊段暫存器(Stack Segment Register),其值為堆疊段的段值;
FS--附加段暫存器(Extra Segment Register),其值為附加資料段的段值;
GS--附加段暫存器(Extra Segment Register),其值為附加資料段的段值。

在16位CPU系統中,它只有4個段暫存器,所以,程式在任何時刻至多有4個正在使用的段 可直接訪問;在32位微機系統中,它有6個段暫存器,所以,在此環境下開發的程式最多可同時訪問6個段。32位CPU有兩個不同的工作方式:實方式和保護 方式。在每種方式下,段暫存器的作用是不同的。有關規定簡單描述如下:
實方式: 前4個段暫存器CS、DS、ES和SS與先前CPU中的所對應的段暫存器的含義完全一致,記憶體單元的邏輯地址仍為”段值:偏移量”的形式。為訪問某記憶體段 內的資料,必須使用該段暫存器和儲存單元的偏移量。
保護方式: 在此方式下,情況要複雜得多,裝入段暫存器的不再是段值,而是稱為”選擇子”(Selector)的某個值。
5、指令指標暫存器
32位 CPU把指令指標擴充套件到32位,並記作EIP,EIP的低16位與先前CPU中的IP作用相同。
指令指標EIP、IP(Instruction Pointer)是存放下次將要執行的指令在程式碼段的偏移量。在具有預取指令功能的系統中,下次要執行的指令通常已被預取到指令佇列中,除非發生轉移情 況。所以,在理解它們的功能時,不考慮存在指令佇列的情況。
在實方式下,由於每個段的最大範圍為64K,所以,EIP中的高16位肯定都為0,此 時,相當於只用其低16位的IP來反映程式中指令的執行次序。
6、標誌暫存器
一、運算結果標誌位
1、進位標誌CF(Carry Flag)
進位標誌CF主要用來反映運算是否產生進位或借位。如果運算結果的最高位產生了一個進位或借位,那麼,其值為1,否則其值為0。使用該 標誌位的情況有:多字(位元組)數的加減運算,無符號數的大小比較運算,移位操作,字(位元組)之間移位,專門改變CF值的指令等。
2、奇偶標誌 PF(Parity Flag)
奇偶標誌PF用於反映運算結果中”1″的個數的奇偶性。如果”1″的個數為偶數,則PF的值為1,否則其值為0。
利 用PF可進行奇偶校驗檢查,或產生奇偶校驗位。在資料傳送過程中,為了提供傳送的可靠性,如果採用奇偶校驗的方法,就可使用該標誌位。
3、輔助進 位標誌AF(Auxiliary Carry Flag)
在發生下列情況時,輔助進位標誌AF的值被置為1,否則其值為0:
(1)、在字 操作時,發生低位元組向高位元組進位或借位時;
(2)、在位元組操作時,發生低4位向高4位進位或借位時。
對以上6個運算結果標誌位,在一般編 程情況下,標誌位CF、ZF、SF和OF的使用頻率較高,而標誌位PF和AF的使用頻率較低。
4、零標誌ZF(Zero Flag)
零標 志ZF用來反映運算結果是否為0。如果運算結果為0,則其值為1,否則其值為0。在判斷運算結果是否為0時,可使用此標誌位。
5、符號標誌 SF(Sign Flag)
符號標誌SF用來反映運算結果的符號位,它與運算結果的最高位相同。在微機系統中,有符號數採用補碼錶示法,所 以,SF也就反映運算結果的正負號。運算結果為正數時,SF的值為0,否則其值為1。
6、溢位標誌OF(Overflow Flag)
溢 出標誌OF用於反映有符號數加減運算所得結果是否溢位。如果運算結果超過當前運算位數所能表示的範圍,則稱為溢位,OF的值被置為1,否則,OF的值被清 為0。”溢位”和”進位”是兩個不同含義的概念,不要混淆。如果不太清楚的話,請查閱《計算機組成原理》課程中的有關章節。
二、狀態控制標誌位
狀 態控制標誌位是用來控制CPU操作的,它們要通過專門的指令才能使之發生改變。
1、追蹤標誌TF(Trap Flag)
當追蹤標誌TF被 置為1時,CPU進入單步執行方式,即每執行一條指令,產生一個單步中斷請求。這種方式主要用於程式的除錯。指令系統中沒有專門的指令來改變標誌位TF的 值,但程式設計師可用其它辦法來改變其值。
2、中斷允許標誌IF(Interrupt-enable Flag)
中斷允許標誌IF是用來決定 CPU是否響應CPU外部的可遮蔽中斷髮出的中斷請求。但不管該標誌為何值,CPU都必須響應CPU外部的不可遮蔽中斷所發出的中斷請求,以及CPU內部 產生的中斷請求。具體規定如下:
(1)、當IF=1時,CPU可以響應CPU外部的可遮蔽中斷髮出的中斷請求;
(2)、當IF=0 時,CPU不響應CPU外部的可遮蔽中斷髮出的中斷