通常在使用VC进行函数定义时会指定该函数调用方式诸如 int __stdcall max(int a int b) { return a>b?a:b; } int __cdecl min(int a int b) { return a<b?a:b; } bool __fastcall equal(int a int b) { return a=b?true:false; } 首先让我们来分个类调用方法分为两大类另加一个较特殊的__thiscall 第一类__stdcall类 别名WINAPICALLBACKPASCAL该类特点是主调函数负责参数入栈由函数本身负责栈的恢复 第二类__cdecl类 别名C/C++中默认调用方式若你定义函数未指定函数调用约定(Calling Conventions)例如在VC中下面两个函数的调用约定是等价的 int max(int a int b) { return a>b?a:b; } int __cdecl min(int a int b) { return a<b?a:b; } 该类调用约定的特点是由主调函数负责参数入栈并由主调函数负责线的恢复 第三类__thiscall 该类比较特殊只用于类成员函数调用你甚至不能强制指定这个函数调用约定它是由C/C++编译器自动添加的在C/C++中类成员函数会默认传入一个this指针对于此在默入情况下C/C++中类成员函数通过此类调用约定来指定this指针 接着介绍一下__thiscall__thiscall是关于类的一种调用方式 它与其他调用方式的最大区别是 __thiscall对每个函数都增加了一个类指针参数 class aa { void bb(int cc) }; 实际上bb的函数原形是void bb(aa &this int cc) __cdecl的调用方式介绍 C和C++缺省调用方式 例子 void Input( int &mint &n)/*相当于void __cdecl Input(int &mint &n)*/ 以下是相应的汇编代码 lea eax[ebp] ;取[ebp]地址(ebp)存到eax B push eax ;然后压栈 C lea ecx[ebp] ;取[ebp]地址(ebp)存到ecx F push ecx ;然后压栈 call @ILT+(Input) (a)然后调用Input函数 add esp ;恢复栈 从以上调用Input函数的过程可以看出在调用此函数之前首先压栈ebp然后压栈ebp然后调用函数Input最后Input函数调用结束后利用esp+恢复栈由此可见在C语言调用中默认的函数修饰_cdecl由主调用函数进行参数压栈并且恢复堆栈 下面看一下地址ebp和ebp是什么? 在VC的VIEW>debug windows>Registers显示寄存器变量值然后在选debug windows>Memory输入ebp的值和ebp的值(或直接输入ebp和)看一下这两个地址实际存储的是什么值实际上是变量n 的地址(ebp)m的地址(ebp) 由此可以看出在主调用函数中进行实参的压栈并且顺序是从右到左另外由于实参是相应的变量的引用也证明实际上引用传递的是变量的地址(类似指针) 总结在C或C++语言调用中默认的函数修饰_cdecl由主调用函数进行参数压栈并且恢复堆栈实参的压栈顺序是从右到左最后由主调函数进行堆栈恢复由于主调用函数管理堆栈所以可以实现变参函数另外命名修饰方法是在函数前加一个下划 线(_) _stdcall调用约定介绍实际上就是PASCALCALLBACKWINAPI 例子 void WINAPI Input( int &mint &n) 看一下相应调用的汇编代码 lea eax[ebp] B push eax C lea ecx[ebp] F push ecx call @ILT+(Input) (a) 从以上调用Input函数的过程可以看出在调用此函数之前首先压栈ebp然后压栈ebp然后调用函数Input在调用函数Input之后没有相应的堆栈恢复工作(为其它的函数调用所以我没有列出)下面再列出Input函数本身的汇编代码(实际此函数不大但做汇编例子还是大了些大家可以只看前和后中间代码与此例子无关) : void WINAPI Input( int &mint &n) : { push ebp mov ebpesp sub esph push ebx push esi push edi lea edi[ebph] C mov ecxh mov eaxCCCCCCCCh rep stos dword ptr [edi] : int si; : : while() mov eax D test eaxeax F je Input+Ch (d) : { : printf(\nPlease input the first number m:) push offset string \nPlease input the first number m… (b) A call printf () F add esp : scanf(%d&m) mov ecxdword ptr [ebp+] push ecx push offset string %d (b) B call scanf (f) add esp : : if ( m= s ) B mov eaxdword ptr [ebp+] B mov ecxdword ptr [eax] B cmp ecxdword ptr [ebp] BB jl Input+AFh (bf) : break; BD jmp Input+Ch (d) : else : printf( m < n*(n+)/Please input again!\n) BF push offset string m < n*(n+)/Please input agai… () C call printf () C add esp : } CC jmp Input+h () : : } D pop edi D pop esi D pop ebx D add esph D cmp ebpesp D call __chkesp (b) DE mov espebp E pop ebp E ret 之后我们看到在函数末尾部分有ret 明显是恢复堆栈由于在位C++中变量地址为个字节(int也为个字节)所以弹栈两个地址即个字节由此可以看出在主调用函数中负责压栈在被调用函数中负责恢复堆栈因此不能实现变参函数因为被调函数不能事先知道弹栈数量但在主调函数中是可以做到的因为参数数量由主调函数确定 下面再看一下ebp和ebp这两个地址实际存储的是什么值ebp地址存储的是n 的值ebp 存储的是m的值说明也是从右到左压栈进行参数传递 总结_stdcall在主调用函数中负责压栈在被调用函数中负责弹出堆栈中的参数并且负责恢复堆栈因此不能实现变参函数参数传递是从右到左另外命名修饰方法是在函数前加一个下划线(_)在函数名后有符号(@)在@后面紧跟参数列表中的参数所占字节数(进制)如void Input(int &mint &n)被修饰成_Input@ 对于大多数api函数以及窗口消息处理函数皆用CALLBACK所以调用前主调函数会先压栈然后api函数自己恢复堆栈 如 push edx push edi push eax push ebx call getdlgitemtexta 最后在SDK中输出API函数的时候经常会利用WINAPI对函数进行约定WINAPI在WIN中它被定义为__stdcall 函数调用约定有多种这里简单说一下 __stdcall调用约定相当于位动态库中经常使用的PASCAL调用约定在位的VC++中PASCAL调用约定不再被支持(实际上它已被定义为__stdcall除了__pascal 外__fortran和__syscall也不被支持)取而代之的是__stdcall调用约定两者实质上是一致的即函数的参数自右向左通过栈传递被调用的函数在返回前清理传送参数的内存栈但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明) _stdcall是Pascal程序的缺省调用方式通常用于Win Api中函数采用从右到左的 压栈方式自己在退出时清空堆栈VC将函数编译后会在函数名前面加上下划线前缀在函数名后加上@和参数的字节数 C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈由调用者把参数弹出栈对于传送参数的内存栈是由调用者来维护的(正因为如此实现可变参数的函数只能使用该调用约定)另外在函数名修饰约定方面也有所不同 _cdecl是C和C++程序的缺省调用方式每一个调用它的函数都包含清空堆栈的代码所以产生的可执行文件大小会比调用_stdcall函数的大函数采用从右到左的压栈方式VC将函数编译后会在函数名前面加上下划线前缀是MFC缺省调用约定 __fastcall调用约定是人如其名它的主要特点就是快因为它是通过寄存器来传送参数的(实际上它用ECX和EDX传送前两个双字(DWORD)或更小的参数剩下的参数仍旧自右向左压栈传送被调用的函数在返回前清理传送参数的内存栈)在函数名修饰约定方面它和前两者均不同 _fastcall方式的函数采用寄存器传递参数VC将函数编译后会在函数名前面加上@前缀在函数名后加上@和参数的字节数 thiscall仅仅应用于C++成员函数this指针存放于CX寄存器参数从右到左压thiscall不是关键词因此不能被程序员指定 naked call采用的调用约定时如果必要的话进入函数时编译器会产生代码来保存ESIEDIEBXEBP寄存器退出函数时则产生代码恢复这些寄存器的内容naked call不产生这样的代码naked call不是类型修饰符故必须和_declspec共同使用 |