写点什么

C/C++ 函数的调用约定详解

作者:dvlinker
  • 2022 年 6 月 23 日
  • 本文字数:2921 字

    阅读完需:约 10 分钟

函数的调用约定其实比较简单,并不复杂,但很多人对这一块内容不太了解,甚至连工作几年的朋友也不太清楚。最近有朋友想了解这一块的内容,所以今天我们就来讲一下 C/C++函数调用约定相关的内容。

1、概述


常见的函数调用约定有__cdecl C 调用、__stdcall 标准调用、__fastcall 快速调用以及__pascal 调用:



这些调用是开发语言中的关键字,放置在函数前,用来指定函数的调用约定,比如:

BOOL __stdcall InitSDK();
复制代码


BOOL __stdcall InitSDK();如上所示,调用约定关键字一般放于返回值类型与函数之间。而函数返回值类型前面一般放置函数的导入导出声明:(dll 库的函数接口有导入导出之分)

// 定义导出导入SDK_DLL_API宏#ifdef DLL_EXPORTS#define SDK_DLL_API _declspec(dllexport)#else#define SDK_DLL_API _declspec(dllimport)#endif // 将导出导入SDK_DLL_API宏放到返回值类型之前SDK_DLL_API BOOL __stdcall InitSDK();
复制代码


函数的调用约定主要决定三方面的内容:


1)函数参数的入栈顺序


函数调用时主调函数的参数是通过栈传递给被调用函数的。从汇编上看的比较清晰,在 call 函数之前,会将参数的值压到栈上,比如:



如果函数有多个参数,则会有两种入栈方式,一种是从右到左依次入栈,一种是从左到右依次入栈,这是函数调用约定决定的。


2)参数栈空间由谁来释放


函数调用完成后传递给被调用函数的参数的占用的栈空间是需要释放掉的,专业术语叫“平栈”,清理掉参数的栈空间才能做到栈平衡。参数占用的栈空间到底是谁来清理,也是函数调用约定决定的。编译器在编译链接生成汇编代码时,就生成好了清理参数栈空间的汇编代码。


3)编译时的函数名称改编


不同的调用约定下编译生成的函数名称格式可能是不同的。C++之所以支持函数重载(源代码中,函数名称相同,函数参数不同),就是因为 C++编译器会对函数名称进行改编,改编后的名称中包含参数类型进而能区分出重载的函数。

2、常见的调用约定说明


常见的函数调用约定有__cdecl C 调用、__stdcall 标准调用、__fastcall 快速调用以及__pascal 调用。C/C++ 中主要使用__cdecl C 调用、__stdcall 标准调用、__fastcall 快速调用三种。__pascal 是用于 Pascal / Delphi 编程语言的调用规则,C/C++ 中也可以使用这种调用规则,但该调用约定已经被 C++废弃,不提倡使用了。


下面我们来看看这几种调用约定的异同点,见下面的表格:


2.1、__cdecl C 调用


它是 C/C++函数默认的调用规范,C/C++运行时库中的函数基本都是__cdecl 调用。在该调用约定下,参数从右向左依次压入栈中,由主调函数负责清理参数的栈空间。该调用约定适用于支持可变参数的函数,因为只有主调函数才知道给该种函数传递了多少个参数,才知道应该清理多少栈空间。比如支持可变参数的 C 函数 printf:

int __cdecl printf ( const char *format, ... ){    va_list arglist;    int buffing;    int retval;     _VALIDATE_RETURN( (format != NULL), EINVAL, -1);     va_start(arglist, format);     _lock_str2(1, stdout);     __try {        buffing = _stbuf(stdout);         retval = _output_l(stdout,format,NULL,arglist);         _ftbuf(buffing, stdout);     }    __finally {        _unlock_str2(1, stdout);    }     return(retval);}
复制代码

2.2、__stdcall 标准调用


它是 Windows 系统提供的系统 API 函数的调用约定,比如 API 函数 GetWindowText 的声明如下:

WINUSERAPIintWINAPIGetWindowTextW(    _In_ HWND hWnd,    _Out_writes_(nMaxCount) LPWSTR lpString,    _In_ int nMaxCount);
复制代码


其中,WINAPI 宏就是__stdcall 标准调用,即:

#define WINAPI __stdcall
复制代码


同时__stdcall 也是很多提供给第三方使用的 SDK 库的 API 接口的调用约定。在该调用约定下,参数从右向左依次压入栈中,由被调用函数负责清理栈空间。如果函数是可变参的,函数的调用约定会自动转化为__cdecl 调用。

