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

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

时间: 2024-12-21 14:50:05

宏WINAPI和几种调用约定的相关文章

64位只有一种调用约定stdcall

procedure TForm2.Button1Click(Sender: TObject); function EnumWindowsProc(Ahwnd: hwnd; AlParam: lParam): Boolean; stdcall; begin ShowMessage('hwnd:' + IntToStr(Ahwnd)); ShowMessage('lParam' + IntToStr(AlParam)); Result := True; end; begin EnumChildWin

在win64里,只有一种调用约定

在win64里,只有一种调用约定.以下是通过寄存器来传递4个整数类型的例子: *RCX:第一个参数 *RDX:第二个参数 *R8:第三个参数 *R9:第四个参数 参数里开头的4个整数会这样传给栈.传递浮点数参数时,使用的是XMMO-XMM3寄存器. 调用约定简化了:一律使用__fastcall,前四个参数用 RCX.RDX.R8 和 R9传递,除了这四个外加RAX.R10.R11,其他寄存器都是非易失的.

几种调用约定

调用约定(Callingconvention)决定以下内容:函数参数的压栈顺序,由调用者还是被调用者把参数弹出栈,以及产生函数修饰名的方法.MFC支持以下调用约定: 1._cdecl 按从右至左的顺序压参数入栈,由调用者把参数弹出栈.对于"C"函数或者变量,修饰名是在函数名前加下划线.对于"C++"函数,有所不同. 如函数voidtest(void)的修饰名是_test:对于不属于一个类的"C++"全局函数,修饰名是[email protecte

3种调用约定的区别与联系

由C代码到汇编代码来看看cdecl.stdcall.fastcall三个调用约定的区别: int __stdcall add1(int x,int y) { return x + y; } int __cdecl add2(int x, int y) { return x + y; } int __fastcall add3(int x, int y,int z) { return x + y +z; } int _tmain(int argc, _TCHAR* argv[]) { add1(1

【系统篇】小议三种函数调用约定

小议三种函数调用约定 __cdecl.__stdcall.__fastcall是C/C++里中经常见到的三种函数调用方式.其中__cdecl是C/C++默认的调用方式,__stdcall是windows API函数的调用方式,只不过我们在头文件里查看这些API的声明的时候是用了WINAPI的宏进行代替了,而这个宏其实就是__stdcall了. 三种调用方式的区别相信大家应该有些了解,这篇文章主要从实例和汇编的角度阐述这些区别的表现形态,使其对它们的区别认识从理论向实际过渡. __cdecl: C

x86 x64下调用约定浅析

x86平台下调用约定 我们都知道x86平台下常用的有三种调用约定,__cdecl.__stdcall.__fastcall.我们分别对这三种调用约定进行分析. __cdecl __cdecl是C/C++的默认调用约定,如果不显示声明调用约定的情况下,就是该调用约定.下面我们来从汇编层次来熟悉这种调用约定. 我写了一个函数,如下: 1 int __cdecl TestCdecl(int a, int b, int c, int d, int e) 2 { 3 return a + b + c +

三种函数调用约定

__cdecl.__stdcall.__fastcall是C/C++里中经常见到的三种函数调用方式.其中__cdecl是C/C++默认的调用方式,__stdcall是windows API函数的调用方式,只不过我们在头文件里查看这些API的声明的时候是用了WINAPI的宏进行代替了,而这个宏其实就是__stdcall了. __cdecl: C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡. __stdcall:            windows API默认方式,参数从右向左入栈,被调

DLL中调用约定和名称修饰(一)

DLL中调用约定和名称修饰(一) 调用约定(Calling Convention)是指在程序设计语言中为了实现函数调用而建立的一种协议.这种协议规定了该语言的函数中的参数传送方式.参数是否可变和由谁来处理堆栈等问题.不同的语言定义了不同的调用约定. 在C++中,为了允许操作符重载和函数重载,C++编译器往往按照某种规则改写每一个入口点的符号名,以便允许同一个名字(具有不同的参数类型或者是不同的作用域)有多个用法,而不会打破现有的基于C的链接器.这项技术通常被称为名称改编(Name Manglin

【原创+整理】简述何为调用约定,函数导出名以及extern C

何为调用约定 调用约定指的是函数在调用时会按照不同规则,翻译成不同的汇编代码.这和参数的压栈顺序和栈的清理方式相关,也就是说不同的调用约定,这些方式会做相应改变.一般编译器是以默认的调用约定编译一份代码,但当一个项目使用不同调用约定的库会产生链接错误. 何为函数导出名 同一个函数,在不同的编译器编译出来的符号名是不一样的,程序目标文件链接的时候不知道源程序的函数名,而是通过目标文件(.obj)中寻找相应的函数符号表.在下面中会指出不同调用约定对应的函数导出名. 三种调用约定   (1)__fas