在VC SDK的WinDef.h中,宏WINAPI被定義為__stdcall,這是C語言中一種調用約定,常用的還有__cdecl和__fastcall。這些調用約定會對我們的代碼產生什么樣的影響?讓我們逐個分析。
首先,在x86平台上,用VC編譯這樣一段代碼:
1 int __cdecl TestC(int n0, int n1, int n2, int n3, int n4, int n5) 2 { 3 int n = n0 + n1 + n2 + n3 + n4 + n5; 4 return n; 5 } 6 7 int __stdcall TestStd(int n0, int n1, int n2, int n3, int n4, int n5) 8 { 9 int n = n0 + n1 + n2 + n3 + n4 + n5; 10 return n; 11 } 12 13 int __fastcall TestFast(int n0, int n1, int n2, int n3, int n4, int n5) 14 { 15 int n = n0 + n1 + n2 + n3 + n4 + n5; 16 return n; 17 } 18 19 int _tmain(int argc, _TCHAR* argv[]) 20 { 21 TestC(0, 1, 2, 3, 4, 5); 22 TestStd(0, 1, 2, 3, 4, 5); 23 TestFast(0, 1, 2, 3, 4, 5); 24 return 0; 25 }
然后在main函數的開始出設置斷點、開始調試。
首先,我們會看到編譯器為__cdecl產生的匯編代碼:
;main函數中的調用代碼
TestC(0, 1, 2, 3, 4, 5); 013F243E push 5 013F2440 push 4 013F2442 push 3 013F2444 push 2 013F2446 push 1 013F2448 push 0 013F244A call TestC (13F11D1h) 013F244F add esp,18h
;TestC函數的實現,省略無關代碼
int __cdecl TestC(int n0, int n1, int n2, int n3, int n4, int n5) { 013F1400 push ebp 013F1401 mov ebp,esp 013F1403 ...
...
013F1439 mov esp,ebp 013F143B pop ebp 013F143C ret
由以上代碼可以發現,main函數中調用TestC函數時,將6個參數由右至左依次壓棧,也就是全部參數都通過棧傳遞。在TestC函數ret時,並沒有清理棧上的參數,而是在main函數中通過調整esp來清理的。正因為如此,使得__cdecl可以支持參數個數不定的函數調用,如 :
void f(char* fmt, ...);
再來看一下__stdcall的匯編代碼:
;main函數中的調用代碼
TestStd(0, 1, 2, 3, 4, 5); 00FB2452 push 5 00FB2454 push 4 00FB2456 push 3 00FB2458 push 2 00FB245A push 1 00FB245C push 0 00FB245E call TestStd (0FB11E0h) ;TestStd函數的實現,省略無關代碼 int __stdcall TestStd(int n0, int n1, int n2, int n3, int n4, int n5) { 00FB1840 push ebp 00FB1841 mov ebp,esp 00FB1843 ...
...
00FB1879 mov esp,ebp 00FB187B pop ebp 00FB187C ret 18h
以上代碼中,main函數中調用TestStd函數時,將6個參數由右至左依次壓棧,這一點與__cdecl相同。不同的是在TestStd函數ret時,清理掉了棧上的6個參數(18h = 4 * 6)。
最后看一下__fastcall產生的代碼:
;main函數中的調用代碼
TestFast(0, 1, 2, 3, 4, 5); 00FB2463 push 5 00FB2465 push 4 00FB2467 push 3 00FB2469 push 2 00FB246B mov edx,1 00FB2470 xor ecx,ecx 00FB2472 call TestFast (00FB11E5)
;TestFast函數的實現,省略無關代碼 int __fastcall TestFast(int n0, int n1, int n2, int n3, int n4, int n5) { 00FB1880 push ebp 00FB1881 mov ebp,esp 00FB1883 ...
...
00FB18C1 mov esp,ebp 00FB18C3 pop ebp 00FB18C4 ret 10h
與以上兩個調用約定顯著不同的是,__fastcall使用ecx和edx來傳遞前兩個參數(如果有的話),剩余的參數依然按照從右到左的順序壓棧傳遞。並且在函數ret時,類似於__stdcall,會清理通過棧傳遞的參數(此處為4個,10h = 4 * 4)。
接下來看一下x64平台上產生的代碼:
;main函數中的調用代碼
000000013F3111A0 ...
... 000000013F3111AA sub rsp,30h 000000013F3111AE ...
... TestC(0, 1, 2, 3, 4, 5); 000000013F3111C1 mov dword ptr [rsp+28h],5 000000013F3111C9 mov dword ptr [rsp+20h],4 000000013F3111D1 mov r9d,3 000000013F3111D7 mov r8d,2 000000013F3111DD mov edx,1 000000013F3111E2 xor ecx,ecx 000000013F3111E4 call TestC (13F31100Ah) TestStd(0, 1, 2, 3, 4, 5); 000000013F3111E9 mov dword ptr [rsp+28h],5 000000013F3111F1 mov dword ptr [rsp+20h],4 000000013F3111F9 mov r9d,3 000000013F3111FF mov r8d,2 000000013F311205 mov edx,1 000000013F31120A xor ecx,ecx 000000013F31120C call TestStd (13F311019h) TestFast(0, 1, 2, 3, 4, 5); 000000013F311211 mov dword ptr [rsp+28h],5 000000013F311219 mov dword ptr [rsp+20h],4 000000013F311221 mov r9d,3 000000013F311227 mov r8d,2 000000013F31122D mov edx,1 000000013F311232 xor ecx,ecx 000000013F311234 call TestFast (13F31101Eh)
000000013F311239 ...
...
000000013F31123B add rsp,30h
000000013F31123F ...
...
;TestC函數的實現,省略無關代碼 int __cdecl TestC(int n0, int n1, int n2, int n3, int n4, int n5) { 000000013F311080 mov dword ptr [rsp+20h],r9d 000000013F311085 mov dword ptr [rsp+18h],r8d 000000013F31108A mov dword ptr [rsp+10h],edx 000000013F31108E mov dword ptr [rsp+8],ecx 000000013F311092 ...
... 000000013F3110D1 ret
;TestStd函數的實現,省略無關代碼 int __stdcall TestStd(int n0, int n1, int n2, int n3, int n4, int n5) { 000000013F3110E0 mov dword ptr [rsp+20h],r9d 000000013F3110E5 mov dword ptr [rsp+18h],r8d 000000013F3110EA mov dword ptr [rsp+10h],edx 000000013F3110EE mov dword ptr [rsp+8],ecx 000000013F3110F2 ...
... 000000013F311131 ret
;TestFast函數的實現,省略無關代碼 int __fastcall TestFast(int n0, int n1, int n2, int n3, int n4, int n5) { 000000013F311140 mov dword ptr [rsp+20h],r9d 000000013F311145 mov dword ptr [rsp+18h],r8d 000000013F31114A mov dword ptr [rsp+10h],edx 000000013F31114E mov dword ptr [rsp+8],ecx 000000013F311152 ...
... 000000013F311191 ret
可以看到,編譯器忽略了3個不同的調用約定keyword,而為它們產生了同樣的代碼:調用者使用rcx/ecx、rdx/edx、r8/r8d、r9/r9d來傳遞前4個參數,剩余的參數通過棧傳遞,這有些類似於x86下的__fastcall,不同的是,棧上保留了前4個參數的存儲空間。而且類似於x86下的__cdecl,函數ret時不會清理棧,棧的平衡由調用者負責。
在Debug版的代碼中,TestXXX函數的開始處,首先將rcx/ecx、rdx/edx、r8/r8d、r9/r9d中的值拷貝到棧上預留的空間里,應該是為了方便調試。在Release版中,這些預留空間有時被用來備份某個通用寄存器的值。
x64下的這種調用約定,像是__fastcall和__cdecl的一個結合,既提高了性能又能支持不定個數的參數。
調用約定是代碼函數化、模塊化的基礎,其實就是一種參數傳遞、棧平衡的策略。我們在代碼中使用一個函數時,只需要提供函數聲明,編譯器就可以依照約定產生出調用這個函數的機器碼,而在被調用的函數中,也是按照約定知道參數如何傳遞過來及如何使用。