天天看點

函數調用約定與 call 指令雜談

首先本文關于函數調用約定部分來自轉載和整理,參考文章:

C/C++函數調用約定

函數調用約定解析

一:函數調用約定;

函數調用約定是函數調用者和被調用的函數體之間關于參數傳遞、傳回值傳遞、堆棧清除、寄存器使用的一種約定;

它是需要二進制級别相容的強約定,函數調用者和函數體如果使用不同的調用約定,将可能造成程式執行錯誤,必須把它看作是函數聲明的一部分;

二:常見的函數調用約定;

VC6中的函數調用約定;

調用約定        堆棧清除    參數傳遞
    __cdecl         調用者      從右到左,通過堆棧傳遞
    __stdcall       函數體      從右到左,通過堆棧傳遞
    __fastcall      函數體      從右到左,優先使用寄存器(ECX,EDX),然後使用堆棧
    thiscall        函數體      this指針預設通過ECX傳遞,其它參數從右到左入棧
           

__cdecl是C/C++的預設調用約定; VC的調用約定中并沒有thiscall這個關鍵字,它是類成員函數預設調用約定;(後來的VC 版本是可以使用這個關鍵字的)

C/C++中的main(或wmain)函數的調用約定必須是__cdecl,不允許更改;

預設調用約定一般能夠通過編譯器設定進行更改,如果你的代碼依賴于調用約定,請明确指出需要使用的調用約定;

Delphi6中的函數調用約定;

調用約定        堆棧清除    參數傳遞
    register        函數體      從左到右,優先使用寄存器(EAX,EDX,ECX),然後使用堆棧
    pascal          函數體      從左到右,通過堆棧傳遞
    cdecl           調用者      從右到左,通過堆棧傳遞(與C/C++預設調用約定相容)
    stdcall         函數體      從右到左,通過堆棧傳遞(與VC中的__stdcall相容)
    safecall        函數體      從右到左,通過堆棧傳遞(同stdcall)
           

Delphi中的預設調用約定是register,它也是我認為最有效率的一種調用方式,而cdecl是我認為綜合效率最差的一種調用方式;

VC中的__fastcall調用約定一般比register效率稍差一些;

C++Builder6中的函數調用約定;

調用約定        堆棧清除    參數傳遞
    __fastcall      函數體      從左到右,優先使用寄存器(EAX,EDX,ECX),然後使用堆棧 (相容Delphi的register)
    (register與__fastcall等同)
    __pascal        函數體      從左到右,通過堆棧傳遞
    __cdecl         調用者      從右到左,通過堆棧傳遞(與C/C++預設調用約定相容)
    __stdcall       函數體      從右到左,通過堆棧傳遞(與VC中的__stdcall相容)
    __msfastcall    函數體      從右到左,優先使用寄存器(ECX,EDX),然後使用堆棧(相容VC的__fastcall)
           

常見的函數調用約定中,隻有cdecl約定需要調用者來清除堆棧;

C/C++中的函數支援參數數目不定的參數清單,比如printf函數;由于函數體不知道調用者在堆棧中壓入了多少參數,

是以函數體不能友善的知道應該怎樣清除堆棧,那麼最好的辦法就是把清除堆棧的責任交給調用者;

這應該就是cdecl調用約定存在的原因吧;

VB一般使用的是stdcall調用約定;(ps:有更強的保證嗎)

Windows的API中,一般使用的是stdcall約定;(ps: 有更強的保證嗎)

建議在不同語言間的調用中(如DLL)最好采用stdcall調用約定,因為它在語言間相容性支援最好;

三:函數傳回值傳遞方式

其實,傳回值的傳遞從處理上也可以想象為函數調用的一個out形參數; 函數傳回值傳遞方式也是函數調用約定的一部分;

有傳回值的函數傳回時:一般int、指針等32bit資料值(包括32bit結構)通過eax傳遞,(bool,char通過al傳遞,short通過ax傳遞),特别的__int64等64bit結構(struct) 通過edx,eax兩個寄存器來傳遞(同理:32bit整形在16bit環境中通過dx,ax傳遞); 其他大小的結構(struct)傳回時把其位址通過eax傳回;(是以傳回值類型不是1,2,4,8byte時,效率可能比較差)

