欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

函数调用的三种方式 __cdecl、__stdcall、__fastcall

程序员文章站 2022-06-08 12:18:06
...

__cdecl、__stdcall、__fastcall是C/C++里中经常见到的三种函数调用方式。

__cdecl是C/C++默认的调用方式

__stdcall是windows API函数的调用方式,只不过我们在头文件里查看这些API的声明的时候是用了WINAPI的宏进行代替了,而这个宏其实就是__stdcall了。

函数的调用过程是通过函数栈帧的不断变化实现的:

函数调用的三种方式 __cdecl、__stdcall、__fastcall 

 

函数的调用,涉及参数传递,返回值传递,调用后返回,这都是通过栈的变化来实现的,对于三种调用约定而言:

__cdecl:

C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡。

__stdcall:

windows API默认方式,参数从右向左入栈,被调函数负责栈平衡。

__fastcall:

快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰“__fastcall”。

 

#include <stdio.h>

class Point
{
public:
	Point(int x, int y)
	{
		nx = x;
		ny = y;
		nTest = nx + ny;
	}
	~Point(){}
	int nx;
	int ny;
	int nTest;
};

void __cdecl Fun1(int x, int y)
{
	Point pt(0, 0);
	pt.nx = x;
	pt.ny = y;
}

void __fastcall Fun2(int x, int y)
{
	Point pt(0, 0);
	pt.nx = x;
	pt.ny = y;
}

void __stdcall Fun3(int x, int y)
{
	Point pt(0, 0);
	pt.nx = x;
	pt.ny = y;
}

int main()
{
	Fun1(2, 5);//__cdecl
	Fun2(2, 5);//__fastcall
	Fun3(2, 5);//__stdcall
	return 0;
}

 

 先打断点,进行反汇编

函数调用的三种方式 __cdecl、__stdcall、__fastcall

 函数调用的三种方式 __cdecl、__stdcall、__fastcall

__cdecl按照参数从右向左的方式进入栈区,注意Fun1()和Fun3()的区别,Fun1()在call Fun1()之后执行了add esp,8。这一操作正是我们前面所说的进行栈的平衡。

调用函数之前连续进行了两次push操作将函数所需的实参5和2先后压入了栈区,调用完成后,我们需要恢复调用前的状态,则需调整栈顶指针esp的位置,这一工作由谁来完成就决定了两种函数调用方式__cdecl(主调函数完成)和__stdcall(被调函数完成)的区别。上图我们看到了__cdecl中由主调函数完成了,那么__stdcall呢,在被调函数Fun3()中,转向被调函数结尾处的代码,我们看到了这一句:

函数调用的三种方式 __cdecl、__stdcall、__fastcall

 fun1()结尾处

函数调用的三种方式 __cdecl、__stdcall、__fastcall

 这个ret指令后面跟没跟值就决定了函数返回是栈指针ESP需要增加的量。这样,不需要主调函数再调用add指令为ESP操作平衡栈区,节约了程序的开销,一条指令开销小,如果十万百万个这样的调用,这个开销就明显了。

__fastcall,如前面图看到的调用时并未使用push指令向栈里传参数,而是使用了

函数调用的三种方式 __cdecl、__stdcall、__fastcall

两条指令。这样直接将参数传入寄存器,被调函数在执行的时候直接从寄存器取值即可,省去了从栈里取出来给寄存器,再从寄存器取出来放入内存。

 

ecx寄存器经常作为计数和C++里this指针的传递媒介。ecx做计数器时,需要将ecx中存储的实参先压入栈区,计数操作完成后再pop出来。如此一来,这个fastcall倒显得不那么fast了。