|
|
| 在 Win32匯編中,我們經(jīng)常要和 Api 打交道,另外也會(huì )常常使用自己編制的類(lèi)似于 Api 的帶參數的子程序,本文要講述的是在子程序調用的過(guò)程中進(jìn)行參數傳遞的概念和分析。一般在程序中,參數的傳遞是通過(guò)堆棧進(jìn)行的,也就是說(shuō),調用者把要傳遞給子程序(或者被調用者)的參數壓入堆棧,子程序在堆棧取出相應的值再使用,比如說(shuō),如果你要調用 SubRouting(Var1,Var2,Var3),編譯后的最終代碼可能是
push Var3 push Var2 push Var1 call SubRouting add esp,12
也就是說(shuō),調用者首先把參數壓入堆棧,然后調用子程序,在完成后,由于堆棧中先前壓入的數不再有用,調用者或者被調用者必須有一方把堆棧指針修正到調用前的狀態(tài)。參數是最右邊的先入堆棧還是最左邊的先入堆棧、還有由調用者還是被調用者來(lái)修正堆棧都必須有個(gè)約定,不然就會(huì )產(chǎn)生不正確的結果,這就是我在前面使用“可能”這兩個(gè)字的原因:各種語(yǔ)言中調用子程序的約定是不同的,它們的不同點(diǎn)見(jiàn)下表: | | C | SysCall | StdCall | Basic | Fortran | Pascal | | 參數從左到右 | | | | 是 | 是 | 是 | | 參數從右到左 | 是 | 是 | 是 | | | | | 調用者清除堆棧 | 是 | | | | | | | 允許使用:VARARG | 是 | 是 | 是 | | | | VARARG 表示參數的個(gè)數可以是不確定的,有一個(gè)例子就是 C 中的 printf 語(yǔ)句,在上表中,StdCall 的定義有個(gè)要說(shuō)明的地方,就是如果 StdCall 使用 :VARARG 時(shí),是由調用者清除堆棧的,而在沒(méi)有:VARARG時(shí)是由被調用者清除堆棧的。在 Win32 匯編中,約定使用 StdCall 方式,所以我們要在程序開(kāi)始的時(shí)候使用 .model stdcall 語(yǔ)句。也就是說(shuō),在 API 或子程序中,最右邊的參數先入堆棧,然后子程序在返回的時(shí)候負責校正堆棧,舉例說(shuō)明,如果我們要調用 MessageBox 這個(gè) API,因為它的定義是 MessageBox(hWnd,lpText,lpCaption,UType) 所以在程序中要這樣使用:
push MB_OK push offset szCaption push offset szText push hWnd call MessageBox ...
我們不必在 API 返回的時(shí)候加上一句 add sp,4*4 來(lái)修正堆棧,因為這已經(jīng)由 MessageBox 這個(gè)子程序做了。在 Windows API 中,唯一一個(gè)特殊的 API 是 wsprintf,這個(gè) API 是 C 約定的,它的定義是 wsprintf(lpOut,lpFormat,Var1,Var2...),所以在使用時(shí)就要:
push 1111 push 2222 push 3333 push offset szFormat push offset szOut call wsprintf add esp,4*5 下面要講的是子程序如何存取參數,因為缺省對堆棧操作的寄存器有 ESP 和 EBP,而 ESP是堆棧指針,無(wú)法暫借使用,所以一般使用 EBP 來(lái)存取堆棧,假定在一個(gè)調用中有兩個(gè)參數,而且在 push 第一個(gè)參數前的堆棧指針 ESP 為 X,那么壓入兩個(gè)參數后的 ESP 為 X-8,程序開(kāi)始執行 call 指令,call 指令把返回地址壓入堆棧,這時(shí)候 ESP 為 X-C,這時(shí)已經(jīng)在子程序中了,我們可以開(kāi)始使用 EBP 來(lái)存取參數了,但為了在返回時(shí)恢復 EBP 的值,我們還是再需要一句 push ebp 來(lái)先保存 EBP 的值,這時(shí) ESP 為 X-10,再執行一句 mov ebp,esp,根據上圖可以看出,實(shí)際上這時(shí)候 [ebp + 8] 就是參數1,[ebp + c]就是參數2。另外,局部變量也是定義在堆棧中的,它們的位置一般放在 push ebp 保存的 EBP 數值的后面,局部變量1、2對應的地址分別是 [ebp-4]、[ebp-8],下面是一個(gè)典型的子程序,可以完成第一個(gè)參數減去第二個(gè)參數,它的定義是:
MyProc proto Var1,Var2 ;有兩個(gè)參數 local lVar1,lVar2 ;有兩個(gè)局部變量
注意,這里的兩個(gè) local 變量實(shí)際上沒(méi)有被用到,只是為了演示用,具體實(shí)現的代碼是:
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
現在對這個(gè)子程序分析一下,push ebp/mov ebp,esp 是例行的保存和設置 EBP 的代碼,sub esp,8 在堆棧中留出兩個(gè)局部變量的空間,mov /add 語(yǔ)句完成相加,add esp,8 修正兩個(gè)局部變量使用的堆棧,ret 8 修正兩個(gè)參數使用的堆棧,相當于 ret / add esp,8 兩句代碼的效果??梢钥闯?,這是一個(gè)標準的 Stdcall 約定的子程序,使用時(shí)最后一個(gè)參數先入堆棧,返回時(shí)由子程序進(jìn)行堆棧修正。當然,這個(gè)子程序為了演示執行過(guò)程,使用了手工保存 ebp 并設置局部變量的方法,實(shí)際上,386 處理器有兩條專(zhuān)用的指令是完成這個(gè)功能用的,那就是 Enter 和 Leave,Enter 語(yǔ)句的作用就是 push ebp/mov ebp,esp/sub esp,xxx,這個(gè) 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
好了,說(shuō)到這兒,參數傳遞的原理也應該將清楚了,還要最后說(shuō)的是,在使用 Masm32 編 Win32 匯編程序的時(shí)候,我們并不需要記住 [ebp + xx] 等麻煩的地址,或自己計算局部變量需要預留的堆??臻g,還有在 ret 時(shí)計算要加上的數值,Masm32 的宏指令都已經(jīng)把這些做好了,如在 Masm32 中,上面的程序只要寫(xiě)成為:
MyProc proc Var1,Var2 local lVar1,lVar2
mov eax,Var1 sub eax,Var2 ret
MyProc endp
編譯器會(huì )自動(dòng)的在 mov eax,Var1 前面插上一句 Enter 語(yǔ)句,它的參數會(huì )根據 local 定義的局部變量的多少自動(dòng)指定,在 ret 前會(huì )自動(dòng)加上一句 Leave,同樣,編譯器會(huì )根據參數的多少把 ret 替換成 ret xxx,把 mov eax,Var1 換成 mov eax,dword ptr [ebp + 8] 等等。 最后是使用 Masm32 的 invoke 宏指令,在前面可以看到,調用帶參數的子程序時(shí),我們需要用 push 把參數壓入堆棧,如果不小心把參數個(gè)數搞錯了,就會(huì )使堆棧不平衡,從而使程序從堆棧中取出錯誤的返回地址引起不可預料的后果,所以有必要有一條語(yǔ)句來(lái)完成自動(dòng)檢驗的任務(wù),invoke 就是這樣的語(yǔ)句,實(shí)際上,它是自動(dòng) push 所有參數,檢測參數個(gè)數、類(lèi)型是否正確,并使用 call 來(lái)調用的一個(gè)宏指令,對于上面的 push/push/call MyProc 的指令,可以用一條指令完成就是:
invoke MyProc,Var1,Var2
當然,當程序編譯好以后你去看機器碼會(huì )發(fā)現它被正確地換成了同樣的 push/push/call 指令。但是,在使用 invoke 之前,為了讓它進(jìn)行正確的參數檢驗,你需要對函數進(jìn)行申明,就象在 C 中一樣,申明的語(yǔ)句是:
MyProc proto :DWORD,:DWORD
語(yǔ)句中 proto 是關(guān)鍵字,表示申明,:DWORD 表示參數的類(lèi)型是 double word 類(lèi)型的,有幾個(gè)就表示有幾個(gè)參數,在 Win32 中參數都是 double word 型的,申明語(yǔ)句要寫(xiě)在 invoke 之前,所以我們一般把它包括在 include 文件中,好了,綜合一下,在 Masm32 中使用一個(gè)帶參數的子程序或者 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 ...
|
| | |