彙編中引數的傳遞和堆疊修正(轉)

heying1229發表於2007-07-28
彙編中引數的傳遞和堆疊修正:

  在 Win32彙編中,我們經常要和 Api 打交道,另外也會常常使用自己編制的類似於 Api 的帶引數的子程式,本文要講述的是在子程式呼叫的過程中進行引數傳遞的概念和分析。一般在程式中,引數的傳遞是透過堆疊進行的,也就是說,呼叫者把要傳遞給子程式(或者被呼叫者)的引數壓入堆疊,子程式在堆疊取出相應的值再使用,比如說,如果你要呼叫 SubRouting(Var1,Var2,Var3),編譯後的最終程式碼可能是

push Var3
push Var2
push Var1
call SubRouting
add esp,12

也就是說,呼叫者首先把引數壓入堆疊,然後呼叫子程式,在完成後,由於堆疊中先前壓入的數不再有用,呼叫者或者被呼叫者必須有一方把堆疊指標修正到呼叫前的狀態。引數是最右邊的先入堆疊還是最左邊的先入堆疊、還有由呼叫者還是被呼叫者來修正堆疊都必須有個約定,不然就會產生不正確的結果,這就是我在前面使用“可能”這兩個字的原因:各種語言中呼叫子程式的約定是不同的,它們的不同點見下表:

C SysCall StdCall Basic Fortran Pascal
引數從左到右 是 是 是
引數從右到左 是 是 是
呼叫者清除堆疊 是
允許使用:VARARG 是 是 是

VARARG 表示引數的個數可以是不確定的,有一個例子就是 C 中的 printf 語句,在上表中,StdCall 的定義有個要說明的地方,就是如果 StdCall 使用 :VARARG 時,是由呼叫者清除堆疊的,而在沒有:VARARG時是由被呼叫者清除堆疊的。
在 Win32 彙編中,約定使用 StdCall 方式,所以我們要在程式開始的時候使用 .model stdcall 語句。也就是說,在 API 或子程式中,最右邊的引數先入堆疊,然後子程式在返回的時候負責校正堆疊,舉例說明,如果我們要呼叫 MessageBox 這個 API,因為它的定義是 MessageBox(hWnd,lpText,lpCaption,UType) 所以在程式中要這樣使用:

push MB_OK
push offset szCaption
push offset szText
push hWnd
call MessageBox
...

我們不必在 API 返回的時候加上一句 add sp,4*4 來修正堆疊,因為這已經由 MessageBox 這個子程式做了。在 Windows API 中,唯一一個特殊的 API 是 wsprintf,這個 API 是 C 約定的,它的定義是 wsprintf(lpOut,lpFormat,Var1,Var2...),所以在使用時就要:

push 1111
push 2222
push 3333
push offset szFormat
push offset szOut
call wsprintf
add esp,4*5

下面要講的是子程式如何存取引數,因為預設對堆疊操作的暫存器有 ESP 和 EBP,而 ESP是堆疊指標,無法暫借使用,所以一般使用 EBP 來存取堆疊,假定在一個呼叫中有兩個引數,而且在 push 第一個引數前的堆疊指標 ESP 為 X,那麼壓入兩個引數後的 ESP 為 X-8,程式開始執行 call 指令,call 指令把返回地址壓入堆疊,這時候 ESP 為 X-C,這時已經在子程式中了,我們可以開始使用 EBP 來存取引數了,但為了在返回時恢復 EBP 的值,我們還是再需要一句 push ebp 來先儲存 EBP 的值,這時 ESP 為 X-10,再執行一句 mov ebp,esp,根據右圖可以看出,實際上這時候 [ebp + 8] 就是引數1,[ebp + c]就是引數2。另外,區域性變數也是定義在堆疊中的,它們的位置一般放在 push ebp 儲存的 EBP 數值的後面,區域性變數1、2對應的地址分別是 [ebp-4]、[ebp-8],下面是一個典型的子程式,可以完成第一個引數減去第二個引數,它的定義是:

MyProc proto Var1,Var2 ;有兩個引數
local lVar1,lVar2 ;有兩個區域性變數

