函式呼叫的三種方式 __cdecl、__stdcall、__fastcall

吾竹清風發表於2020-10-10

__cdecl、__stdcall、__fastcall是C/C++裡中經常見到的三種函式呼叫方式。

__cdecl是C/C++預設的呼叫方式

__stdcall是windows API函式的呼叫方式,只不過我們在標頭檔案裡檢視這些API的宣告的時候是用了WINAPI的巨集進行代替了,而這個巨集其實就是__stdcall了。

函式的呼叫過程是通過函式棧幀的不斷變化實現的:

 

 

函式的呼叫,涉及引數傳遞,返回值傳遞,呼叫後返回,這都是通過棧的變化來實現的,對於三種呼叫約定而言:

__cdecl:

C/C++預設方式,引數從右向左入棧,主調函式負責棧平衡。

__stdcall:

windows API預設方式,引數從右向左入棧,被調函式負責棧平衡。

__fastcall:

快速呼叫方式。所謂快速,這種方式選擇將引數優先從暫存器傳入(ECX和EDX),剩下的引數再從右向左從棧傳入。因為棧是位於記憶體的區域,而暫存器位於CPU內,故存取方式快於記憶體,故其名曰“__fastcall”。

 

#include <stdio.h>

class Point
{
public:
	Point(int x, int y)
	{
		nx = x;
		ny = y;
		nTest = nx + ny;
	}
	~Point(){}
	int nx;
	int ny;
	int nTest;
};

void __cdecl Fun1(int x, int y)
{
	Point pt(0, 0);
	pt.nx = x;
	pt.ny = y;
}

void __fastcall Fun2(int x, int y)
{
	Point pt(0, 0);
	pt.nx = x;
	pt.ny = y;
}

void __stdcall Fun3(int x, int y)
{
	Point pt(0, 0);
	pt.nx = x;
	pt.ny = y;
}

int main()
{
	Fun1(2, 5);//__cdecl
	Fun2(2, 5);//__fastcall
	Fun3(2, 5);//__stdcall
	return 0;
}

 

 先打斷點,進行反彙編

 

__cdecl按照引數從右向左的方式進入棧區,注意Fun1()和Fun3()的區別,Fun1()在call Fun1()之後執行了add esp,8。這一操作正是我們前面所說的進行棧的平衡。

呼叫函式之前連續進行了兩次push操作將函式所需的實參5和2先後壓入了棧區,呼叫完成後,我們需要恢復呼叫前的狀態,則需調整棧頂指標esp的位置,這一工作由誰來完成就決定了兩種函式呼叫方式__cdecl(主調函式完成)和__stdcall(被調函式完成)的區別。上圖我們看到了__cdecl中由主調函式完成了,那麼__stdcall呢,在被調函式Fun3()中,轉向被調函式結尾處的程式碼,我們看到了這一句:

 fun1()結尾處

 這個ret指令後面跟沒跟值就決定了函式返回是棧指標ESP需要增加的量。這樣,不需要主調函式再呼叫add指令為ESP操作平衡棧區,節約了程式的開銷,一條指令開銷小,如果十萬百萬個這樣的呼叫,這個開銷就明顯了。

__fastcall,如前面圖看到的呼叫時並未使用push指令向棧裡傳引數,而是使用了

兩條指令。這樣直接將引數傳入暫存器,被調函式在執行的時候直接從暫存器取值即可,省去了從棧裡取出來給暫存器,再從暫存器取出來放入記憶體。

 

ecx暫存器經常作為計數和C++裡this指標的傳遞媒介。ecx做計數器時,需要將ecx中儲存的實參先壓入棧區,計數操作完成後再pop出來。如此一來,這個fastcall倒顯得不那麼fast了。

 

相關文章