程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 匯編語言 >> 匯編中參數的傳遞和堆棧修正

匯編中參數的傳遞和堆棧修正

編輯:匯編語言

在 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
...

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved