【转】C/C++函数的系统调用的几种方式

【转自】:http://blog.csdn.net/sunshinewave/article/details/7981034#comments

我们知道,调用函数时,计算机常用栈来存放函数执行需要的参数,由于栈的空间大小是有限的,在windows下栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,windows下栈的大小是2M(也有的说是1M),如果申请的空间超过栈的剩余空间时,将提示overflow。
在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
在参数传递中,有两个重要的问题必须要明确说明:
1. 当参数个数多于一个时,按照什么顺序把参数压入堆栈;
2. 函数调用后,由谁来把堆栈恢复原状。
在高级语言中,就是通过函数的调用方式来说明这两个问题的。常见的调用方式有:
stdcall
cdecl
fastcall
thiscall
thiscall
naked call
下面就分别介绍这几种调用方式:
1. stdcall
stdcall调用方式又被称为Pascal调用方式。在Microsoft C++系列的C/C++编译器中,使用PASCAL宏,WINAPI宏和CALLBACK宏来指定函数的调用方式为stdcall。
stdcall调用方式的函数声明为:
int _stdcall function(int a, int b);
stdcall的调用方式意味着:
(1) 参数从右向左依次压入堆栈
(2) 由被调用函数自己来恢复堆栈
(3) 函数名自动加前导下划线,后面紧跟着一个@,其后紧跟着参数的尺寸
上面那个函数翻译成汇编语言将变成:
push b 先压入第二个参数
push a 再压入第一个参数
call function 调用函数
在编译时,此函数的名字被翻译为_function@8 
2. cdecl
cdecl调用方式又称为C调用方式,是C语言缺省的调用方式,它的语法为:
int function(int a, int b) // 不加修饰符就是C调用方式
int _cdecl function(int a, int b) // 明确指定用C调用方式
cdecl的调用方式决定了:
(1) 参数从右向左依次压入堆栈
(2) 由调用者恢复堆栈
(3) 函数名自动加前导下划线
由于是由调用者来恢复堆栈,因此C调用方式允许函数的参数个数是不固定的,这是C语言的一大特色。
此方式的函数被翻译为:
push b // 先压入第二个参数
push a // 在压入第一个参数
call funtion // 调用函数
add esp, 8 // 清理堆栈 。。。。。需要熟悉一下esp寄存器的功能,建议看一下汇编有关的书,基本都有讲
在编译时,此方式的函数被翻译成:_function
3. fastcall
fastcall 按照名字上理解就可以知道,它是一种快速调用方式。此方式的函数的第一个和第二个DWORD参数通过ecx和edx传递,
后面的参数从右向左的顺序压入栈。
被调用函数清理堆栈。
函数名修个规则同stdcall
其声明语法为:
int fastcall function(int a, int b);
4. thiscall
thiscall 调用方式是唯一一种不能显示指定的修饰符。它是c++类成员函数缺省的调用方式。由于成员函数调用还有一个this指针,因此必须用这种特殊的调用方式。
thiscall调用方式意味着:
参数从右向左压入栈。
如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压入栈后被压入栈。
参数个数不定的,由调用者清理堆栈,否则由函数自己清理堆栈。
可以看到,对于参数个数固定的情况,它类似于stdcall,不定时则类似于cdecl。
5. naked call
是一种比较少见的调用方式,一般高级程序设计语言中不常见。
函数的声明调用方式和实际调用方式必须一致,必然编译器会产生混乱。
函数名字修改规则:
1. C编译时函数名修饰约定规则:
__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_function@8。
__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_function。
__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@function@8。
它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。
2. C++编译时函数名修饰约定规则:
__stdcall调用约定:
(1)以“?”标识函数名的开始,后跟函数名;
(2)函数名后面以“@@YG”标识参数表的开始,后跟参数表;
(3)参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
....
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代
表一次重复;
(4)参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
(5)参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2() -----“?Test2@@YGXXZ”
__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。
VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用。



——————————————————————————————————————————————————————————————————————


4. 名字修饰约定

"C" 或者 "C++" 函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出"C++"重载函数、构造函数、析构函数,又如在汇编代码里调用"C""或"C++"函数等。修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。函数名修饰约定随编译种类(C或C++)和调用约定的不同而不同,下面分别说明。

 

C编译时函数名修饰约定规则:

__stdcall调用约定:

    在输出函数名前加上一个下划线前缀,后面加上一个"@"符号和其参数的字节数,格式为 _functionname@number。

__cdecl调用约定:

    仅在输出函数名前加上一个下划线前缀,格式为 _functionname。

__fastcall调用约定:

    在输出函数名前加上一个"@"符号,后面也是一个"@"符号和其参数的字节数,格式为@functionname@number。

它们均不改变输出函数名中的字符大小写。

 

C++编译时函数名修饰约定规则:

__stdcall调用约定:

以"?"标识函数名的开始,后跟函数名;函数名后面以"@@YG"标识参数表的开始,后跟参数表;

参数表以代号表示:

    X——void,

    D——char,

    E——unsigned char,

    F——short,

    H——int,

    I——unsigned int,

    J——long,

    K——unsigned long,

    M——float,

    N——double,

    _N——bool,

    ....

    PA——表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;

 

参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。

参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。其格式为

    "?functionname@@YG*****@Z" 或 "?functionname@@YG*XZ",

    例如

    int Test1(char *var1,unsigned long)    -----“?Test1@@YGHPADK@Z”

    void Test2()    -----“?Test2@@YGXXZ”(第一个X表示返回类型,第二个X表示参数类型)

 

__cdecl调用约定:

    规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用。

 

__fastcall调用约定:

    规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。

对于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”表示这是一个私有函数。“X”表示返回类型为void,“H”表示参数类型为int类型。

 

成员函数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”。

 

举例

比如动态链接库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也完成了自己的功能,只是函数返回后引发错误。解决的方法也很简单,只要保证两个模块的在编译时设置相同的函数调用约定就行了。

 

现在再假定两个模块在编译的时候都采用__stdcall调用约定,但是a.dll使用C语言的语法编译的(C语言方式),所以a.dll的载入库a.lib中MakeFun函数的名字修饰就是“_MakeFun@4”。b包含了a提供的头文件中MakeFun函数声明,但是由于b采用的是C++语言编译,所以MakeFun在b模块中被按照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"。

5. 单看函数的名字修饰

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值