1. 本文讲述WIN32下的calling convention,也就是__cdecl, __stdcall, __fastcall这些编译器指示代码。本文所有的内容都是针对WIN32的,准确点说,是针对windows的C/C++编译器的。UNIX /Linux下基于GNU编译器的,就没有这个东西(可能是就一种calling convention吧),不过GNU编译器也有一个有趣的编译指令-__attribute__,有兴趣的可以参考Linux版中的“Using GNU C __attribute__”一文。
2. 所谓calling convention(呼叫约定)其实是我们代码中对编译器行为的一些指令。比如函数的参数如何进栈,函数执行完成,参数何时出栈等。所以,如果我们在代 码中不清楚这些东西而导致我们在定义函数和调用函数的时候使用的呼叫约定不匹配,将导致程序出错!下面是详细的解释。
3. 首先我们来看__cdecl,这是C函数的传统呼叫约定。他的逻辑是这样的:
/* example of __cdecl */
push arg1
push arg2
push arg3
call function
add sp,12 // effectively "pop; pop; pop"
可以看到,__cdecl下,首先将参数进栈,然后call这个function,完成 后将参数全部pop出堆栈。__cdecl是微软编译器的默认呼叫约定。当然,我们可以在我们代码的project中指定默认的call convention,也就是说,如果一个函数申明的时候,没有指定call convention,就是用project中设置的这个。
4. 但是我们在windows编程的过程中,经常会碰到另外一个呼叫约定-__stdcall,比如我们在申明WinMain函数的时候,前面加上的 WINAPI,其实就是__stdcall,这种呼叫方式我们称为PASCAL呼叫约定。和__cdecl不同的是,__stdcall呼叫约定不需要函 数手动将参数出栈,他通过callee统一负责把参数弹出堆栈:
/* example of __stdcall */
push arg1
push arg2
push arg3
call function
// no stack cleanup - callee does this
OK,看到这里应该已经明白了,如果我们在写代码的时候,不注意呼叫约定,在定义函数和调用函数的时候使用呼叫约定不匹配的话,会导致堆栈遭破坏,从而整个程序执行出错!!
5. 还有一个呼叫约定常用的叫__fastcall,这里我们并不过多涉及。这个呼叫约定使用CPU register来存放参数从而达到高效的目的,但事实上,实际效果并没有宣称的那么高性能。本文我们关注的是__cdecl, __stdcall。
6. OK,从上面的内容,我们可以发现这样一些结论:
(1) 使用__cdecl,我们的二进制代码如果用汇编来看的话,会发现每个函数调用的后面,都会跟着一两句“参数出栈”的代码,这会导致二进制代码的文件大小增加,相比与使用__stdcall--一般要在程序很大的时候才能明显看出。
(2) 对于一些可变参数的函数,比如printf,这种函数的参数个数不确定,所以,对于这种函数,我们就不能使用__stdcall,很明显,因为 __stdcall使用callee来做“参数出栈”的动作,对于这样的函数,callee无法确定要将多少个参数出栈,所以我们只能使用__cdecl
(3) 其实我们关注的不是__cdecl, __stdcall本身,我们关注的是在我们的代码中,我们要确保函数的呼叫约定是一致的(match的),否则就会导致堆栈被破坏,从而程序出现严重错误。
7. Linker symbol name decorations. 从上面我们可以看到,如果我们使用呼叫约定不匹配的话,会带来非常严重的后果。为了解决这样的问题,微软做了一些工作(应该说是微软的编译器)。这就是 symbol name decoration. 针对不同的呼叫约定,微软编译器给函数定义不同的名字(decoration),从而避免呼叫约定不匹配的问题。这也是为什么我们的代码经过编译后,函数 的名字发生了变化的原因:
(1) 对于__cdecl(编译器开关是cl /Gd),微软编译器在编译的时候,将这种呼叫约定的函数名改成 _<function name>
(2) 对于__stdcall(编译器编译选项是cl /Gz), 微软编译器在编译的时候,将这种呼叫约定的函数名改成 _<function name>@<所有参数的字节数>
(3) 对于__fastcall(编译器编译选项是cl /Gr), 微软编译器在编译的时候,将这种呼叫约定的函数名改成 @<function name>@<所有参数的字节数>
形象点,看下面的图片:
附件1
8. 通过上面的做法,我们看到,不同的呼叫约定,编译器为我们生成的函数名字不一样,这样在一定程度上避免了呼叫约定的错用。为什么能避免呼叫约定的误用呢?我们可以来考虑这样的一个例子:
我 们的代码需要使用到一个第三方的函数库。这个函数库提供了.h文件和.lib文件,但是在.h文件中,函数前没有指定call convention,也就是说,这个函数的calling convention依赖于我们这个Project中指定的calling convention(在Visual Studio的Project Properties对话框中,在C/C++选项下的calling convention一项中可以指定),假设是__cdecl吧。OK,这样,我们的代码编译完成后,对这个第三方的库的函数的调用就是__cdecl方 式,但是很可能,这个第三方的库在编译的时候,函数用的是__stdcall的呼叫约定,此时程序就有可能出错。但是我们说道了,微软的编译器有 symbol name decoration,这样一来,在我们的代码中,实际要调用的这个第三方的函数是 "_<function name>",但是在lib中,这个函数的名字是 "_<function name>@<parameter byte count>",这是不匹配的,所以在程序Link的时候,就会出错,这样就避免了呼叫约定不匹配的问题!
9. 调用DLL中的函数。DLL有点特殊,他没有.h文件,也没有lib文件,我们用LoadLibrary这个API来载入dll中的函数。此时我们不知道 dll中这些函数的call convention,在这个时候,请参考DLL的文档,确保使用的call convention是正确的!!
10. Good Practise. 我们在写代码的时候,这里给出一些有关call convention的good practise。
(1) Rule 1: Library headers should explicitly name a calling convention everywhere — Do not rely on the default. 所有的函数都手动指定call convention,不要依赖于呼叫约定的默认值。这是最好的做法了。这里注意,对于一个函数,我们可以在函数申明的时候定义该函数的呼叫约定,也可以 在函数的实现部分,定义call convention,那么,哪个为准呢?--定义在函数申明处的call convention为准。比如:
2. 所谓calling convention(呼叫约定)其实是我们代码中对编译器行为的一些指令。比如函数的参数如何进栈,函数执行完成,参数何时出栈等。所以,如果我们在代 码中不清楚这些东西而导致我们在定义函数和调用函数的时候使用的呼叫约定不匹配,将导致程序出错!下面是详细的解释。
3. 首先我们来看__cdecl,这是C函数的传统呼叫约定。他的逻辑是这样的:
/* example of __cdecl */
push arg1
push arg2
push arg3
call function
add sp,12 // effectively "pop; pop; pop"
可以看到,__cdecl下,首先将参数进栈,然后call这个function,完成 后将参数全部pop出堆栈。__cdecl是微软编译器的默认呼叫约定。当然,我们可以在我们代码的project中指定默认的call convention,也就是说,如果一个函数申明的时候,没有指定call convention,就是用project中设置的这个。
4. 但是我们在windows编程的过程中,经常会碰到另外一个呼叫约定-__stdcall,比如我们在申明WinMain函数的时候,前面加上的 WINAPI,其实就是__stdcall,这种呼叫方式我们称为PASCAL呼叫约定。和__cdecl不同的是,__stdcall呼叫约定不需要函 数手动将参数出栈,他通过callee统一负责把参数弹出堆栈:
/* example of __stdcall */
push arg1
push arg2
push arg3
call function
// no stack cleanup - callee does this
OK,看到这里应该已经明白了,如果我们在写代码的时候,不注意呼叫约定,在定义函数和调用函数的时候使用呼叫约定不匹配的话,会导致堆栈遭破坏,从而整个程序执行出错!!
5. 还有一个呼叫约定常用的叫__fastcall,这里我们并不过多涉及。这个呼叫约定使用CPU register来存放参数从而达到高效的目的,但事实上,实际效果并没有宣称的那么高性能。本文我们关注的是__cdecl, __stdcall。
6. OK,从上面的内容,我们可以发现这样一些结论:
(1) 使用__cdecl,我们的二进制代码如果用汇编来看的话,会发现每个函数调用的后面,都会跟着一两句“参数出栈”的代码,这会导致二进制代码的文件大小增加,相比与使用__stdcall--一般要在程序很大的时候才能明显看出。
(2) 对于一些可变参数的函数,比如printf,这种函数的参数个数不确定,所以,对于这种函数,我们就不能使用__stdcall,很明显,因为 __stdcall使用callee来做“参数出栈”的动作,对于这样的函数,callee无法确定要将多少个参数出栈,所以我们只能使用__cdecl
(3) 其实我们关注的不是__cdecl, __stdcall本身,我们关注的是在我们的代码中,我们要确保函数的呼叫约定是一致的(match的),否则就会导致堆栈被破坏,从而程序出现严重错误。
7. Linker symbol name decorations. 从上面我们可以看到,如果我们使用呼叫约定不匹配的话,会带来非常严重的后果。为了解决这样的问题,微软做了一些工作(应该说是微软的编译器)。这就是 symbol name decoration. 针对不同的呼叫约定,微软编译器给函数定义不同的名字(decoration),从而避免呼叫约定不匹配的问题。这也是为什么我们的代码经过编译后,函数 的名字发生了变化的原因:
(1) 对于__cdecl(编译器开关是cl /Gd),微软编译器在编译的时候,将这种呼叫约定的函数名改成 _<function name>
(2) 对于__stdcall(编译器编译选项是cl /Gz), 微软编译器在编译的时候,将这种呼叫约定的函数名改成 _<function name>@<所有参数的字节数>
(3) 对于__fastcall(编译器编译选项是cl /Gr), 微软编译器在编译的时候,将这种呼叫约定的函数名改成 @<function name>@<所有参数的字节数>
形象点,看下面的图片:
附件1
8. 通过上面的做法,我们看到,不同的呼叫约定,编译器为我们生成的函数名字不一样,这样在一定程度上避免了呼叫约定的错用。为什么能避免呼叫约定的误用呢?我们可以来考虑这样的一个例子:
我 们的代码需要使用到一个第三方的函数库。这个函数库提供了.h文件和.lib文件,但是在.h文件中,函数前没有指定call convention,也就是说,这个函数的calling convention依赖于我们这个Project中指定的calling convention(在Visual Studio的Project Properties对话框中,在C/C++选项下的calling convention一项中可以指定),假设是__cdecl吧。OK,这样,我们的代码编译完成后,对这个第三方的库的函数的调用就是__cdecl方 式,但是很可能,这个第三方的库在编译的时候,函数用的是__stdcall的呼叫约定,此时程序就有可能出错。但是我们说道了,微软的编译器有 symbol name decoration,这样一来,在我们的代码中,实际要调用的这个第三方的函数是 "_<function name>",但是在lib中,这个函数的名字是 "_<function name>@<parameter byte count>",这是不匹配的,所以在程序Link的时候,就会出错,这样就避免了呼叫约定不匹配的问题!
9. 调用DLL中的函数。DLL有点特殊,他没有.h文件,也没有lib文件,我们用LoadLibrary这个API来载入dll中的函数。此时我们不知道 dll中这些函数的call convention,在这个时候,请参考DLL的文档,确保使用的call convention是正确的!!
10. Good Practise. 我们在写代码的时候,这里给出一些有关call convention的good practise。
(1) Rule 1: Library headers should explicitly name a calling convention everywhere — Do not rely on the default. 所有的函数都手动指定call convention,不要依赖于呼叫约定的默认值。这是最好的做法了。这里注意,对于一个函数,我们可以在函数申明的时候定义该函数的呼叫约定,也可以 在函数的实现部分,定义call convention,那么,哪个为准呢?--定义在函数申明处的call convention为准。比如:
/*
somefile.c
*/
extern void __stdcall foo1( void );
..
void foo1( void ) // OK - __stdcall taken from the declaration just seen
{
...
}
extern void __stdcall foo2( void );
...
void __cdecl foo2( void ) // ERROR - clashes with __stdcall above
{
...
}
extern void foo3( void ); // presume __cdecl
...
void __stdcall foo3( void ) // ERROR - clashes with presumed __cdecl
{
...
}
extern void __stdcall foo1( void );
..
void foo1( void ) // OK - __stdcall taken from the declaration just seen
{
...
}
extern void __stdcall foo2( void );
...
void __cdecl foo2( void ) // ERROR - clashes with __stdcall above
{
...
}
extern void foo3( void ); // presume __cdecl
...
void __stdcall foo3( void ) // ERROR - clashes with presumed __cdecl
{
...
}
(2) Rule 2: Use the C preprocessor and a portability-related header file to make this work seamlesly on non-MSVC platforms. 在非微软的OS上,比如UNIX,Linux,是没有__stdcall, __cdecl这些东西的,那么如何保证我们的代码的可移植性呢?--使用PreProcessor,我们可以定义一个portable.h文件:
#ifndef _WIN32
# define __cdecl /* nothing */
# define __stdcall /* nothing */
# define __fastcall /* nothing */
#endif /* _WIN32 */
11. 总结一下。Calling convention其实不是一个非常复杂的东西,但是误用他会让我们的程序堆栈损坏,程序出现严重错误。同时,程序员如果不清楚calling convention,在编译和调试程序的时候会抓狂的。--为什么Linker就是找不到一个函数呢?
我们在书写代码的时候,尽量做到 每个函数都带有明确的Calling convention定义。特别是如果写的是一个给别人用的Libary,那么开放给使用者的函数最好必须明确定义calling convention,那么不开放给外部的函数,自己用的函数自己清楚即可。
在调用第三方的函数的时候,一定要搞清楚,我们调用的函数的calling convention!不要误用。
函数的呼叫约定以定义在函数申明时候的为准。如果一个函数没有申明,直接就是实现代码,那么以实现代码中定义的呼叫约定为准。没有定义任何呼叫约定,以编译的时候给出的/G(x)这个编译选项为准,如果这个编译选项也没有,那么呼叫约定默认取成__cdecl。
#ifndef _WIN32
# define __cdecl /* nothing */
# define __stdcall /* nothing */
# define __fastcall /* nothing */
#endif /* _WIN32 */
11. 总结一下。Calling convention其实不是一个非常复杂的东西,但是误用他会让我们的程序堆栈损坏,程序出现严重错误。同时,程序员如果不清楚calling convention,在编译和调试程序的时候会抓狂的。--为什么Linker就是找不到一个函数呢?
我们在书写代码的时候,尽量做到 每个函数都带有明确的Calling convention定义。特别是如果写的是一个给别人用的Libary,那么开放给使用者的函数最好必须明确定义calling convention,那么不开放给外部的函数,自己用的函数自己清楚即可。
在调用第三方的函数的时候,一定要搞清楚,我们调用的函数的calling convention!不要误用。
函数的呼叫约定以定义在函数申明时候的为准。如果一个函数没有申明,直接就是实现代码,那么以实现代码中定义的呼叫约定为准。没有定义任何呼叫约定,以编译的时候给出的/G(x)这个编译选项为准,如果这个编译选项也没有,那么呼叫约定默认取成__cdecl。