函数修饰

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

函数调用约定(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调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printfwindowsAPI wsprintf就是__cdecl调用方式。对于C函数,__cdecl方式的名字修饰约定是在函数名称前添加一个下划线;对于C++函数,除非特别使用extern "C"C++函数使用不同的名字修饰方式。

2.__fastcall

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

3.__stdcall

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

4.thiscall

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

5.naked call

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

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

 

C:

[FunctionName]       函数名

[Size]                 参数字节数

_stdcall    _[FunctionName]@[Size]

_cdecl               _[FunctionName]

_fastcall @[FunctionName]@[Size]

C++:

[FunctionName]       函数名

[Class]     类标志

         仅对_thiscall                      @(接类名)

[Begin]     表开始标志

         _cdecl
                                 @@YA

         _stdcall
                     @@YG

         _fastcall
                    @@YI

         thiscall(public)
                  @@QAE

         thiscall(protected)
            @@IAE

         thiscall(private)
                 @@AAE

         thiscall(public)const
                  @@QBE

         thiscall(protected)const
   @@IBE

         thiscall(private)const
       @@ABE

[ReturnValue] 返回值表(语法同参数表)

[ParamTable] 参数表

         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     (
后面接struct的类型名,并以@@结束)

         PA
指针             (后面接指针的指向类型,若相同类型的指针连续出现,以O(大写字母O)代替,一个O代表重复一次)

         PB
const
指针  (后面接指针的指向类型,若相同类型的指针连续出现,以O(大写字母O)代替,一个O代表重复一次)

[End]                 表结束标志

         函数无参数则标志为                Z

         函数有参数则标志为                @Z

        

_cdecl               ?[FunctionName][Begin][ReturnValue][ParamTable][End]

_stdcall    ?[FunctionName][Begin][ReturnValue][ParamTable][End]

_fastcall ?[FunctionName][Begin][ReturnValue][ParamTable][End]

thiscall     ?[FunctionName][Class][Begin][ReturnValue][ParamTable][End]

例:int _stdcall GetStuID(char * pStuName,unsigned
long hState); = ?GetStuID@@YGHPADK@Z

         void
_cdecl RunProc(); = ?RunProc@@YAXXZ

         class
CTest

         {

         ......

         private:

             void Function(int); =
?Function@CTest@@AAEXH@Z

         protected:

             void CopyInfo(const CTest &src); =
?CopyInfo@CTest@IAEXABV1@Z

         public:

             long DrawText(HDC hdc, long pos, const
TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet); =
?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z (HDC
结构体HDC__的指针类型,RGBQUAD即为结构体tagRGBQUAD)

             long InsightClass(DWORD dwClass)const; =
?InsightClass@CTest@@QBEJK@Z

         ......

         };

 

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

long MakeFun(long lFun);

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

 

在了解了函数调用约定和函数的名修饰规则之后,再来看在C++程序中使用C语言编译的库时经常出现的LNK 2001错误就很简单了。还以上面例子的两个模块为例,这一次两个模块在编译的时候都采用__stdcall调用约定,但是a.dll使用C语言的语法编译的(C语言方式),所以a.dll的载入库a.libMakeFun函数的名字修饰就是“_MakeFun@4”。b包含了a提供的头文件中MakeFun函数声明,但是由于b采用的是C++语言编译,所以MakeFunb模块中被按照C++的名字修饰规则命名为“?MakeFun@@YGJJ@Z”,编译过程相安无事,链接程序时c++的链接器就到a.lib中去找“?MakeFun@@YGJJ@Z”,但是a.lib中只有“_MakeFun@4”,没有“?MakeFun@@YGJJ@Z”,于是链接器就报告:

error LNK2001: unresolved external symbol
?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@4”,就不会有链接错误了。

 许多人不明白,为什么我使用的编译器都是VC的编译器还会产生“error LNK2001”错误?其实,VC的编译器会根据源文件的扩展名选择编译方式,如果文件的扩展名是“.C”,编译器会采用C的语法编译,如果扩展名是“.cpp”,编译器会使用C++的语法编译程序,所以,最好的方法就是使用extern
"C"

 

原文链接: https://www.cnblogs.com/ziwuge/archive/2011/09/19/2181900.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    函数修饰

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/32587

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月8日 上午9:46
下一篇 2023年2月8日 上午9:48

相关推荐