C/C++函数调用约定

关于 C/C++ 函数调用约定,大多数时候并不会影响程序逻辑,但遇到跨语言编程时,了解一下还是有好处的。

VC 中默认调用是 __cdecl 方式,Windows API 使用 __stdcall 调用方式,在 DLL 导出函数中,为了跟 Windows API保持一致,建议使用 __stdcall 方式。

调用约定跟堆栈清除密切相关。如果写一个汇编函数,给 C/C++ 调用,在 __cdecl 方式下,则汇编函数无需清除堆栈,在 __stdcall 方式下,汇编函数需要在返回(RET)之前恢复堆栈。

C 语言有 __cdecl、__stdcall、__fastcall、naked、__pascal。

C++ 语言有 __cdecl、__stdcall、__fastcall、naked、__pascal、__thiscall,比 C 语言多出一种 __thiscall 调用方式。

 

    在VC中,可以设置默认的调用约定,设置路径为:

Project à Properties à Configuration Properties à C/C++ à Advanced à Call Conversion。

 

下面详细介绍如上六种调用方式:

1、__cdecl

__cdecl调用约定又称为 C 调用约定,是 C/C++ 语言缺省的调用约定。参数按照从右至左的方式入栈,函数本身不清理栈,此工作由调用者负责,返回值在EAX中。由于由调用者清理栈,所以允许可变参数函数存在,如int sprintf(char* buffer,const char* format,...);。

 

2、__stdcall

__stdcall 很多时候被称为 pascal 调用约定。pascal 语言是早期很常见的一种教学用计算机程序设计语言,其语法严谨。参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在EAX中。

 

3、__fastcall

顾名思义,__fastcall 的特点就是快,因为它通过 CPU 寄存器来传递参数。他用 ECX 和 EDX 传送前两个双字(DWORD)或更小的参数,剩下的参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在 EAX 中。

 

4、naked

naked 是一个很少见的调用约定,一般不建议使用。编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果,此调用约定必须跟 __declspec 同时使用。例如定义一个求和程序,如__declspec(naked) int  add(int a,int b);。

 

5、__pascal

这是 pascal 语言的调用约定,跟 __stdcall 一样,参数按照从右至左的方式入栈,函数自身清理堆栈,返回值在EAX中。VC 中已经废弃了这种调用方式,因此在写 VC 程序时,建议使用 __stdcall 代替。

 

6、__thiscall

这是 C++ 语言特有的一种调用方式,用于类成员函数的调用约定。如果参数确定,this 指针存放于 ECX 寄存器,函数自身清理堆栈;如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈。__thiscall 不是关键字,程序员不能使用。参数按照从右至左的方式入栈。

==================================================================================================

都是学习过程中做的笔记。

在编程的过程中,函数是必不可少的基础之一。c语言的程序完全由函数构成,所有的代码都在某一个函数中;pascal区分函数和过程,但是本质是类似的。而对计算机硬件而言CPU只关心一条条的指令,而不是它们是什么样的结构组织。call和ret只是为了函数调用的方便而已,并不是函数存在的证据。最简单的例子就是在木马免杀过程中call+ret和jmp是等价的。因此一种高级语言如何实现函数调用并不受约束,故出现了不同的函数调用规则。

在windows平台上常用的函数调用方式有pascal方式(psscal调用约定),WINAPI方式(StdCall调用约定),c方式(Cdedel调用约定)。

假设有一高级语言函数Message(p1,p2,p3)

 

c方式(Cdedel调用约定):

1.参数从右到左进入堆栈;

2.在函数返回后,调用者要负责清除堆栈,所以这种调用会生成较大的可执行程序。

push p3

push p2

push p1

call Message

add esp,0ch ;之前压入了3个四字节的参数

 

WINAPI方式(StdCall调用约定):

    1.函数从右到左进入堆栈;

   2.被调用的函数在返回之前自行清理堆栈,所以生成的代码较小。

push p3

push p2

push p1

call Message

 

 

pascal方式(psscal调用约定):主要用于win16函数库中,现在基本不用

    1.参数从左到右进入堆栈

    2.被调用的函数在返回前自行清理堆栈

   3.不支持可变参数的函数调用。

push p1

push p2

push p3

call Message

 

此外在windows内核中还有快速调用方式(_fastcall);在C++编译的代码中有this call方式(_thiscall)。


// tt.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>

using namespace std;

int addFun1(int a,int b)
{
	int t = a+b;
	cout<< t<<endl;
	return t;
}

int __stdcall addFun2(int a,int b)
{
	int t = a+b;
	cout<< t<<endl;
	return t;
}

int __cdecl addFun3(int a,int b)
{
	int t = a+b;
	cout<< t<<endl;
	return t;
}

