三大酒神:函数调用约定与函数名称修饰规则

来源:百度文库 编辑:中财网 时间:2024/04/28 23:58:31

使用C/C++语言开发软件的程序员经常碰到这样的问题:有时候是程序编译没有问题,但是链接的时候总是报告函数不存在(经典的LNK 2001错误),有时候是程序编译和链接都没有错误,但是只要调用库中的函数就会出现堆栈异常。这些现象通常是出现在C和C++的代码混合使用的情况下或在C++程序中使用第三方的库的情况下(不是用C++语言开发的),其实这都是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则惹的祸。函数调用方式决定了函数参数入栈的顺序,是由调用者函数还是被调用函数负责清除栈中的参数等问题;而函数名修饰规则决定了编译器使用何种名字修饰方式来区分不同的函数,如果函数之间的调用约定不匹配或者名字修饰不匹配就会产生以上的问题。本文分别对C和C++这两种编程语言的函数调用约定和函数名修饰规则进行详细的解释,比较了它们的异同之处,并举例说明了以上问题出现的原因。

 

 

函数调用约定(Calling Convention)

 

     函数调用约定不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用函数负责清除栈中的参数,还原堆栈。函数调用约定有很多方式,除了常见的__cdecl,__fastcall和__stdcall之外,C++的编译器还支持thiscall方式,不少C/C++编译器还支持naked call方式。这么多函数调用约定常常令许多程序员很迷惑,到底它们是怎么回事,都是在什么情况下使用呢?下面就分别介绍这几种函数调用约定。

 

 

1.__cdecl

 

     编译器的命令行参数是/Gd。__cdecl方式是C/C++编译器默认的函数调用约定,所有非C++成员函数和那些没有用__stdcall或__fastcall声明的函数都默认是__cdecl方式,它使用C函数调用方式,函数参数按照从右向左的顺序入栈,函数调用者负责清除栈中的参数,由于每次函数调用都要由编译器产生清除(还原)堆栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是__cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf和windows的APIwsprintf就是__cdecl调用方式。对于C函数,__cdecl方式的名字修饰约定是在函数名称前添加一个下划线;对于C++函数,除非特别使用extern "C",C++函数使用不同的名字修饰方式。

 

 

2.__fastcall

 

     编译器的命令行参数是/Gr。__fastcall函数调用约定在可能的情况下使用寄存器传递参数,通常是前两个 DWORD类型的参数或较小的参数使用ECX和EDX寄存器传递,其余参数按照从右向左的顺序入栈,被调用函数在返回之前负责清除栈中的参数。编译器使用两个@修饰函数名字,后跟十进制数表示的函数参数列表大小,例如:@function_name@number。需要注意的是__fastcall函数调用约定在不同的编译器上可能有不同的实现,比如16位的编译器和32位的编译器,另外,在使用内嵌汇编代码时,还要注意不能和编译器使用的寄存器有冲突。

 

 

3.__stdcall

 

     编译器的命令行参数是/Gz,__stdcall是Pascal程序的缺省调用方式,大多数Windows的API也是__stdcall调用约定。__stdcall函数调用约定将函数参数从右向左入栈,除非使用指针或引用类型的参数,所有参数采用传值方式传递,由被调用函数负责清除栈中的参数。对于C函数,__stdcall的名称修饰方式是在函数名字前添加下划线,在函数名字后添加@和函数参数的大小,例如:_functionname@number

 

4.thiscall

 

     thiscall只用在C++成员函数的调用,函数参数按照从右向左的顺序入栈,类实例的this指针通过ECX寄存器传递。需要注意的是thiscall不是C++的关键字,不能使用thiscall声明函数,它只能由编译器使用。

 

5.naked call

 

     采用前面几种函数调用约定的函数,编译器会在必要的时候自动在函数开始添加保存ESI,EDI,EBX,EBP寄存器的代码,在退出函数时恢复这些寄存器的内容,使用naked call方式声明的函数不会添加这样的代码,这也就是为什么称其为naked的原因吧。naked   call不是类型修饰符,故必须和_declspec共同使用。

 

     VC的编译环境默认是使用__cdecl调用约定,也可以在编译环境的Project Setting...菜单-》C/C++ =》Code   Generation项选择设置函数调用约定。也可以直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等单独确定函数的调用方式。在Windows系统上开发软件常用到WINAPI宏,它可以根据编译设置翻译成适当的函数调用约定,在WIN32中,它被定义为__stdcall。

 

 

 