參數和傳回值傳遞中,引用方式的類型可以看作與傳遞指針方式相同;

float/double(包括Delphi中的extended)都是通過浮點寄存器st(0)傳回;

========================================================================

本文環境使用 VC 2013 release win32 編譯,禁用優化,非特别說明都是這個環境。

debug release,是否優化,win32 和 x64 選項都對彙編有影響,一定要注意。

先看整個代碼:

int __stdcall FunStdcall(int a, int b, int c)
{
	return a + b + c;
}

int __cdecl FunCdecl(int a, int b, int c)
{
	return a + b + c;
}

int __fastcall FunFastcall(int a, int b, int c)
{
	return a + b + c;
}

int __vectorcall FunVectorcall(int a, int b, int c)
{
	return a + b + c;
}

int _tmain(int argc, _TCHAR* argv[])
{
	if (1)
	{
		int a = FunStdcall(1, 2, 3);
		int b = FunCdecl(1, 2, 3);
		int c = FunFastcall(1, 2, 3);
		int d = FunVectorcall(1, 2, 3);
		if (a < b)
		{
			return 1;
		}
	}
}
           

在我的VC 版本上發現了這四種約定, thiscall 不讨論。

StdCall

函數調用約定與 call 指令雜談

首先看 stdcall 調用部分,也就是 A函數調用B函數中的A。

首先參數按照從右到左的順序依次 push 壓入棧中,然後調用函數,調用結束後将 eax 的值拷貝給指針 ebp-4 處。可以猜測 ebp - 4 就是變量 a 的位址。

不妨就在這裡直接看一下,在螢幕裡面看看:

函數調用約定與 call 指令雜談

顯然,ebp-4 就是 a 的位址,那麼誰能保證函數執行完的時候他們依然相等呢?

可以說,這就是函數調用約定的一部分。隻有約定俗成,達成規範,才能保證這一點。

上圖中也可以看到 esp 的值是 0xa30(前面部分不關心,除非剛需要關心)。執行三次 push看看:

函數調用約定與 call 指令雜談

發現 esp 變成了 0xa24,比 0xa30 減小了0xC(注意是16進制)。也正是三個 int 的大小。

也就是棧的變化是從大到小,壓棧會導緻棧頂位址減小。記住現在的大小 0xa24

接下來就是 call 了。

函數調用約定與 call 指令雜談

對比之前的圖,發現 a esp 都變化了 ebp 沒變。

a 變化的原因是這裡的a 是函數裡面,也就是參數 a,而不是之前的a了。同時會發現這裡的a 的位址就是之前提到的 esp !

因為 push c push b puch a 三個指令執行後,當然 a 位址就是 esp。(這裡 abc 說的是三個參數)。

是以這裡解釋了為什麼 a會變化并且恰好就是之前的 esp。

至于為什麼 esp 會變化?參考:

http://www.360doc.com/content/15/0602/00/12129652_474998519.shtml

也就是說,如果當次函數調用是段内偏移,Call Func 等價于:

push eip

jump Func

是以導緻 esp 變化,減小了 4。現在驗證一下,也就是這時候棧頂存的應該就是 eip。

函數調用約定與 call 指令雜談

可以看到 eip = 0xeb38a0。

esp = 0x 5dfa20。跳轉到這個記憶體位址看看:

函數調用約定與 call 指令雜談

看到内容是 0xeb38fa 和 0xeb38a0 很像,為什麼不一樣???再仔細想一想,往回看一下:

函數調用約定與 call 指令雜談

可以看到 位址 0xeb38a0 其實是被調用函數的位址,而 0xeb38fa 是 call 之後代碼的位址!而打斷點的時候已經進入函數内部了,這時候的 eip (指令指針)已經變化了,随意他們不一緻。

是以這裡代碼可以猜測一下應該是:

push 3;

push 2;

push 1;

push eip;// eip = 0xeb38fa

jmp 0xeb38a0;

// 執行函數

// 将 eax 的值給 a

mov dword ptr [ebp-4],eax

進入函數後執行兩條指令:

函數調用約定與 call 指令雜談

這裡是把 ebp 壓棧儲存,然後将最新的 esp 當作 ebp。

接下來三條指令就是做加法了,等效于: eax = a + b + c。

