C语言的初学者常犯的一个失误是调用系统或第三方API时忘了在函数声明中标注WINAPI调用方式,而菜鸟C++程序员也每每困惑为什么成员函数必须指示为CALLBACK再添加static关键字才能作为回调函数。在x86系统中,存在多种函数调用约定。如果调用者与被调用者采用不同的调用约定,很可能导致堆栈破坏、非法访问等致命错误。
也许您会得出一个结论,除非借助汇编指令,否则调用约定之间的转换是不可能的。不过很多事情都不是绝对的。我们先看下面的例子:
#include <stdio.h>
int __cdecl CDeclFunction()
{
printf("From CDecl function\n");
return 1;
}
int main()
{
printf("Begin call STDCall function\n");
typedef int(__stdcall * STDCALLFUNCTION)();
STDCALLFUNCTION pfn =(STDCALLFUNCTION)CDeclFunction;
int i=pfn();
printf("End call STDCall function\n");
return i;
}
这是一个简单的函数调用的例子,唯一特别的地方是函数定义为__cdecl,而调用时采用__stdcall方式。我们把这段代码编译后执行。嗬,一切正常。如果您不服气,再加一个for循环看看
我们再来看另一个更特别的例子:
#include <stdio.h>
int __stdcall StdCallFunction (int i, char* pszString)
{
printf("From STDCall function\n");
printf(pszString);
return i;
}
int main()
{
printf("Begin call FastCall function\n");
typedef int(__fastcall * FASTCALLFUNCTION)(int, int, int, char*);
FASTCALLFUNCTION pfn =(FASTCALLFUNCTION)StdCallFunction;
int i=pfn(0,0,3,"test\n");
printf("End call FastCall function\n");
return i;
}
这次不光调用方式不同,连参数的个数都不一样。别担心,我们编译后执行。还是一切正常!
如果您对上述两个例子百思不得其解的话,现在该是解开谜底的时候了。这个谜底就是函数调用约定本身。网上介绍调用约定的资料已经相当多了,不过几乎都侧重比较各种调用方式的差别,而本文将把重点放在这些调用方式之间的联系上面(这里也略去诸多与主题无关的细节,要想了解函数调用约定的所有方式,相应的汇编代码,以及在参数或返回值超过32位等复杂情况的处理方式,请查阅本文列出的参考书目)。
调用函数时,需要传递参数,并接收返回值。对于C++非静态成员函数,还要考虑如何传递this指针。这要么采用寄存器,要么采用堆栈。如果采用堆栈,还要考虑由谁负责恢复堆栈的平衡。调用约定即是调用者和被调用者之间传递参数和接收返回值的规范。对于C语言中的函数和C++语言中的全局函数、静态成员函数,常用的调用约定有C调用约定(用__cdecl关键字指示,默认编译环境下可省略该关键字)、标准调用约定(用__stdcall关键字或WINAPI、CALLBACK宏指示)、快速调用约定(用__fastcall关键字指示)3种。这三种方式都使用EAX寄存器接收返回值,且参数都是按从右到左的顺序压入堆栈,先压最后一个参数。其中,C调用是调用者收回参数压栈占用的空间,这样可支持变长参数;而标准调用是被调用函数自己负责恢复堆栈的平衡;快速调用也是被调用函数清理堆栈,但函数的前两个参数通过ECX和EDX寄存器传递。
对于C++非静态成员函数,默认采用THIS调用约定(不需要关键字指示)。该方式除用ECX寄存器传递对象的this指针外,其他方面同标准调用。另外一种资料上较少提及的非静态成员函数调用方式为C++标准调用(同样用__stdcall关键字指示),这种方式把this指针视为第一个参数,其他方面完全按C函数标准调用约定处理。
在上述5种调用方式中,完全相同的地方是都使用EAX寄存器传递返回值,这扫除了彼此转换的第一个障碍。其次,较一致的地方是除了C调用外,其他调用约定都由被调用函数负责恢复堆栈平衡,这意味着在转换这些调用方式时,只需关注如何传递参数(及this指针,对于C++非静态成员函数的话)就可以了。对于C调用与其他调用约定的转换,堆栈的平衡便成为其无法逾越的鸿沟。但当函数没有参数时,不需要考虑参数压栈引起的堆栈平衡问题,C调用与其他调用约定之间的转换便成为可能。这正是第一个例子正常运行的原因所在。当包含参数时,C调用与其他调用约定之间的转换要么借助汇编指令,要么采用“对等补偿”的方式(不推荐)。如下面的代码所示:
#include <stdio.h>
int __cdecl CDeclFunction(int a,char* pszString)
{
printf("From CDecl function\n");
printf(pszString);
return a;
}
int __stdcall StdCallFunction (int i)
{
printf("From STDCall function\n");
return i;
}
int main()
{
printf("Begin call STDCall function\n");
typedef int(__stdcall * STDCALLFUNCTION)(int,char*);
STDCALLFUNCTION pfn1 =(STDCALLFUNCTION)CDeclFunction;
int i=pfn1(2,"test\n");
printf("End call STDCall function\n");
printf("Begin call CDecl function\n");
typedef int(__cdecl * CDECLFUNCTION)(int);
CDECLFUNCTION pfn2 =(CDECLFUNCTION)StdCallFunction;
int j=pfn2(3);
int k=pfn2(4);
printf("End call CDecl function\n");
return i+j+k;
}
以上代码要正常运行,需关闭编译器的堆栈帧检查功能(取消/RTCs编译选项)。在我使用的Visual C++ 2005开发环境中,在项目的属性页,选择“代码生成”,把“基本运行时检查”由“两者”改为“未初始化的变量”。
现在我们撇开C调用,专注于其他几种调用方式之间的转换。如前所述,我们只需考察参数是如何传递的。如果把C++非静态成员函数的this指针看作是第一个参数,我们会发现这些调用约定更为相似。当用堆栈压参数时,参数在堆栈中的顺序是一致的,只要能定位其中一个参数,就可以定位堆栈上的其他参数。标准调用和C++标准调用都使用堆栈传递所有参数,这意味着两种调用方式可以直接相互转换。即形如 RETURN_TYPE __stdcall NonStaticFunction (Type1 param1, Type2 param2... Typen paramn) 的C++非静态成员函数与形如 RETURN_TYPE __stdcall StaticFunction (PVOID pThis, Type1 param1, Type2 param2... Typen paramn) 的全局函数或静态成员函数是等价的。
其他两种调用方式使用寄存器传递前面的一两个参数:THIS调用约定用ECX寄存器传递第一个参数(即this指针);快速调用约定用ECX和EDX寄存器传递前两个参数。快速调用的第三个参数,与THIS调用的第二个参数(不考虑this指针则为第一个参数),与标准调用的第一个参数,在堆栈上的位置是相同的。我们无法用C/C++语言直接操纵寄存器,不过可以忽略寄存器。通过增加虚拟的用寄存器传递的参数,我们可以用THIS调用模拟标准调用,或用快速调用模拟THIS调用和标准调用。即在下列3个函数原型中,后者可以模拟前者。本文前面的第二个例子,就是基于这样的原理。
RETURN_TYPE __stdcall StdCallFunction (Type1 param1, Type2 param2... Typen paramn);
class AClass
{
RETURN_TYPE MemberFunction (Type1 param1, Type2 param2... Typen paramn);
};
RETURN_TYPE __fastcall FastCallFunction (ECXType ecx_param, EDXType edx_param, Type1 param1, Type2 param2... Typen paramn);
参考书目:
Visual C++语言参考手册:Calling Conventions。
Debugging Tools for Windows联机手册:x86 Architecture。
Debugging Applications for Microsoft .NET and Microsoft Windows:Chapter 7: Advanced Native Code Techniques with Visual Studio .NET。x86 Assembly Language
- 标 题:C与C++函数调用约定之间的转换
- 作 者:wdbg
- 时 间:2010-03-03 10:23:42
- 链 接:http://bbs.pediy.com/showthread.php?t=108111