在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
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiZpdmLrNWY0N3LcJzMul2dvwVbhJ3ZvJHcvwlbj5SbvNmLtNXYyMjbpdnL3d3dvw1LcpDc0RHaiojIsJye.gif)
下
面要講的是子程式如何存取參數,因為預設對堆棧操作的寄存器有 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
就行了,如何,是不是很簡單啊?不過我可苦了,這篇文章整整花了我一個晚上 ... ##%$^&(*&^(*&(^&(*