­

函數調用的三種約定,你都清楚嗎

  • 2020 年 3 月 11 日
  • 筆記

__cdecl、__stdcall、__fastcall是C/C++里中經常見到的三種函數調用方式。其中__cdecl是C/C++默認的調用方式,__stdcall是windows API函數的調用方式,只不過我們在頭文件里查看這些API的聲明的時候是用了WINAPI的宏進行代替了,而這個宏其實就是__stdcall了。

三種調用方式的區別相信大家應該有些了解,這篇文章主要從實例和彙編的角度闡述這些區別的表現形態,使其對它們的區別認識從理論向實際過渡。

我們知道,函數的調用過程是通過函數棧幀的不斷變化實現的:

函數的調用,涉及參數傳遞,返回值傳遞,調用後返回,這都是通過棧的變化來實現的,對於三種調用約定而言:

__cdecl:

C/C++默認方式,參數從右向左入棧,主調函數負責棧平衡。

__stdcall

windows API默認方式,參數從右向左入棧,被調函數負責棧平衡。

__fastcall:

快速調用方式。所謂快速,這種方式選擇將參數優先從寄存器傳入(ECX和EDX),剩下的參數再從右向左從棧傳入。因為棧是位於內存的區域,而寄存器位於CPU內,故存取方式快於內存,故其名曰「__fastcall」。

下面從實例來認識一下這三種調用約定。先來看一個簡單的不能再簡單的程序了:

三個函數的內容都是一樣的,不同的是使用了三種調用的方式。我們先來看看在main函數調用三個函數的時候的彙編代碼:

按照上面說的那樣,__cdecl按照參數從右向左的方式進入棧區,注意Fun1()和Fun3()的區別,Fun1()在call Fun1()之後執行了add esp,8。這一操作正是我們前面所說的進行棧的平衡。調用函數之前連續進行了兩次push操作將函數所需的實參5和2先後壓入了棧區,調用完成後,我們需要恢復調用前的狀態,則需調整棧頂指針esp的位置,這一工作由誰來完成就決定了兩種函數調用方式__cdecl(主調函數完成)和__stdcall(被調函數完成)的區別。上圖我們看到了__cdecl中由主調函數完成了,那麼__stdcall呢,在被調函數Fun3()中,轉向被調函數結尾處的代碼,我們看到了這一句:

那麼Fun1()結尾處又是如何呢?

看到了吧,這個ret指令後面跟沒跟值就決定了函數返回是棧指針ESP需要增加的量。這樣,不需要主調函數再調用add指令為ESP操作平衡棧區,節約了程序的開銷,一條指令開銷小,如果十萬百萬個這樣的調用,這個開銷就明顯了。

說完了__cdecl和__stdcall,再來看看__fastcall,如前面圖看到的調用時並未使用push指令向棧里傳參數,而是使用了

mov edx, 5

mov ecx, 2

兩條指令。這樣直接將參數傳入寄存器,被調函數在執行的時候直接從寄存器取值即可,省去了從棧里取出來給寄存器,再從寄存器取出來放入內存。

不過,說個題外話,ecx寄存器經常作為計數和C++里this指針的傳遞媒介。在這種情況下,情況又是怎樣的呢,下次分析C++操作符 new 的時候再予以討論。ecx做計數器時,需要將ecx中存儲的實參先壓入棧區,計數操作完成後再pop出來。如此一來,這個fastcall倒顯得不那麼fast了。

當然,上面所說的這些操作都是由編譯器在背後為我們完成的,開發人員無需關心這些操作,對我們是透明的。不過,知其然更知其所以然方能立於不敗之地!