__cdecl、__stdcall、__fastcall是C/C++中经常见到的三种函数调用方法。其中__cdecl是C/C++默认的调用方式,__stdcall是windowsAPI函数的调用方式,但是我们在查看头文件中这些API的声明时,却使用了WINAPI宏,而这个宏是实际上是__stdcall。相信大家应该对三种调用方式的区别有所了解。本文主要从实例和汇编的角度解释这些差异的表现形式,使对它们差异的理解能够从理论过渡到实践。我们知道函数的调用过程是通过函数栈帧的不断变化来实现的:函数的调用涉及参数传递、返回值传递、调用后返回,这些都是通过栈的变化来实现的。对于三种调用约定而言:__cdecl:C/C++默认模式,参数从右向左入栈,调用函数负责栈平衡。__stdcall:WindowsAPI默认方法,参数从右到左入栈,被调用函数负责栈平衡。__fastcall:快速调用方法。所谓快,这种方法选择先从寄存器(ECX和EDX)传参数,然后从右到左从栈中传剩余的参数。因为栈位于内存区,而寄存器位于CPU,访问方式比内存快,所以称为“__fastcall”。让我们从示例中看一下这三个调用约定。先来看一个再简单不过的程序:三个函数的内容是一样的,不同的是使用了三种调用方式。下面看一下main函数调用三个函数时的汇编代码:__cdecl根据上面的参数从右到左进入栈区,注意Fun1()和Fun3()的区别,Fun1()在调用Fun1()后执行addesp,8。这个操作就是我们前面说的平衡栈。在调用函数之前,连续进行两次入栈操作,将函数需要的实参5和2依次压入栈区。调用完成后,我们需要恢复调用前的状态,需要调整栈顶指针esp的位置。一旦工作完成,它就判断__cdecl(调用函数完成)和__stdcall(被调用函数完成)两种函数调用方式的区别。上图中,我们看到__cdecl是由调用函数完成的,那么__stdcall呢,在被调用函数Fun3()中,翻到被调用函数最后的代码,我们看到这句话:ThenattheendFun1()那又如何呢?看,ret指令后面没有值,这就决定了函数返回的量就是栈指针ESP需要增加的量。这样调用函数就不需要调用add指令来为ESP操作平衡栈区,节省了程序的开销。一条指令的开销很小。如果有数十万个这样的调用,开销将是显而易见的。说完__cdecl和__stdcall,我们再来看看__fastcall。如上图所示,调用时并没有使用push指令将参数入栈,而是使用了两条指令:movedx,5movecx,2。这样就直接将参数传入了寄存器,并且被调用的函数在执行时可以直接从寄存器中取值,省去了从栈中取到寄存器,再从寄存器中取出放入内存的过程。然而,顺便说一句,ecx寄存器通常用作C++中计数和this指针的传输介质。这样的话,到底是什么情况,我们下次分析C++的operatornew时再讨论。当ecx作为计数器使用时,需要先将ecx中存放的实际参数压入栈区,等计数完成后再弹出。这样一来,这个fastcall就没有那么快了。当然,上面提到的这些操作都是编译器在后台帮我们完成的,开发者不需要关心这些操作,对我们来说是透明的。但是,知其所以然,知其所以然,方能立于不败之地!