等等,這個 abc 哪裡來的?之前 push 3 push 2 push 1 有啥用? 上面兩條有關 ebp 的指令有啥用?

看圖:

函數調用約定與 call 指令雜談

可以看到這裡有個選項,再右上角,有個顯示符号名。也就是 abc 其實是友善我們檢視的,并不是本身就是 abc。如果不勾選:變成了 ebp + 8 (0ch/10h)了。回顧整理之前的代碼:

push 3;

push 2;

push 1;

push eip;// eip = 0xeb38fa

jmp 0xeb38a0;

// 執行函數

push ebp;

mov ebp, esp;

eax = a + b + c;// 這裡是用意思代表實際操作

pop ebp;

ret 0ch;

// 将 eax 的值給 a

mov dword ptr [ebp-4],eax

可以看到目前的棧頂 esp 被 ebp儲存了。從棧頂往下的資料分别是:

原來ebp的值 原來eip的值 1 2 3。

是以 ebp + 8/0ch/10h 分别就是 1 2 3。

而 ebp 的一個作用也正是友善去使用參數。

接下來看隻有2 條指令:

pop ebp;

ret 0ch;

參考之前的入棧操作:

push 3;

push 2;

push 1;

push eip;// eip = 0xeb38fa

push ebp;

除了做了一條 ebp 的逆操作,無法還原棧!

顯然重點就在 ret 0Ch這裡了。

看幾張圖:

函數調用約定與 call 指令雜談
函數調用約定與 call 指令雜談

可以看到 pop ebp 前後導緻 esp 增加了 4。下一句指令就是 ret 0ch了,先記住目前 esp 的值:0x5dfa20

執行語句後:

函數調用約定與 call 指令雜談

可以看到 esp 變成l 0x5dfa30。剛好恢複到了 push 3 之前的狀态。

也就是這一條指令做了很多事情,參考:

http://www.360doc.com/content/15/0602/00/12129652_474998519.shtml

其實就是:

pop eip;

pop 3;

pop 2;

pop 1;

// 注意這裡的 pop 3 隻是一種邏輯意義,也就是彈出之前壓入的三個參數。

ret 0ch。稍微準确點是:

pop eip;

add esp 0ch;

看目前狀态:

函數調用約定與 call 指令雜談

執行一條語句:

函數調用約定與 call 指令雜談

可以看到 确實把 eax 的值給 a了,變成了6。本函數完成!

有了上面的一個詳細的分析,可以大概看一下調用約定的一些差異了:

函數調用約定與 call 指令雜談

圖中框出來的是調用方對四次調用的相關代碼,可以看到 push 參數 3 2 1。順序都是一樣的。

也就是最開始說的從右到左的順序。

push 完之後都是一句 call 。再款選的最後都是 mov xxx eax;其實就是對應的四條指派語句。

同時看到 stdcall cdecl 都是 push 3 2 1。三個參數直接入棧。

而 fastcall vectorcall 是把第1 2 個參數分别放到了 ecx edx,第三個參數入棧。

同時看到 cdecl 是調用者做的平衡堆棧:

add esp, 0ch。

猜想:其他都是被調用者做的:

函數調用約定與 call 指令雜談
函數調用約定與 call 指令雜談

可以看到 cdecl 函數裡面是一句 ret。而其他的是 ret 0ch ret 4(這後面的資料是根據入棧參數個數而定的)。同時發現沒有 vectorcall 對應的函數彙編!因為他和 fastcall 是一個東西,至少這裡是這樣的:

函數調用約定與 call 指令雜談

接下來對比一下,用一個 FunStdcall(double, double, double)測試一下:

函數調用約定與 call 指令雜談

可以看到執行了三次圖中方框中的類似操作,不難猜測,這其實就是三次:

push double。至少 double 不能直接 push 而已。為例驗證,我們看到最後一次是邏輯意義:

push ds:[4108F8h]。也就是 push 第一個參數 push 1.0f。

觀察監視發現:

*(double*)0x4108F8 确實就是 1.0f

同樣*(double*)esp 是 1.0f。因為剛好把 1.0f 入棧了。

本文隻粗略提供一些思路,也是備忘。後面再深入學習了。

繼續閱讀