函數(shù)調(diào)用的三種約定,你都清楚嗎
__cdecl、__stdcall、__fastcall是C/C++里中經(jīng)常見(jiàn)到的三種函數(shù)調(diào)用方式。其中__cdecl是C/C++默認(rèn)的調(diào)用方式,__stdcall是windows API函數(shù)的調(diào)用方式,只不過(guò)我們?cè)陬^文件里查看這些API的聲明的時(shí)候是用了WINAPI的宏進(jìn)行代替了,而這個(gè)宏其實(shí)就是__stdcall了。
三種調(diào)用方式的區(qū)別相信大家應(yīng)該有些了解,這篇文章主要從實(shí)例和匯編的角度闡述這些區(qū)別的表現(xiàn)形態(tài),使其對(duì)它們的區(qū)別認(rèn)識(shí)從理論向?qū)嶋H過(guò)渡。
我們知道,函數(shù)的調(diào)用過(guò)程是通過(guò)函數(shù)棧幀的不斷變化實(shí)現(xiàn)的:
函數(shù)的調(diào)用,涉及參數(shù)傳遞,返回值傳遞,調(diào)用后返回,這都是通過(guò)棧的變化來(lái)實(shí)現(xiàn)的,對(duì)于三種調(diào)用約定而言:
__cdecl:
C/C++默認(rèn)方式,參數(shù)從右向左入棧,主調(diào)函數(shù)負(fù)責(zé)棧平衡。
__stdcall:
windows API默認(rèn)方式,參數(shù)從右向左入棧,被調(diào)函數(shù)負(fù)責(zé)棧平衡。
__fastcall:
快速調(diào)用方式。所謂快速,這種方式選擇將參數(shù)優(yōu)先從寄存器傳入(ECX和EDX),剩下的參數(shù)再?gòu)挠蚁蜃髲臈魅?。因?yàn)闂J俏挥趦?nèi)存的區(qū)域,而寄存器位于CPU內(nèi),故存取方式快于內(nèi)存,故其名曰“__fastcall”。
下面從實(shí)例來(lái)認(rèn)識(shí)一下這三種調(diào)用約定。先來(lái)看一個(gè)簡(jiǎn)單的不能再簡(jiǎn)單的程序了:
三個(gè)函數(shù)的內(nèi)容都是一樣的,不同的是使用了三種調(diào)用的方式。我們先來(lái)看看在main函數(shù)調(diào)用三個(gè)函數(shù)的時(shí)候的匯編代碼:
按照上面說(shuō)的那樣,__cdecl按照參數(shù)從右向左的方式進(jìn)入棧區(qū),注意Fun1()和Fun3()的區(qū)別,F(xiàn)un1()在call Fun1()之后執(zhí)行了add esp,8。這一操作正是我們前面所說(shuō)的進(jìn)行棧的平衡。調(diào)用函數(shù)之前連續(xù)進(jìn)行了兩次push操作將函數(shù)所需的實(shí)參5和2先后壓入了棧區(qū),調(diào)用完成后,我們需要恢復(fù)調(diào)用前的狀態(tài),則需調(diào)整棧頂指針esp的位置,這一工作由誰(shuí)來(lái)完成就決定了兩種函數(shù)調(diào)用方式__cdecl(主調(diào)函數(shù)完成)和__stdcall(被調(diào)函數(shù)完成)的區(qū)別。上圖我們看到了__cdecl中由主調(diào)函數(shù)完成了,那么__stdcall呢,在被調(diào)函數(shù)Fun3()中,轉(zhuǎn)向被調(diào)函數(shù)結(jié)尾處的代碼,我們看到了這一句:
那么Fun1()結(jié)尾處又是如何呢?
看到了吧,這個(gè)ret指令后面跟沒(méi)跟值就決定了函數(shù)返回是棧指針ESP需要增加的量。這樣,不需要主調(diào)函數(shù)再調(diào)用add指令為ESP操作平衡棧區(qū),節(jié)約了程序的開(kāi)銷,一條指令開(kāi)銷小,如果十萬(wàn)百萬(wàn)個(gè)這樣的調(diào)用,這個(gè)開(kāi)銷就明顯了。
說(shuō)完了__cdecl和__stdcall,再來(lái)看看__fastcall,如前面圖看到的調(diào)用時(shí)并未使用push指令向棧里傳參數(shù),而是使用了
mov edx, 5
mov ecx, 2
兩條指令。這樣直接將參數(shù)傳入寄存器,被調(diào)函數(shù)在執(zhí)行的時(shí)候直接從寄存器取值即可,省去了從棧里取出來(lái)給寄存器,再?gòu)募拇嫫魅〕鰜?lái)放入內(nèi)存。
不過(guò),說(shuō)個(gè)題外話,ecx寄存器經(jīng)常作為計(jì)數(shù)和C++里this指針的傳遞媒介。在這種情況下,情況又是怎樣的呢,下次分析C++操作符 new 的時(shí)候再予以討論。ecx做計(jì)數(shù)器時(shí),需要將ecx中存儲(chǔ)的實(shí)參先壓入棧區(qū),計(jì)數(shù)操作完成后再pop出來(lái)。如此一來(lái),這個(gè)fastcall倒顯得不那么fast了。
當(dāng)然,上面所說(shuō)的這些操作都是由編譯器在背后為我們完成的,開(kāi)發(fā)人員無(wú)需關(guān)心這些操作,對(duì)我們是透明的。不過(guò),知其然更知其所以然方能立于不敗之地!



































