天天看點

函數的調用約定(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)的修飾名是[email protected]。對于“C++”函數,則有所不同。所有的Win32 API函數都遵循該約定。

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

3、fastcall調用約定

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

  對于“C”函數或者變量,修飾名以“@”為字首,然後是函數名,接着是符号“@”及參數的位元組數,如函數int func(int a, double b)的修飾名是@[email protected]。對于“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調用約定在輸出函數名前加上一個下劃線字首,後面加上一個"@"符号和其參數的位元組數,格式為[email protected],例如:function(int a, int b),其修飾名為:[email protected]

  __cdecl調用約定僅在輸出函數名前加上一個下劃線字首,格式為_functionname。

  __fastcall調用約定在輸出函數名前加上一個"@"符号,後面也是一個"@"符号和其參數的位元組數,格式為@[email protected]。

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@@[email protected]

void Test2()-----“?Test2@@YGXXZ”

__cdecl調用約定

  規則同上面的_stdcall調用約定,隻是參數表的開始辨別由上面的"@@YG"變為"@@YA"。

__fastcall調用約定

  規則同上面的_stdcall調用約定,隻是參數表的開始辨別由上面的"@@YG"變為"@@YI"。

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