首页
社区
课程
招聘
[旧帖] [原创]C与C++函数调用约定之间的转换 0.00雪花
发表于: 2010-3-3 10:23 2902

[旧帖] [原创]C与C++函数调用约定之间的转换 0.00雪花

2010-3-3 10:23
2902

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


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 7
支持
分享
最新回复 (8)
雪    币: 458
活跃值: (421)
能力值: ( LV9,RANK:610 )
在线值:
发帖
回帖
粉丝
2
第一个例子之所以不会崩溃是因为被调用的函数没有参数,也就不存在堆栈平衡的问题      你加一个参数试试,看他崩溃不崩溃
ps.讲解堆栈平衡  如果不结合反汇编码来看的话   终究是留着层面上   不能深入
2010-3-3 10:36
0
雪    币: 287
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
Thinking low level, writing high level
2010-3-3 10:47
0
雪    币: 724
活跃值: (81)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
楼主的钻研精神可嘉,对于有参数的函数,__cdecl和__stdcall错不得!

    你可以通过控制编译选项,让编译器为调用者函数main建立栈结构, 从而使main函数的栈是平的。但你忽视了错误使用__cdecl和__stdcall的结果不止是可能给main函数带来栈不平,更大的风险是它破坏了调用的上下文:
    将__cdecl函数作为__stdcall调用会使栈指针向栈顶方向移,结果是如果有后续的POP,则得到错误的值;
    将__stdcall函数作为__cdecl调用,则会使栈指针向栈底方向移,甚至可能超过函数栈,如果后续有PUSH,则会破坏栈中的内容,也许你的main函数的返回地址已被修改,这是灾难性的。
   以上两种情况都造成隐性“越界”,会给程序造成非常难查的错误!
2010-3-3 21:36
0
雪    币: 90
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
谢谢分享了,很有帮助
2010-3-3 23:24
0
雪    币: 37
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
谢谢分享,学习了
2010-3-6 00:50
0
雪    币: 6
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
谢谢楼主,学习了。
2010-3-6 09:56
0
雪    币: 78
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
编译环境:VC++6.0
编译方式:Debug

24:       StdCallFUN(1,'1',1.0f,1.0); //stdcall调用特征:纯堆栈传参
00401198   push        3FF00000h
0040119D   push        0
0040119F   push        3F800000h
004011A4   push        31h
004011A6   push        1
004011A8   call        @ILT+5(StdCallFUN) (0040100a)
25:       FastCallFUN(1,'1',1.0f,1.0);//fastCall调用特征:堆栈和寄存器传参
004011AD   push        3FF00000h
004011B2   push        0
004011B4   push        3F800000h
004011B9   mov         dl,31h
004011BB   mov         ecx,1
004011C0   call        @ILT+15(FastCallFUN) (00401014)
26:       CdeclCallFUN(1,'1',1.0f,1.0);//cdecl调用特征:堆栈传参,堆栈平衡
004011C5   push        3FF00000h
004011CA   push        0
004011CC   push        3F800000h
004011D1   push        31h
004011D3   push        1
004011D5   call        @ILT+0(CdeclCallFUN) (00401005)
004011DA   add         esp,14h
2010-3-6 18:31
0
雪    币: 109
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
支持楼主谢谢了
2010-3-6 19:42
0
游客
登录 | 注册 方可回帖
返回
//