函数名字修饰(Decorated Name)方式

 

     函数的名字修饰(Decorated Name)就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。多数情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。当然,在某些情况下需要指定函数的名字修饰,例如在C++程序中,为了让LINK程序或其他工具能够匹配到正确的函数名字,就必须为重载函数和一些特殊的函数(如构造函数和析构函数)指定名字装饰。另一种需要指定函数的名字修饰的情况是在汇编程序中调用C或C++的函数。如果函数名字,调用约定,返回值类型或函数参数有任何改变,原来的名字修饰就不再有效,必须指定新的名字修饰。C和C++程序的函数在内部使用不同的名字修饰方式,下面将分别介绍这两种方式。

 

1. C编译器的函数名修饰规则

 

     对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number。__cdecl调用约定仅在输出函数名前加上一个下划线前缀,例如_functionname。__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,例如@functionname@number。   

 

举例如下:

__cdecl调用约定(默认方式)

//lib.h

//文件:lib.h

#ifndefLIB_H

#defineLIB_H

intadd(int x,int y);        //或者int__cdecl add(int x,int y);但完全没必要添加一个__cdecl

#endif

 

//lib.c

//文件:lib.c

#include"lib.h"

intadd(int x,int y)        // 或者int__cdecl add(int x,int y)

{

        return x + y;

}

使用dumpbin/all libname.lib得到的相应的symbol:

LINENUMBERS#3

 

 Symbolindex:       12 Base linenumber:     4

 Symbolname = _add

 00000018(   5) 0000001E(    6)

 

__stdcall调用约定

//lib.h

//文件:lib.h

#ifndefLIB_H

#defineLIB_H

int __stdcall add(intx,int y); //①

#endif

 

//lib.c

//文件:lib.c

#include "lib.h"

int __stdcall add(intx,int y)//②注意①处和本行的形式必须一模一样,尤其②的调用约定__stdcall不能漏掉

//否则会出现重复定义错误(error C2373: 'add' : redefinition; different type modifiers)