2.3、__fastcall 快速调用


该调用约定之所以被称作为快速调用,因为有部分参数可以通过寄存器直接传递,效率比较高。对于内存大小小于等于 4 字节的参数,直接使用 ECX 和 EDX 寄存器传递,剩余的参数则依次从右到左压入栈中通过栈传递,参数传递占用的栈空间由被调用函数清理。

2.4、__thiscall 调用


__thiscall 是 C++中的非静态类成员函数的默认调用约定。该调用约定也用到了寄存器传参,在调用 C++类的非静态成员函数时会传入当前类对象的地址,该地址通过 ECX 寄存器来传递的。在该调用约定下,函数的参数按照从右到左的顺序入栈,被调用的函数在返回前清理参数的栈空间。

3、调用约定不一致导致的软件异常问题


以前我们将 C++开发的 SDK 库提供给第三方厂商做二次开发,第三方客户使用的是 C#语言,即 C#开发的程序去调用 C++开发的 SDK 库,当时因为 SDK 头文件中声明的回调函数没有指定调用约定,导致程序出现异常崩溃的问题。


我们 C++开发的 SDK 提供了设置消息回调的 API 接口,并给出了回调函数的声明,如下:

/* 函数功能:用于消息回发的回调函数指针(服务器主动推送的消息通过该回调函数推给上层)   参数:DWORD dwMsgId:消息id          const unsigned char* pMsgBuf:消息中携带的数据buffer,buffer中的具体内容取决于消息id,参看消息id的头文件                 DWORD dwMsgBufLen:消息中携带的数据buffer长度   返回值:void*/typedef void (*PMsgCallBackFunc)( DWORD dwMsgId, const unsigned char* pMsgBuf, DWORD dwMsgBufLen );
复制代码


设置回调函数的接口如下:

// 设置业务消息回调接口SDK_DLL_API void __stdcall SetMsgCallBack( IN PMsgCallBackFunc pMsgCallBackFunc );
复制代码


回调函数的实现在上层的 C#程序中,回调函数的调用在 C++实现的 SDK 中,因为回调函数 PMsgCallBackFunc 在声明时没有指定函数调用约定,在 C#程序中默认是__stdcall 标准约定,所以在 C#中编译时回调函数内部会清理栈空间。而回调函数是在 C++ SDK 中调用的,在 SDK 编译时默认是__cdecl 调用,会在调用回调函数处的主调函数中释放栈空间,这样导致回调函数调用后,主调函数会释放一次栈空间,回调函数内部会释放一次栈空间,所以多释放了一次参数栈空间,导致了栈不平衡,导致程序运行出异常。


考虑跨语言调用的场景,SDK 要提供标准的 C 接口。在 SDK 的头文件中,SDK 导出接口要指定调用约定,回调函数的声明也要指定调用约定。

4、与调用约定相关的工程配置选项及/RTC 编译选项


在 Visual Studio 创建的 C++工程中,在没明确指定函数调用约定时,默认使用的都是__cdecl 调用,我们可以在工程属性配置中看到:



对于 C++工程,我们一般不需要修改默认的调用约定。如果要指定 dll 库导出接口的调用约定,我们也不需要修改工程配置,只需要在导出接口的头文件的函数声明处指定调用约定就可以了。


有人可能会说,工程属性配置中使用了默认的__cdecl 调用,我们又在头文件中将接口指定为__stdcall 标准调用,会不会有冲突?到底以哪个为准呢?没有冲突的,编译时是优先以接口声明处指定的调用约定为准的。


在 Debug 下/RTC 运行时检测编译选项是默认开启的,/RTC 运行时检测在函数调用完成后会去检测栈是否平衡,关于这一点的说明如下:(MSDN 上对/RTC 编译选项的说明)



如果没有释放参数的栈空间或者参数栈空间多释放了一次,都能检测出来。如果检测到,会弹出如下的提示:



发布于: 2022 年 06 月 23 日阅读数: 26
用户头像

dvlinker

关注

宁静致远 2022.06.19 加入

CSDN博客专家,C++高级软件工程师。从事C++软件开发十多年,通过数年的软件开发实践,积累了大量的实战经验,特别在C++软件调试及异常排查方面积累了丰富经验。现任C++高级软件工程师,并担任C++软件开发培训讲师!

评论

发布
暂无评论
C/C++函数的调用约定详解_c++_dvlinker_InfoQ写作社区