class T
{
public:
// 	int addFun4(int a,int b)
// 	{
// 		int t = a+b;
// 		cout<< t<<endl;
// 		return t;
// 	}
	static int addFun5(int a,int b)
	{
		int t = a+b;
		cout<< t<<endl;
		return t;
	}
};


int main()
{

	int a = T::addFun5(1,2);

	char c = getchar();

	return 0;
}

	int a = addFun1(1,2);
004115AE  push        2    
004115B0  push        1    
004115B2  call        addFun1 (4110CDh) 
004115B7  add         esp,8 
004115BA  mov         dword ptr [a],eax 

	int a = addFun2(1,2);
004115AE  push        2    
004115B0  push        1    
004115B2  call        addFun2 (4110FAh) 
004115B7  mov         dword ptr [a],eax 

	int a = addFun3(1,2);
004115AE  push        2    
004115B0  push        1    
004115B2  call        addFun3 (41105Fh) 
004115B7  add         esp,8 
004115BA  mov         dword ptr [a],eax 

	T t;
	int a = t.addFun4(1,2);
0041158E  push        2    
00411590  push        1    
00411592  lea         ecx,[t]     //反汇编程序时只要发现先push ..lea ...然后又call,该调用很大可能是成员函数的调用
00411595  call        T::addFun4 (4111EFh) 
0041159A  mov         dword ptr [a],eax 

	int a = T::addFun5(1,2);
0041158E  push        2    
00411590  push        1    
00411592  call        T::addFun5 (4111F4h) 
00411597  add         esp,8 
0041159A  mov         dword ptr [a],eax 
调用测试截图:


=======================================================================================================================

VC默认为__stdcall, 
BCB默认为__cdecl, 
Delphi默认为__fastcall。 
由于BCB使用Delphi的VCL库, 
所以也必须使用__fastcall。

 

关键字 调用规则 参数传递方向 返回 参数寄存器 堆栈的清除
__cdecl C调用规则 从右向左 EAX 调用者
__fastcall 寄存器 从左向右 EAX EAX、EBX、ECX 被调用者
__stdcall Win32标准 从右向左 EAX 被调用者
__pascal Pascal 从左向右 EAX 被调用者
__msfastcall Ms寄存器 从右向左 EAX/EDX ECX、EDX 被调用者

 

 

