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