{

        return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbolindex:       12 Base linenumber:     4

 Symbol name = _add@8

 00000018(   5) 0000001E(    6)

 

 

__fastcall调用约定

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int __fastcall add(int x,int y); //

#endif

 

//lib.c

//文件:lib.c

#include "lib.h"

int __fastcall add(int x,int y)//

{

        return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbolindex:       12 Base linenumber:     4

 Symbol name = @add@8

 00000020(   5) 00000026(    6)

 

 

 

2. C++编译器的函数名修饰规则

 

     C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管__cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。参数表的拼写代号如下所示:

X--void   

D--char   

E--unsigned char   

F--short   

H--int    

I--unsigned int   

J--long   

K--unsigned long(DWORD)

M--float   

N--double   

_N--bool

U--struct

....

指针的方式有些特别,用PA表示指针,用PB表示const类型的指针。后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复。U表示结构类型,通常后跟结构体的类型名,用“@@”表示结构类型名的结束。函数的返回值不作特殊处理,它的描述方式和函数参数一样,紧跟着参数表的开始标志,也就是说,函数参数表的第一项实际上是表示函数的返回值类型。参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。下面举两个例子,假如有以下函数声明:

int Function1(char *var1,unsignedlong);

 其函数修饰名为“?Function1@@YGHPADK@Z”,而对于函数声明:

 

void Function2();

 其函数修饰名则为“?Function2@@YGXXZ” 。

 

类似地,以上的三个完整的例子在C++编译器下:

__cdecl调用约定(默认方式)

//lib.h

//文件:lib.h

#ifndefLIB_H

#defineLIB_H

intadd(int x,int y);        //或者int__cdecl add(int x,int y);但完全没必要添加一个__cdecl

#endif

 

//lib.cpp

//文件:lib.cpp

#include"lib.h"

intadd(int x,int y)        // 或者int__cdecl add(int x,int y)

{

        return x + y;

}

使用dumpbin/all libname.lib得到的相应的symbol:

LINENUMBERS#3

 

 Symbolindex:       12 Base linenumber:     4

 Symbolname = ?add@@YAHHH@Z (int __cdecl add(int,int))

 00000018(    5) 0000001E(   6)

 

附:调用约定保持不变,如果采用以下的代码,

//lib.h

//文件:lib.h

#ifndefLIB_H

#defineLIB_H

extern"C" int add(int x,int y);//声明为C编译、连接方式的外部函数

#endif

 

//lib.cpp(编译器自动识别文件为C++文件然后采用C++编译器进行编译,#ifdef __cplusplus常常用来进行这种判断,MSDN__cplusplus一个预定义宏(PredefinedMacros … __cplusplus Defined for C++programs only.))

//文件:lib.cpp

#include "lib.h"

int add(intx,int y)        //

{

        return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS#3

 

 Symbolindex:       12 Base linenumber:     4

 Symbolname = _add

 00000018(   5) 0000001E(    6)

可以看到extern “C”使得其作用的函数采用C名字修饰方式进行编译。

 

另外,不要在C程序(源程序文件以.c作为后缀)中使用extern “C”,否则也会出现错误。看下例:

//lib.h

//文件:lib.h

#ifndefLIB_H

#defineLIB_H

extern"C" int add(int x,int y);

#endif

//lib.c

//文件:lib.c

#include"lib.h"

intadd(int x,int y)       

{

        return x + y;

}

由于C编译器不认识extern “C”,对lib.c进行编译时报错:

…lib.h(4) : error C2059: syntax error : 'string'//…lib.h(4)à extern "C" int add(int x,int y)

 

 

__stdcall调用约定

//lib.h

//文件:lib.h

#ifndefLIB_H

#defineLIB_H

int __stdcall add(intx,int y); //①

#endif

 

//lib.cpp

//文件:lib.cpp

#include "lib.h"

int __stdcall add(intx,int y)//②注意①处和本行的形式必须一模一样,尤其②的调用约定__stdcall不能漏掉

//否则会出现重复定义错误(error C2373: 'add' : redefinition; different type modifiers)

{

        return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbolindex:       12 Base linenumber:     4

 Symbol name = ?add@@YGHHH@Z(int __stdcall add(int,int))

 00000018(   5) 0000001E(    6)

 

__fastcall调用约定

//lib.h

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int __fastcall add(int x,int y); //

#endif

 

//lib.cpp

//文件:lib.cpp

#include "lib.h"

int __fastcall add(int x,int y)//

{

        return x + y;

}

使用dumpbin /all libname.lib得到的相应的symbol:

LINENUMBERS #3

 

 Symbolindex:       12 Base linenumber:     4

 Symbol name = ?add@@YIHHH@Z(int __fastcall add(int,int))

 00000020(   5) 00000026(    6)

    

对于C++的类成员函数(其调用方式是thiscall),函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和参数表之间插入以“@”字符引导的类名;其次是参数表的开始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是“@@IAE”,私有(private)成员函数的标识是“@@AAE”,如果函数声明使用了const关键字,则相应的标识应分别为“@@QBE”,“@@IBE”和“@@ABE”。如果参数类型是类实例的引用,则使用“AAV1”,对于const类型的引用,则使用“ABV1”。下面就以类CTest为例说明C++成员函数的名字修饰规则:

 

 

class CTest

{

......

private:

     void Function(int);

protected:

     void CopyInfo(const CTest&src);

public:

     long DrawText(HDC hdc, long pos,const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet);

     long InsightClass(DWORD dwClass)const;

......

};

 

 

 

对于成员函数Function,其函数修饰名为“?Function@CTest@@AAEXH@Z”,字符串“@@AAE”表示这是一个私有函数。成员函数CopyInfo只有一个参数,是对类CTest的const引用参数,其函数修饰名为“?CopyInfo@CTest@@IAEXABV1@@Z”。DrawText是一个比较复杂的函数声明,不仅有字符串参数,还有结构体参数和HDC句柄参数,需要指出的是HDC实际上是一个HDC__结构类型的指针,这个参数的表示就是“PAUHDC__@@”,其完整的函数修饰名为“?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z”。InsightClass是一个共有的const函数,它的成员函数标识是“@@QBE”,完整的修饰名就是“?InsightClass@CTest@@QBEJK@Z”。

 

无论是C函数名修饰方式还是C++函数名修饰方式均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。

 

3.查看函数的名字修饰

 

     有两种方式可以检查你的程序中的函数的名字修饰:使用编译输出列表或使用Dumpbin工具。使用/FAc,/FAs或/FAcs命令行参数可以让编译器输出函数或变量名字列表。使用dumpbin.exe/SYMBOLS命令也可以获得obj文件或lib文件中的函数或变量名字列表。此外,还可以使用 undname.exe 将修饰名转换为未修饰形式。

 

 

函数调用约定和名字修饰规则不匹配引起的常见问题

     函数调用时如果出现堆栈异常,十有八九是由于函数调用约定不匹配引起的。比如动态链接库a有以下导出函数:

 

long MakeFun(long lFun);

 

动态库生成的时候采用的函数调用约定是__stdcall,所以编译生成的a.dll中函数MakeFun的调用约定是_stdcall,也就是函数调用时参数从右向左入栈,函数返回时自己还原堆栈。现在某个程序模块b要引用a中的MakeFun,b和a一样使用C++方式编译,只是b模块的函数调用方式是__cdecl,由于b包含了a提供的头文件中MakeFun函数声明,所以MakeFun在b模块中被其它调用MakeFun的函数认为是__cdecl调用方式,b模块中的这些函数在调用完MakeFun当然要帮着恢复堆栈啦,可是MakeFun已经在结束时自己恢复了堆栈,b模块中的函数这样多此一举就引起了栈指针错误,从而引发堆栈异常。宏观上的现象就是函数调用没有问题(因为参数传递顺序是一样的),MakeFun也完成了自己的功能,只是函数返回后引发错误。解决的方法也很简单,只要保证两个模块的在编译时设置相同的函数调用约定就行了。

 

     在了解了函数调用约定和函数的名修饰规则之后,再来看在C++程序中使用C语言编译的库时经常出现的LNK 2001错误就很简单了。还以上面例子的两个模块为例,这一次两个模块在编译的时候都采用__stdcall调用约定,但是a.dll使用C语言的语法编译的(C语言方式),所以a.dll的载入库a.lib中MakeFun函数的名字修饰就是“_MakeFun@

 

error LNK2001: unresolved externalsymbol ?MakeFun@@YGJJ@Z

 

解决的方法和简单,就是要让b模块知道这个函数是C语言编译的,extern "C"可以做到这一点。一个采用C语言编译的库应该考虑到使用这个库的程序可能是C++程序(使用C++编译器),所以在设计头文件时应该注意这一点。通常应该这样声明头文件:

 

#ifdef _cplusplus

extern "C" {

#endif

 

long MakeFun(long lFun);

 

#ifdef_cplusplus

}

#endif

 

这样C++的编译器就知道MakeFun的修饰名是“_MakeFun@

 

     许多人不明白,为什么我使用的编译器都是VC的编译器还会产生“errorLNK

 

 

 

 

 

“error LNK2001: unresolvedexternal symbol”深入浅出

错误现场再现:在工程A(Win32 Static Library)创建一个静态库,然后在另一个工程B(Win32 Console Application)中使用这个库。

A //库工程(Win32Static Library)

//文件:lib.h

#ifndef LIB_H

#define LIB_H

extern "C" int add(intx,int y);//声明为C编译、连接方式的外部函数

#endif

 

//文件:lib.cpp

#include "lib.h"

int add(intx,int y)

{

       return x + y;

}

或者采用以下另一种形式(但习惯上人们都在.cpp文件中实现C程序,而基本不用.c后缀。):

//文件:lib.h

#ifndef LIB_H

#define LIB_H

int add(int x,int y);//声明为C编译、连接方式的外部函数

#endif

 

//文件:lib.c

#include "lib.h"

int add(int x,int y)

{

       return x + y;

}

Build该工程,生成staticlib.lib库文件。

 

B //调用程序工程(Win32Console Application)

//文件appCaller.cpp

//文件:appCaller.cpp

#include

#include "..\静态链接库\lib.h"     //包含进相应的程序,可以与lib工程共用。

#pragma comment( lib, "..\\静态链接库\\debug\\staticlib.lib" )   //指定与静态库一起连接

int main(int argc, char* argv[])

{

    printf("2 + 3 = %d", add( 2, 3 ) );

}

编译(Compile)该文件(先不要马上Build,尽管完全可以这样做。),除了可能出现几个warning之外,没有其他问题,这就表示编译通过。

 

之后进行Build(先前已经进行了编译,所以这里主要的工作就只剩下了链接(Link),不过编译器中没有专门的命令。),结果显示错误:

--------------------Configuration:StaticlibCaller - Win32 Debug--------------------

Linking...

appCaller.obj : error LNK2001:unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z)

Debug/StaticlibCaller.exe : fatalerror LNK1120: 1 unresolved externals

Error executing link.exe.

 

StaticlibCaller.exe - 2 error(s), 0warning(s)

 

"int __cdecladd(int,int)" (?add@@YAHHH@Z):

其中的"int __cdecl add(int,int)"是指lib.h中的函数,这个是应用程序自己的函数,也即unresolved externalsymbol无法解析的符号是指自身要用到的符号无法解析,这个解析是要到相应的库文件(staticlib.lib)中去解析,如果在库文件中找不到这个符号,就报错,表示连接失败。

       而(?add@@YAHHH@Z)是指左边的函数的完整的C++编译器修饰名。

 

解决办法:

//文件:lib.c改为lib.cpp,然后使用extern “C”处理头文件中的add函数,重新编译生成库。完了原样处理文件appCaller.cpp就可以了。

当然还有别的方法,这里限于篇幅不一一例举。

 

 

 

下面给出几个较复杂的函数及其相应的C++修饰名:

"public: virtual long __stdcallCBaseRenderer::FindPin(wchar_t const *,struct IPin * *)"(?FindPin@CBaseRenderer@@UAGJPB_WPAPAUIPin@@@Z)

 

"public: virtual long __stdcallCBaseVideoRenderer::JoinFilterGraph(struct IFilterGraph *,wchar_t const*)" (?JoinFilterGraph@CBaseVideoRenderer@@UAGJPAUIFilterGraph@@PB_W@Z)

 

"public: virtual long __stdcallCBaseFilter::QueryVendorInfo(wchar_t * *)"(?QueryVendorInfo@CBaseFilter@@UAGJPAPA_W@Z)

 

"public: __thiscallCBaseVideoRenderer::CBaseVideoRenderer(struct _GUID const &,char *,structIUnknown *,long *)"(??0CBaseVideoRenderer@@QAE@ABU_GUID@@PADPAUIUnknown@@PAJ@Z) referenced infunction "public: __thiscallOgre::CTextureRenderer::CTextureRenderer(struct IUnknown *,long *)"(??0CTextureRenderer@Ogre@@QAE@PAUIUnknown@@PAJ@Z)

 

假设将以上的appCaller.cpp改为:

//文件appCaller.cpp

#include

//#include "..\静态链接库\lib.h"

#pragma comment( lib, "..\\静态链接库\\debug\\staticlib.lib" )//指定与静态库一起连接

//int add(int x,int y);

extern "C" chargetChar1(void);

char getChar2(void);

int main(int argc, char* argv[])

{

    //printf("2 + 3 = %d", add( 2, 3 ) );

       char ch1 = getChar1();

       char ch2 = getChar2();

}

 

编译通过,链接出错,原因在于函数getChar1()和getChar2()压根就没有定义过,所以也没法用:

--------------------Configuration:StaticlibCaller - Win32 Debug--------------------

Linking...

appCaller.obj : error LNK2001:unresolved external symbol "char __cdecl getChar2(void)" (?getChar2@@YADXZ)

appCaller.obj : error LNK2001:unresolved external symbol _getChar1

Debug/StaticlibCaller.exe : fatalerror LNK1120: 2 unresolved externals

Error executing link.exe.

 

StaticlibCaller.exe - 3 error(s), 0warning(s)

注意两个函数的symbol的区别所在(一个C名字修饰,一个C++名字修饰)。