函數的調用約定(cdecl,stdcall,fastcall,...)

調用約定

  調用約定闡釋了程序中函數的調用方式。當一個調用約定形成,我們需要討論的是被調用的函數是如何獲取數據(例如參數),以及這些數據在堆棧中是如何存放的。對於逆向工程來說,深入瞭解調用約定是很有必要的。因爲在逆向工程中會經常遇見不同的調用約定。而且,確定一個函數的調用約定在逆向工程中對於你理解函數也有比較好的幫助。

  在我們討論不同的調用約定之前,先了解一些基礎的函數調用指令:CALL和RET。CALL指令先將當前指令指針值(它實際上存儲的是CALL語句之後的那條指令地址)壓入堆棧,然後通過使用一條無條件轉移指令(jmp指令)轉移到新的代碼段地址。(其實這裏也就是進入了調用函數的內部,從第一條指令開始執行)。

  RET指令是和CALL指令相對應的。作爲最後一條指令,基本上出現在每個函數的結尾。RET將地址(先前被CALL指令存儲的)彈出堆棧,並存放到EIP(指令指針寄存器)中。然後從該地址開始繼續執行。

1、cdecl調用約定

  cdecl 調用約定是C和C++標準的調用約定。它的獨特之處就是允許函數接收動態數量的參數。因爲它是按從右至左的順序壓參數入棧,由於是調用者負責把參數彈出棧,所以可以給函數定義個數不定的參數,如printf函數。

  逆向工程中鑑別是否爲cdecl調用相當簡單。任何有着一個或多個參數,同時以簡單的,沒有其他操作數的RET結尾的函數都很可能是採用cdecl調用約定的函數。

  對於“C”函數或者變量,修飾名是在函數名前加下劃線。對於“C++”函數,有所不同。如函數void test(void)的修飾名是_test;對於不屬於一個類的“C++”全局函數,修飾名是?test@@ZAXXZ。

2、stdcall調用約定

  stdcall調用約定按從右至左的順序壓參數入棧,由被調用者把參數彈出棧。

 對於“C”函數或者變量,修飾名以下劃線爲前綴,然後是函數名,然後是符號“@”及參數的字節數,如函數int func(int a, double b)的修飾名是_func@12。對於“C++”函數,則有所不同。所有的Win32 API函數都遵循該約定。

  stdcall調用約定的函數通常是用RET指令來清理堆棧。RET指令能夠選擇性的接受一個操作數,這個操作數也就是在被調函數返回其主函數之前所使用的堆棧字節數。這也就意味着在stdcall調用約定的函數中傳遞給RET的操作數通常也就暴露出了函數參數的個數。通過字節數/4也就得到了函數所接受到的參數個數。無論是在逆向工程中判定是否函數是使用的stdcall調用約定,還是確定一個函數有多少參數,這都是一個非常好的提示。

3、fastcall調用約定

  頭兩個DWORD類型或者佔更少字節的參數被放入ECX和EDX寄存器,其他剩下的參數按從右到左的順序壓入棧。由被調用者把參數彈出棧。

  對於“C”函數或者變量,修飾名以“@”爲前綴,然後是函數名,接着是符號“@”及參數的字節數,如函數int func(int a, double b)的修飾名是@func@12。對於“C++”函數,有所不同。

  fastcall 起初是微軟的一種特殊的調用約定方式。但是現在已經被絕大多數的編譯器所支持。未來的編譯器可能使用不同的寄存器來存放參數。

4、thiscall調用約定

  僅僅應用於“C++”成員函數。this指針存放於ECX寄存器,參數從右到左壓棧。thiscall不是關鍵詞,因此不能被程序員指定。

  當一個C++類方法的函數所接受的參數個數固定的時候,Microsoft和Inter編譯器會用這種調用約定。

  一種快速識別這種約定調用的技巧是:函數調用之前,使用這種調用約定的函數指令流會將一個相關的有效指針保存到ecx中,同時將參數壓入堆棧,但是沒有使用edx寄存器。這是因爲任何一個C++類方法都必須接受一個類指針(我們稱作this指針)並且會經常用到該指針,編譯器使用這種高效的技巧來傳遞和存儲這個特殊的元素。

  對於參數個數不確定的類方法,編譯器通常會使用cdecl調用約定,並把this指針作爲第一個參數首先壓入堆棧中。

5、nakecall調用約定

  採用cdecl,stdcall,fastcall或thiscall的調用約定時,如果必要的話,進入函數時編譯器會產生代碼來保存ESI,EDI,EBX,EBP寄存器,退出函數時則產生代碼恢復這些寄存器的內容。

  naked call不產生這樣的代碼。naked call不是類型修飾符,故必須和_declspec共同使用,如下:

__declspec( naked ) int func( formal_parameters )
{
// Function body
}

6、過時的調用約定

原來的一些調用約定可以不再使用。它們被定義成調用約定_stdcall或者_cdecl。例如:

#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall

名字修飾約定

  "C"或者"C++"函數在內部(編譯和鏈接)通過修飾名識別(Decoration name)

1、C編譯時函數名修飾約定規則

  __stdcall調用約定在輸出函數名前加上一個下劃線前綴,後面加上一個"@"符號和其參數的字節數,格式爲_functionname@number,例如:function(int a, int b),其修飾名爲:_function@8
  __cdecl調用約定僅在輸出函數名前加上一個下劃線前綴,格式爲_functionname。
  __fastcall調用約定在輸出函數名前加上一個"@"符號,後面也是一個"@"符號和其參數的字節數,格式爲@functionname@number。

2、C++編譯時函數名修飾約定規則

__stdcall調用約定

1)、以"?"標識函數名的開始,後跟函數名;
2)、函數名後面以"@@YG"標識參數表的開始,後跟參數表;
3)、參數表以代號表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
PA--表示指針,後面的代號表明指針類型,如果相同類型的指針連續出現,以"0"代替,一個"0"代表一次重複;
4)、參數表的第一項爲該函數的返回值類型,其後依次爲參數的數據類型,指針標識在其所指數據類型前;
5)、參數表後以"@Z"標識整個名字的結束,如果該函數無參數,則以"Z"標識結束。
其格式爲"?functionname@@YG*****@Z"或"?functionname@@YG*XZ",例如
int Test1(char *var1,unsigned long)----?Test1@@YGHPADK@Z
void Test2()-----“?Test2@@YGXXZ

__cdecl調用約定

  規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的"@@YG"變爲"@@YA"。

__fastcall調用約定

  規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的"@@YG"變爲"@@YI"。

  VC++對函數的省缺聲明是"__cedcl",將只能被C/C++調用.

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章