【转】调用约定__cdecl、__stdcall和__fastcall的区别
什么是調用約定
函數的調用約定,顧名思義就是對函數調用的一個約束和規定(規范),描述了函數參數是怎么傳遞和由誰清除堆棧的。它決定以下內容:(1)函數參數的壓棧順序,(2)由調用者還是被調用者把參數彈出棧,(3)以及產生函數修飾名的方法。
歷史背景
在微機出現之前,計算機廠商幾乎都會提供一份操作系統和為不同編程語言編寫的編譯器。平臺所使用的調用約定都是由廠商的軟件實現定義的。 在Apple Ⅱ出現之前的早期微機幾乎都是“裸機”,少有一份OS或編譯器的,即是IBM PC也是如此。IBM PC兼容機的唯一的硬件標準是由Intel處理器(8086, 80386)定義的,并由IBM分發出去。硬件擴展和所有的軟件標準(BIOS調用約定)都開放有市場競爭。 一群獨立的軟件公司提供了操作系統,不同語言的編譯器以及一些應用軟件。基于不同的需求,歷史實踐和開發人員的創造力,這些公司都使用了各自不同的調用約定,往往差異很大。 在IBM兼容機市場洗牌后,微軟操作系統和編程工具(有不同的調用約定)占據了統治地位,此時位于第二層次的公司如Borland和Novell,以及開源項目如GCC,都還各自維護自己的標準。互操作性的規定最終被硬件供應商和軟件產品所采納,簡化了選擇可行標準的問題。
調用者清理
在這些約定中,調用者自己清理堆棧上的參數(arguments),這樣就允許了可變參數列表的實現,如printf()。
cdecl
cdecl(C declaration,即C聲明)是源起C語言的一種調用約定,也是C語言的事實上的標準。在x86架構上,其內容包括:
Visual C++規定函數返回值如果是POD值且長度如果不超過32比特,用寄存器EAX傳遞;長度在33-64比特范圍內,用寄存器EAX:EDX傳遞;長度超過64比特或者非POD值,則調用者為函數返回值預先分配一個空間,把該空間的地址作為隱式參數傳遞給被調函數。
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上, 會產生如下匯編代碼(AT&T 語法):
.globl callercaller:pushl %ebpmovl %esp,%ebppushl $3pushl $2pushl $1call calleeaddl $12,%espaddl $5,%eaxleaveret在函數返回后,調用的函數清理了堆棧。 在cdecl的理解上存在一些不同,尤其是在如何返回值的問題上。結果,x86程序經過不同OS平臺的不同編譯器編譯后,會有不兼容的情況,即使它們使用的都是“cdecl”規則并且不會使用系統調用。某些編譯器返回簡單的數據結構,長度大致占用兩個寄存器,放在寄存器對EAX:EDX中;大點的結構和類對象需要異常處理器的一些特殊處理(如一個定義的構造函數,析構函數或賦值),存放在內存上。為了放置在內存上,調用者需要分配一些內存,并且讓一個指針指向這塊內存,這個指針就作為隱藏的第一個參數;被調用者使用這塊內存并返回指針----返回時彈出隱藏的指針。 在Linux/GCC,浮點數值通過x87偽棧被推入堆棧。像這樣:
sub esp, 8 ; 給double值一點空間fld [ebp + x] ; 加載double值到浮點堆棧上fstp [esp] ; 推入堆棧call functadd esp, 8使用這種方法確保能以正確的格式推入堆棧。 cdecl調用約定通常作為x86 C編譯器的默認調用規則,許多編譯器也提供了自動切換調用約定的選項。如果需要手動指定調用規則為cdecl,編譯器可能會支持如下語法:
return_type _cdecl funct();其中_cdecl修飾符需要在函數原型中給出,在函數聲明中會覆蓋掉其他的設置。
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 12pascal
基于Pascal語言的調用約定,參數從左至右入棧(與cdecl相反)。被調用者負責在返回前清理堆棧。 此調用約定常見在如下16-bit 平臺的編譯器:OS/2 1.x,微軟Windows 3.x,以及Borland Delphi版本1.x。
register
Borland fastcall的別名。
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均是指此種調用約定。
fastcall
此約定還未被標準化,不同編譯器的實現也不一致。
Microsoft/GCC fastcall
Microsoft或GCC的__fastcall約定(也即__msfastcall)把第一個(從左至右)不超過32比特的參數通過寄存器ECX/CX/CL傳遞,第二個不超過32比特的參數通過寄存器EDX/DX/DL,其他參數按照自右到左順序壓棧傳遞。
Borland fastcall
從左至右,傳入三個參數至EAX, EDX和ECX中。剩下的參數推入棧,也是從左至右。 在32位編譯器Embarcadero Delphi中,這是缺省調用約定,在編譯器中以register形式為人知。 在i386上的某些版本Linux也使用了此約定。
調用者或被調用者清理
thiscall
在調用C++非靜態成員函數時使用此約定。基于所使用的編譯器和函數是否使用可變參數,有兩個主流版本的thiscall。 對于GCC編譯器,thiscall幾乎與cdecl等同:調用者清理堆棧,參數從右到左傳遞。差別在于this指針,thiscall會在最后把this指針推入棧中,即相當于在函數原型中是隱式的左數第一個參數。
在微軟Visual C++編譯器中,this指針通過ECX寄存器傳遞,其余同cdecl約定。當函數使用可變參數,此時調用者負責清理堆棧(參考cdecl)。thiscall約定只在微軟Visual C++ 2005及其之后的版本被顯式指定。其他編譯器中,thiscall并不是一個關鍵字(反匯編器如IDA使用__thiscall)。
Intel ABI
根據Intel ABI,EAX、EDX及ECX可以自由在過程或函數中使用,不需要保留。
x86-64調用約定
x86-64調用約定得益于更多的寄存器可以用來傳參。而且,不兼容的調用約定也更少了,不過還是有2種主流的規則。
微軟x64調用約定
微軟x64調用約定使用RCX, RDX, R8, R9這四個寄存器傳遞頭四個整型或指針變量(從左到右),使用XMM0, XMM1, XMM2, XMM3來傳遞浮點變量。其他的參數直接入棧(從右至左)。整型返回值放置在RAX中,浮點返回值在XMM0中。少于64位的參數并沒有做零擴展,此時高位充斥著垃圾。 在Windows x64環境下編譯代碼時,只有一種調用約定----就是上面描述的約定,也就是說,32位下的各種約定在64位下統一成一種了。 在微軟x64調用約定中,調用者的一個職責是在調用函數之前(無論實際的傳參使用多大空間),在棧上的函數返回地址之上(靠近棧頂)分配一個32字節的“影子空間”;并且在調用結束后從棧上彈掉此空間。影子空間是用來給RCX, RDX, R8和R9提供保存值的空間,即使是對于少于四個參數的函數也要分配這32個字節。
例如, 一個函數擁有5個整型參數,第一個到第四個放在寄存器中,第五個就被推到影子空間之外的棧頂。當函數被調用,此棧用來組成返回值----影子空間32位+第五個參數。
在x86-64體系下,Visual Studio 2008在XMM6和XMM7中(同樣的有XMM8到XMM15)存儲浮點數。結果對于用戶寫的匯編語言例程,必須保存XMM6和XMM7(x86不用保存這兩個寄存器),這也就是說,在x86和x86-64之間移植匯編例程時,需要注意在函數調用之前/之后,要保存/恢復XMM6和XMM7。
System V AMD64 ABI
此約定主要在Solaris,GNU/Linux,FreeBSD和其他非微軟OS上使用。頭六個整型參數放在寄存器RDI, RSI, RDX, RCX, R8和R9上;同時XMM0到XMM7用來放置浮點變元。對于系統調用,R10用來替代RCX。同微軟x64約定一樣,其他額外的參數推入棧,返回值保存在RAX中。 與微軟不同的是,不需要提供影子空間。在函數入口,返回值與棧上第七個整型參數相鄰。
以上內容來源中文維基:https://zh.wikipedia.org/zh-hans/X86%E8%B0%83%E7%94%A8%E7%BA%A6%E5%AE%9A
?
我們知道函數由以下幾部分構成:返回值類型 函數名(參數列表),如:?
【code1】
以上是大家所熟知的構成部分,其實函數的構成還有一部分,那就是調用約定。如下:?
【code2】
上面的__cdecl和__stdcall就是調用約定,其中__cdecl是C和C++默認的調用約定,所以通常我們的代碼都如 【code1】中那樣定義,編譯器默認會為我們使用__cdecl調用約定。常見的調用約定有__cdecl、__stdcall、fastcall,應用最廣泛的是__cdecl和__stdcall,下面我們會詳細進行講述。。還有一些不常見的,如 __pascal、__thiscall、__vectorcall。
聲明和定義處調用約定必須要相同
在VC++中,調用約定是函數類型的一部分,因此函數的聲明和定義處調用約定要相同,不能只在聲明處有調用約定,而定義處沒有或與聲明不同。如下:?
【code3】 錯誤的使用一:
報錯:
error C2373: ‘add’: redefinition; different type modifiers?
error C2440: ‘initializing’: cannot convert from ‘int (__stdcall *)(int,int)’ to ‘int’
補充:
int __cdecl add(int a, int b); int add(int a, int b) {return a + b; }以上就沒問題,因為默認是__cdecl。
【code4】 錯誤的使用二:
int add(int a, int b); int __stdcall add(int a, int b) {return a + b; }報錯:
error C2373: ‘add’: redefinition; different type modifiers?
error C2440: ‘initializing’: cannot convert from ‘int (__cdecl *)(int,int)’ to ‘int’
【code5】 錯誤的使用三:
int __stdcall add(int a, int b); int __cdecl add(int a, int b) {return a + b; }報錯:
error C2373: ‘add’: redefinition; different type modifiers?
error C2440: ‘initializing’: cannot convert from ‘int (__stdcall *)(int,int)’ to ‘int’
【code6】 正確的用法:
int __stdcall add(int a, int b); int __stdcall add(int a, int b) {return a + b; }函數的調用過程
要深入理解函數調用約定,你須要了解函數的調用過程和調用細節。?
假設函數A調用函數B,我們稱A函數為”調用者”,B函數為“被調用者”。如下面的代碼,ShowResult為調用者,add為被調用者。
函數調用過程可以這么描述:?
(1)先將調用者(A)的堆棧的基址(ebp)入棧,以保存之前任務的信息。?
(2)然后將調用者(A)的棧頂指針(esp)的值賦給ebp,作為新的基址(即被調用者B的棧底)。?
(3)然后在這個基址(被調用者B的棧底)上開辟(一般用sub指令)相應的空間用作被調用者B的棧空間。?
(4)函數B返回后,從當前棧幀的ebp即恢復為調用者A的棧頂(esp),使棧頂恢復函數B被調用前的位置;然后調用者A再從恢復后的棧頂可彈出之前的ebp值(可以這么做是因為這個值在函數調用前一步被壓入堆棧)。這樣,ebp和esp就都恢復了調用函數B前的位置,也就是棧恢復函數B調用前的狀態。?
這個過程在AT&T匯編中通過兩條指令完成,即:?
?
__cdecl的特點
__cdecl 是 C Declaration 的縮寫,表示 C 和 C++ 默認的函數調用約定。是C/C++和MFCX的默認調用約定。
- 按從右至左的順序壓參數入棧、。
- 由調用者把參數彈出棧。切記:對于傳送參數的內存棧是由調用者來維護的,返回值在EAX中。因此對于像printf這樣可變參數的函數必須用這種約定。
- 編譯器在編譯的時候對這種調用規則的函數生成修飾名的時候,在輸出函數名前加上一個下劃線前綴,格式為_function。如函數int add(int a, int b)的修飾名是_add。
(1).為了驗證參數是從右至左的順序壓棧的,我們可以看下面這段代碼,Debug進行單步調試,可以看到我們的調用棧會先進入GetC(),再進入GetB(),最后進入GetA()。?
(2).第二點“調用者把參數彈出棧”,這是編譯器的工作,暫時沒辦法驗證。要深入了解這部分,需要學習匯編語言相關的知識。
(3).函數的修飾名,這個可以通過對編譯出的dll使用VS的”dumpbin /exports?ProjectName.dll”命令進行查看(后面章節會進行詳細介紹),或直接打開.obj文件查找對應的方法名(如搜索add)。
從代碼和程序調試的層面考慮,參數的壓棧順序和棧的清理我們都不用太觀注,因為這是編譯器的決定的,我們改變不了。但第三點卻常常困擾我們,因為如果不弄清楚這點,在多個庫之間(如dll、lib、exe)相互調用、依賴時常常出出現莫名其妙的錯誤。這個我在后面章節會進行詳細介紹。
__stdcall的特點
__stdcall是Standard Call的縮寫,是C++的標準調用方式,當然這是微軟定義的標準,__stdcall通常用于Win32 API中(可查看WINAPI的定義)。?? microsoft的vc默認的是__cdecl方式,而windows API則是__stdcall,如果用vc開發dll給其他語言用,則應該指定__stdcall方式。堆棧由誰清除這個很重要,如果是要寫匯編函數給C調用,一定要小心堆棧的清除工作,如果是__cdecl方式的函數,則函數本身(如果不用匯編寫)則不需要關心保存參數的堆棧的清除,但是如果是__stdcall的規則,一定要在函數退出(ret)前恢復堆棧。
- 按從右至左的順序壓參數入棧。
- 由被調用者把參數彈出棧。切記:函數自己在退出時清空堆棧,返回值在EAX中。
- __stdcall調用約定在輸出函數名前加上一個下劃線前綴,后面加上一個“@”符號和其參數的字節數,格式為_function@number。如函數int sub(int a, int b)的修飾名是_sub@8。
__fastcall的特點
__fastcall調用的主要特點就是快,因為它是通過寄存器來傳送參數的。
- 實際上__fastcall用ECX和EDX傳送前兩個DWORD或更小的參數,剩下的參數仍自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的內存棧。
- __fastcall調用約定在輸出函數名前加上一個“@”符號,后面也是一個“@”符號和其參數的字節數,格式為@function@number,如double multi(double a, double b)的修飾名是@multi@16。
- __fastcall和__stdcall很象,唯一差別就是頭兩個參數通過寄存器傳送。注意通過寄存器傳送的兩個參數是從左向右的,即第1個參數進ECX,第2個進EDX,其他參數是從右向左的入棧,返回仍然通過EAX。
__thiscall
__thiscall是C++類成員函數缺省的調用約定,但它沒有顯示的聲明形式。因為在C++類中,成員函數調用還有一個this指針參數,因此必須特殊處理,thiscall調用約定的特點:
- 參數入棧:參數從右向左入棧
- this指針入棧:如果參數個數確定,this指針通過ecx傳遞給被調用者;如果參數個數不確定,this指針在所有參數壓棧后被壓入棧。
- 棧恢復:對參數個數不定的,調用者清理棧,否則函數自己清理棧。
總結
這里主要總結一下_cdecl、_stdcall、__fastcall三者之間的區別:
| 參數傳遞方式 | 右->左 | 右->左 | 左邊開始的兩個不大于4字節(DWORD)的參數分別放在ECX和EDX寄存器,其余的參數自右向左壓棧傳送 |
| 清理棧方 | 調用者清理 | 被調用函數清理 | 被調用函數清理 |
| 適用場合 | C/C++、MFC的默認方式; 可變參數的時候使用; | Win API | 要求速度快 |
| C編譯修飾約定 | _functionname | _functionname@number | @functionname@number |
以上內容參考:https://blog.csdn.net/luoweifu/article/details/52425733#commentBox
_cdecl 是C Declaration的縮寫,表示C語言默認的函數調用方法:所有參數從右到左依次入棧,這些參數由調用者清除,稱為手動清棧。被調用函數無需要求調用者傳遞多少參數,調用者傳遞過多或者過少的參數,甚至完全不同的參數都不會產生編譯階段的錯誤。
_stdcall 是Standard Call的縮寫,是C++的標準調用方式:所有參數從右到左依次入棧,如果是調用類成員的話,最后一個入棧的是this指針。這些堆棧中的參數由被調用的函數在返回后清除,使用的指令是 retn X,X表示參數占用的字節數,CPU在ret之后自動彈出X個字節的堆棧空間。稱為自動清棧。函數在編譯的時候就必須確定參數個數,并且調用者必須嚴格的控制參數的生成,不能多,不能少,否則返回后會出錯。
PASCAL 是Pascal語言的函數調用方式,也可以在C/C++中使用,參數壓棧順序與前兩者相反。返回時的清棧方式忘記了。。。
_fastcall 是編譯器指定的快速調用方式。由于大多數的函數參數個數很少,使用堆棧傳遞比較費時。因此_fastcall通常規定將前兩個(或若干個)參數由寄存器傳遞,其余參數還是通過堆棧傳遞。不同編譯器編譯的程序規定的寄存器不同。返回方式和_stdcall相當。
_thiscall 是為了解決類成員調用中this指針傳遞而規定的。_thiscall要求把this指針放在特定寄存器中,該寄存器由編譯器決定。VC使用ecx,Borland的C++編譯器使用eax。返回方式和_stdcall相當。
_fastcall 和 _thiscall涉及的寄存器由編譯器決定,因此不能用作跨編譯器的接口。所以Windows上的COM對象接口都定義為_stdcall調用方式。
C中不加說明默認函數為_cdecl方式(C中也只能用這種方式),C++也一樣,但是默認的調用方式可以在IDE環境中設置。
帶有可變參數的函數必須且只能使用_cdecl方式,例如下面的函數:
int printf(char * fmtStr, ...);
int scanf(char * fmtStr, ...);
這兩個關鍵字看起來似乎很少和我們打交道,但是看了下面的定義(來自windef.h
),你一定會覺得驚訝:
???? #define CALLBACK???? __stdcall
???? #define WINAPI?????? __stdcall
???? #define WINAPIV????? __cdecl
???? #define APIENTRY???? WINAPI
???? #define APIPRIVATE?? __stdcall
???? #define PASCAL?????? __stdcall
???? #define cdecl _cdecl
???? #ifndef CDECL
???? #define CDECL _cdecl
???? #endif
????幾乎我們寫的每一個WINDOWS API函數都是__stdcall類型的,為什么??
???? 首先,我們談一下兩者之間的區別:
?????? WINDOWS的函數調用時需要用到棧(STACK,一種先入后出的存儲結構)。當函數調用完成后,棧需要清除,這里就是問題的關鍵,如何清除??
?????? 如果我們的函數使用了_cdecl,那么棧的清除工作是由調用者,用COM的術語來講就是客戶來完成的。這樣帶來了一個棘手的問題,不同的編譯器產生棧的方式不盡相同,那么調用者能否正常的完成清除工作呢?答案是不能。
?????? 如果使用__stdcall,上面的問題就解決了,函數自己解決清除工作。所以,在跨(開發)平臺的調用中,我們都使用__stdcall(雖然有時是以WINAPI的樣子出現)。
? ? ? ?那么為什么還需要_cdecl呢?當我們遇到這樣的函數如fprintf()它的參數是可變的,不定長的,被調用者事先無法知道參數的長度,事后的清除工作也無法正常的進行,因此,這種情況我們只能使用_cdecl。
?????? 到這里我們有一個結論,如果你的程序中沒有涉及可變參數,最好使用__stdcall關鍵字
總結
以上是生活随笔為你收集整理的【转】调用约定__cdecl、__stdcall和__fastcall的区别的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 影像拼接(3种方法)
- 下一篇: 信用卡以卡办卡哪家银行好办