天天看點

彙編中參數的傳遞和堆棧修正【轉載】

 在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 語句。也就是說,<b>在 api 或子程式中,最右邊的參數先入堆棧,然後子程式在傳回的時候負責校正堆棧</b>,舉例說明,如果我們要調用 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

   leave

了,說到這兒,參數傳遞的原理也應該将清楚了,還要最後說的是,在使用 masm32 編 win32 彙程式設計式的時候,我們并不需要記住 [ebp +

xx] 等麻煩的位址,或自己計算局部變量需要預留的堆棧空間,還有在 ret 時計算要加上的數值,masm32 的宏指令都已經把這些做好了,如在

masm32 中,上面的程式隻要寫成為:

myproc proc var1,var2

   local lvar1,lvar2

   mov eax,var1

   sub eax,var2

   ret

譯器會自動的在 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

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

繼續閱讀