X86函數調用約定(cdecl,stdcall,fastcall,thiscall…)
調用者清理堆棧的約定:
在這些約定中,調用者自己清理堆棧上的參數(arguments),這樣就允許了可變參數列表的實現,如printf()
。
cdecl
cdecl(C declaration,即C聲明)是源起C語言的一種調用約定,也是C語言的事實上的標準。在x86架構上,其內容包括:
- 函數實參在線程棧上按照從右至左的順序依次壓棧。
- 函數結果保存在寄存器EAX/AX/AL中
- 浮點型結果存放在寄存器ST0中
- 編譯後的函數名前綴以一個下劃線字符
- 調用者負責從線程棧中彈出實參(即清棧)
- 8比特或者16比特長的整形實參提升爲32比特長。
- 受到函數調用影響的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS
- 不受函數調用影響的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS
- RET指令從函數被調用者返回到調用者(實質上是讀取寄存器EBP所指的線程棧之處保存的函數返回地址並加載到IP寄存器)
GCC的函數返回值都是由調用者分配空間,並把該空間的地址作爲隱式參數傳遞給被調函數,而不使用寄存器EAX。GCC自4.5版本開始,調用函數時,堆棧上的數據必須以16B對齊(之前的版本只需要4B對齊即可)。
考慮下面的C代碼片段:
int callee(int, int, int);
int caller(void)
{
register int ret;
ret = callee(1, 2, 3);
ret += 5;
return ret;
}
在X86上,會產生如下彙編代碼:
caller:
pushl %ebp
movl %esp,%ebp
pushl $3
pushl $2
pushl $1
call callee
addl $12,%esp
addl $5,%eax
leave
ret
cdecl調用約定通常作爲x86 C編譯器的默認調用規則,許多編譯器也提供了自動切換調用約定的選項。如果需要手動指定調用規則爲cdecl,編譯器可能會支持如下語法:
return_type _cdecl funct();
syscall
與cdecl類似,參數被從右到左推入堆棧中。EAX, ECX和EDX不會保留值。參數列表的大小被放置在AL寄存器中(?)。 syscall是32位OS/2 API的標準。
optlink
參數也是從右到左被推入堆棧。從最左邊開始的三個字符變元會被放置在EAX, EDX和ECX中,最多四個浮點變元會被傳入ST(0)到ST(3)中----雖然這四個參數的空間也會在參數列表的棧上保留。函數的返回值在EAX或ST(0)中。保留的寄存器有EBP, EBX, ESI和EDI。 optlink在IBM VisualAge編譯器中被使用。
被調用者清理堆棧的約定:
如果被調用者要清理棧上的參數,需要在編譯階段知道棧上有多少字節要處理。因此,此類的調用約定並不能兼容於可變參數列表,如printf()。然而,這種調用約定也許會更有效率,因爲需要解堆棧的代碼不要在每次調用時都生成一遍。 使用此規則的函數容易在asm代碼被認出,因爲它們會在返回前解堆棧。x86 ret指令允許一個可選的16位參數說明棧字節數,用來在返回給調用者之前解堆棧。代碼類似如下:
ret 12
pascal
基於Pascal語言的調用約定,參數從左至右入棧(與cdecl相反)。被調用者負責在返回前清理堆棧。 此調用約定常見在如下16-bit 平臺的編譯器:OS/2 1.x,微軟Windows 3.x,以及Borland Delphi版本1.x。
stdcall
stdcall是由微軟創建的調用約定,是Windows API的標準調用約定。非微軟的編譯器並不總是支持該調用協議。GCC編譯器如下使用:
int __attribute__((__stdcall__ )) func()
stdcall是Pascal調用約定與cdecl調用約定的折衷:被調用者負責清理線程棧,參數從右往左入棧。其他各方面基本與cdecl相同。但是編譯後的函數名後綴以符號"@",後跟傳遞的函數參數所佔的棧空間的字節長度。寄存器EAX, ECX和EDX被指定在函數中使用,返回值放置在EAX中。stdcall對於微軟Win32 API和Open Watcom C++是標準。
微軟的編譯工具規定:
PASCAL, WINAPI, APIENTRY, FORTRAN, CALLBACK, STDCALL, __far __pascal, __fortran, __stdcall
均是此規定.
調用者或被調用者清理堆棧:
thiscall
在調用C++非靜態成員函數時使用此約定。基於所使用的編譯器和函數是否使用可變參數,有兩個主流版本的thiscall。 對於GCC編譯器,thiscall幾乎與cdecl等同:調用者清理堆棧,參數從右到左傳遞。差別在於this指針,thiscall會在最後把this指針推入棧中,即相當於在函數原型中是隱式的左數第一個參數。
在微軟Visual C++編譯器中,this指針通過ECX寄存器傳遞,其餘同cdecl約定。當函數使用可變參數,此時調用者負責清理堆棧(參考cdecl)。thiscall約定只在微軟Visual C++ 2005及其之後的版本被顯式指定。其他編譯器中,thiscall並不是一個關鍵字(反彙編器如IDA使用__thiscall)。
在VS中,默認的函數調用方式是cdecl
在X86_64中
微軟x64調用約定使用%rdi, %rsi, %rdx, %rcx,%r8, %r9 六個寄存器用於存儲函數調用時的6個參數(從左到右),使用XMM0, XMM1, XMM2, XMM3來傳遞浮點變量。其他的參數直接入棧(從右至左)。整型返回值放置在RAX中,浮點返回值在XMM0中。少於64位的參數並沒有做零擴展,此時高位充斥着垃圾。
在微軟x64調用約定中,調用者的一個職責是在調用函數之前(無論實際的傳參使用多大空間),在棧上的函數返回地址之上(靠近棧頂)分配一個32字節的“影子空間”;並且在調用結束後從棧上彈掉此空間。影子空間是用來給RCX, RDX, R8和R9提供保存值的空間,即使是對於少於四個參數的函數也要分配這32個字節。