摘自《面向状态的程序设计与C++   Builder》第四章第一节       Lewolf著 
1.4.1 __cdecl、_cdecl、cdecl关键字 
这是在C++   Builder中特有的关键字,__cdecl、_cdecl、cdecl可以用来修饰变量或者函数,其含义是指定的变量或者函数使用C语言的调用规则和命名规则。在C++   Builder中C调用规则是缺省的设置,可以通过Project的Option菜单在Advanced   Compiler标签中来改变缺省设置。 
对于C命名规则的变量或函数,具有大小些敏感、编译后变量名称前下划线前导符,这与Pascal语言是不一样的。对于函数,C调用规则要求使用堆栈传递参数,参数传递的顺序为自右向左依次压入堆栈,由调用者自行清除堆栈,C调用规则允许向函数传递不定参数,但这种调用方式系统需要更多的开销来完成参数的匹配,除非特殊用途一般尽量不要使用,这种调用方式最典型的例子是控制台程序中的main函数,程序运行时可以不带参数,也可以带多个参数。 
在一个独立的程序中,采用那种调用规则或者命名规则影响并不十分大,但是在多种语言混合编程,或者编写带有数据输出或函数输出的可执行模块(可执行文件和动态链接库)时,命名规则和调用规则显得尤为重要,使用不当则可能导致程序无法正常运行,甚至使系统崩溃。 
__cdecl、_cdecl、cdecl的使用语法如下: 
cdecl   <data/function   definition> ; 
_cdecl   <data/function   definition> ; 
__cdecl   <data/function   definition> ; 
例如: 
int   cdecl   I;                     //编译后标识符为“_I” 
void   __cdecl   Fun();   //编译后函数名为“_Fun” 
1.4.2 __fastcall、_fastcall关键字 
__fastcall和_fastcall也是C++   Builder中特有的关键字,只能用于修饰函数,其作用是指定函数使用“寄存器”调用规则,使用语法如下: 
return-type   _fastcall   function-name(parm-list) 
return-type   __fastcall   function-name(parm-list) 
例如: 
void   __fastcall   GetName(int   Index); 
使用“寄存器”调用规则的函数,其参数传递顺序为自左向右依次传递,并且将前三个参数尽可能的使用EAX、EBX、ECX这三个寄存器传递,对于前三个参数中浮点类型、结构类型等超过四个字节的变量,和第四个及以后的参数则采用堆栈来传递,因此采用“寄存器”调用规则的函数只能传递固定数量的参数。 
在C++   Builder中,所有属于VCL的成员函数,必须是__fastcall类型,编译器将“寄存器”调用规则和C调用规则、Pascal调用规则以及Win32的标准等其它调用规则是同等对待的,因此__fastcall关键字不能和__cdecl、__pascal、__stdcall等关键字联合使用,也不能和__export关键字同时使用(实际上使用__fastcall规则的函数是允许输出的,只是这种调用规则编写的dll或可执行模块只能被C++   Builder或者Delphi开发的程序载入并调用输出的函数)。 
被指定为“寄存器”调用的函数会在编译时被冠以前导符@,这将使得C及C++的函数命名过于混乱,但是这种调用规则有很高的执行效率,因此在C++   Builder中几乎所有VCL的成员函数都使用这种调用规则。 
但是__fastcall和_fastcall是C++   Builder之前的C++编译器所没有的调用规则,为了保证兼容性,可以使程序连接以前版本的编译器生成的库文件,C++   Builder提供了-VC命令行选项,缺省状态下,该选项是关闭的,当需要时,可以通过命令行编译并选择-VC选项。 
1.4.3 __msfastcall、__msreturn关键字 
__msfastcall和__msreturn也是C++   Builder中特有的关键字,使用这两个关键字可以为程序提供和Microsoft兼容的“寄存器调用”规则以及和Microsoft兼容的函数返回的规则。 
被__msfastcall修饰的函数,其参数传递顺序为前两个参数如果长度小于4字节,则分别使用ECX和EDX寄存器传递,其余的参数和前两个不能使用寄存器传递的参数则按照从右向左的顺序通过堆栈来传递,堆栈由被调用函数负责清除。 
__msreturn是使用Microsoft兼容的“寄存器调用”返回规则,在这种返回规则中,返回值的长度在4~8个字节时,将采用EAX/EDX寄存器返回函数值。 
1.4.4 __pascal、_pascal、pascal关键字 
__pascal、_pascal、pascal关键字表示一个变量或者函数遵循Pascal的调用和命名规则。 
在Pascal的命名规则中,对大小写并不敏感,而且不象C调用规则,会在变量和函数明成前面加前导符。Pascal调用规则中,参数的传递是按照从左到右的顺序依次压入堆栈,并且由被调用函数自行清除堆栈。 
Pascal调用规则破坏了C语言的本身的调用体系,同时也带来了更多的灵活性,被表示为__pascal调用规则的函数,编译后的目标文件中,名称会被全部转换为大写字母,在Pascal语言中,过程和函数的书写是对大小些不敏感的,但是在C++   Builder中采用Pascal调用规则的函数,在C++源文件中仍然需要遵循C语言中对大小些必须严格一致的要求,Pascal调用规则的大小写不敏感性只能体现在动态链接库、或者输出、输入函数以及数据的时侯。也正是这样的原因,对一些采用Object   Pascal编写的第三方控件或者书写不是很规范的Pascal程序代码,使用C++   Builder编译的时候,会出现找不到函数原型的错误,实际上可能只是程序中函数名称的大小些不一致造成的。 
1.4.5 __stdcall、_stdcall关键字 
在Windows程序设计中,有一种Win32的标准调用规则,基本上所有的Windows   API函数都采用了这种Win32的标准调用规则,在讲__declspec(nothrow)关键字时的例子,其中宏定义就是C++   Builder中winuser.h宏定义。Win32标准调用在C++   Builder中以__stdcall、_stdcall关键字来标识。 
在__stdcall、_stdcall调用规则中,函数的参数是按照从右向左的顺序依次压入堆栈,所有的参数均采用堆栈传递,而且是被调用函数负责清除堆栈。在参数的传递顺序上,Win32标准调用规则和C调用规则一样,从右向左,但和C调用规则不同的是,Win32的标准调用要求调用者必须严格按照被调用函数的参数数量及类型进行参数传递。 
关于C++   Builder中函数调用规则以及命名规则的详细内容,读者参考相关书籍,这里不做过多讲解,下表给出了C++   Builder中几种调用规则的一个简单比较,具有汇编语言基础的读者可以参考本书光盘中相关代码自行分析区别。 
关键字   调用规则 参数传递方向 返回 参数寄存器 堆栈的清除 
__cdecl   C调用规则 从右向左 EAX 无 调用者 
__fastcall 寄存器 从左向右 EAX EAX、EBX、ECX 被调用者 
__stdcall Win32标准 从右向左 EAX 无 被调用者 
__pascal Pascal 从左向右 EAX 无 被调用者 
__msfastcall Ms寄存器 从右向左 EAX/EDX ECX、EDX 被调用者


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值