注意,這裡的兩個 local 變數實際上沒有被用到,只是為了演示用,具體實現的程式碼是:

MyProc proc

push ebp
mov ebp,esp

sub esp,8

mov eax,dword ptr [ebp + 8]
sub eax,dword ptr [ebp + c]

add esp,8

pop ebp
ret 8

MyProc endp

現在對這個子程式分析一下,push ebp/mov ebp,esp 是例行的儲存和設定 EBP 的程式碼,sub esp,8 在堆疊中留出兩個區域性變數的空間,mov /add 語句完成相加,add esp,8 修正兩個區域性變數使用的堆疊,ret 8 修正兩個引數使用的堆疊,相當於 ret / add esp,8 兩句程式碼的效果。可以看出,這是一個標準的 Stdcall 約定的子程式,使用時最後一個引數先入堆疊,返回時由子程式進行堆疊修正。當然,這個子程式為了演示執行過程,使用了手工儲存 ebp 並設定區域性變數的方法,實際上,386 處理器有兩條專用的指令是完成這個功能用的,那就是 Enter 和 Leave,Enter 語句的作用就是 push ebp/mov ebp,esp/sub esp,xxx,這個 xxx 就是 Enter 的,Leave 則完成 add esp,xxx/pop ebp 的功能,所以上面的程式可以改成:

MyPorc proc
enter 8,0

mov eax,dword ptr [ebp + 8]
sub eax,dword ptr [ebp + c]

leave
ret 8
MyProc endp


好了,說到這兒,引數傳遞的原理也應該將清楚了,還要最後說的是,在使用 Masm32 編 Win32 彙編程式的時候,我們並不需要記住 [ebp + xx] 等麻煩的地址,或自己計算區域性變數需要預留的堆疊空間,還有在 ret 時計算要加上的數值,Masm32 的宏指令都已經把這些做好了,如在 Masm32 中,上面的程式只要寫成為:

MyProc proc Var1,Var2
local lVar1,lVar2

mov eax,Var1
sub eax,Var2
ret

MyProc endp

編譯器會自動的在 mov eax,Var1 前面插上一句 Enter 語句,它的引數會根據 local 定義的區域性變數的多少自動指定,在 ret 前會自動加上一句 Leave,同樣,編譯器會根據引數的多少把 ret 替換成 ret xxx,把 mov eax,Var1 換成 mov eax,dword ptr [ebp + 8] 等等。

最後是使用 Masm32 的 invoke 宏指令,在前面可以看到,呼叫帶引數的子程式時,我們需要用 push 把引數壓入堆疊,如果不小心把引數個數搞錯了,就會使堆疊不平衡,從而使程式從堆疊中取出錯誤的返回地址引起不可預料的後果,所以有必要有一條語句來完成自動檢驗的任務,invoke 就是這樣的語句,實際上,它是自動 push 所有引數,檢測引數個數、型別是否正確,並使用 call 來呼叫的一個宏指令,對於上面的 push/push/call MyProc 的指令,可以用一條指令完成就是:

invoke MyProc,Var1,Var2

當然,當程式編譯好以後你去看機器碼會發現它被正確地換成了同樣的 push/push/call 指令。但是,在使用 invoke 之前,為了讓它進行正確的引數檢驗,你需要對函式進行申明,就象在 C 中一樣,申明的語句是:

MyProc proto :DWORD,:DWORD

語句中 proto 是關鍵字,表示申明,:DWORD 表示引數的型別是 double word 型別的,有幾個就表示有幾個引數,在 Win32 中引數都是 double word 型的,申明語句要寫在 invoke 之前,所以我們一般把它包括在 include 檔案中,好了,綜合一下,在 Masm32 中使用一個帶引數的子程式或者 Api ,我們只需用:

...
MyProc proto :dword,:dword
...
.data
x dd ?
y dd ?
dwResult dd ?
...
mov x,1
mov y,2
invoke MyProc x,y
mov dwResult,eax
...

就行了,如何,是不是很簡單啊?不過我可苦了,這篇文章整整花了我一個晚上 ... ##%$^&(*&^(*&(^&(* ...

[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10172717/viewspace-928824/,如需轉載,請註明出處,否則將追究法律責任。

相關文章