宏WINAPI和几种调用约定

在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的一个结合,既提高了性能又能支持不定个数的参数。

调用约定是代码函数化、模块化的基础,其实就是一种参数传递、栈平衡的策略。我们在代码中使用一个函数时,只需要提供函数声明,编译器就可以依照约定产生出调用这个函数的机器码,而在被调用的函数中,也是按照约定知道参数如何传递过来及如何使用。

posted on 2014-09-12 10:30  孤影对酌  阅读(2698)  评论(2编辑  收藏  举报

导航