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