函数调用约定 (cdecl stdcall)
在 C 语言里,我们通过阅读函数声明,就知道怎么携带参数去调用函数,也能在函数体定义内使用这些参数。但是 CPU 并不直接完成函数调用的传参操作,这需要人为的约定。这些约定被编译器识别和使用,生成所需的代码。
一般每个线程都维护着一个堆栈,称为调用栈。调用方想进行函数调用并传递参数,就往栈里压入参数。被调函数从栈顶取出一定数量的参数使用,这就完成了参数的传递。这个过程需要一些约定,使调用方和被调函数都能正确地识别对应参数的位置等。
顺便一提,函数返回值是怎么实现的呢?是往调用栈里压入了一个空值占位,被调函数往这个位置写入返回值,调用方再取出来使用,这就完成了返回值的传递。
函数调用约定主要包括三方面的内容:
压栈时的参数顺序,是从左至右还是从右至左
调用完成后堆栈由谁清理,是调用方还是被调函数
编译器的函数名修饰约定
主要有 cdecl stdcall 两种不同的约定,同时这也是 C 的关键字。
cdecl
cdecl 表示:
参数从右至左入栈
调用方来维护堆栈
告知 C 编译器,输出的函数名应该前加 _,即修饰为 _
cdecl 是 C/C++ 的默认函数调用约定。
在使用时,用特定的关键字来修饰函数名,Windows 下常用的 Visual C++ Compiler 支持 __cdecl;Linux 下常用的 GCC 支持 __attribute__((cdecl))。
cdecl 带来了两件事:
被调函数虽然不知道有多少参数入了栈,但会自上而下按需取走需要的参数。你可能想到了,使用可变参数(vararg/stdarg)的函数(如 printf)就只能使用 cdecl 约定。
每次函数调用后都要生成一段清栈代码,生成的目标代码会比较大
stdcall
stdcall 又称为 Pascal 约定,因为这也是 Pascal 语言使用的函数调用约定。
stdcall 表示:
参数从右至左入栈
被调函数来维护堆栈。这一过程一般通过指令 retn x 完成,x 是参数占用的字节数(记为 )
告知 C 编译器,输出的函数名应该前加 _ 后跟 @,即修饰为 _@
这里的第三点,举例说明:在 win64 环境下,函数 int __stdcall foo(void * p) 就可以在外部使用 _foo@8 来引用。
在使用时,vc++ 下使用 __stdcall;gcc 下使用 __attribute__((stdcall))。
在 Windows 编程中,宏 CALLBACK WINAPI 都指向 __stdcall。
因为 stdcall 严格控制了参数的字节数,所以不能实现可变参数。
__fastcall __thiscall
在 __stdcall 之上,引入了两个优化版本 __fastcall __thiscall。
__fastcall 将前两个或多个参数由寄存器传递(由编译器选决定选用哪些寄存器),以优化大多数参数个数较少的函数的调用耗时。C 编译器输出的函数名修饰为 @@。
__thiscall 是 C++ 中引入的,这不是关键字,不可手动指定,而是针对类成员函数自动添加,将 this 指针放在特定寄存器中(由编译器自行决定)。
__fastcall 和 __thiscall 都与编译器相关,可移植性较差。
naked call
表示函数无需保护现场(prolog)和恢复现场(epilog)的代码。
这在 vc++ 下不是通过关键字实现的,而是通过 __declspec(naked) 函数声明;gcc 下通过 __attribute__((naked))。
naked 标志跟 __inline 内联函数是不同的机制,不能一起使用。
关于函数名修饰规则
编译器输出的函数名,在其他目标文件中引用该函数时使用。
C 语言采用上述规则,而 C++ 则采用另一套规则,该规则详尽描述了函数原型,因此显得有些复杂。
但是,在 C++ 中可以使用 extern "C" 来要求以 C 编译器的方式来编译和链接,这也会以 C 的方式来修饰函数名。
在编写可移植的代码时,常用以下代码段来实现这一目标:
#ifdef __cplusplus
extern "C" {
#endif
// code
#ifdef __cplusplus
}
#endif /* end of __cplusplus */
拓展阅读
转载至链接:https://my.oschina.net/tridays/blog/1930040