C++中的函数调用约定主要针对三个问题:
1、参数传递的方式(是否采用寄存器传递参数、采用哪个寄存器传递参数、参数压桟的顺序等);
2、函数调用结束后的栈指针由谁恢复(被调用的函数恢复还是调用者恢复);
3、函数编译后的名称;
对实例代码有几点说明(使用的平台为vs2012+intel x86架构)
1、栈顶指针即为esp;
2、int型占32字节内存;
3、桟顶为小地址端,栈底为大地址端,因此出栈需要增大esp;
下面对C++中见到的stdcall、cdecl、fastcall和thiscall做简要说明。
使用stdcall的函数声明方式为:int __stdcall function(int a,int b)
stdcall的调用约定意味着:
1)采用桟传递全部参数,参数从右向左压入栈;
2)被调用函数负责恢复栈顶指针 ;
3) 函数名自动加前导的下划线,后面是函数名,之后紧跟一个@符号,其后紧跟着参数的尺寸,例如_function@4;
下面给出实例:
int _stdcall funb(int p,int q) //声明为stdcall方式
{
return p-q;
}
e=funb(3,4);
012C42F7 push 4 //参数q入栈
012C42F9 push 3 //参数p入栈
012C42FB call funb (012C1244h) //调用函数
012C4300 mov dword ptr [e],eax //调用者没有处理esp
int _stdcall funb(int p,int q)
{
012C3D80 push ebp
012C3D81 mov ebp,esp //将esp保存入ebp中
012C3D83 sub esp,0C0h
012C3D89 push ebx
012C3D8A push esi
012C3D8B push edi
012C3D8C lea edi,[ebp-0C0h]
012C3D92 mov ecx,30h
012C3D97 mov eax,0CCCCCCCCh
012C3D9C rep stos dword ptr es:[edi]
return p-q;
012C3D9E mov eax,dword ptr [p]
012C3DA1 sub eax,dword ptr [q]
}
012C3DA4 pop edi
012C3DA5 pop esi
012C3DA6 pop ebx
012C3DA7 mov esp,ebp
012C3DA9 pop ebp
012C3DAA ret 8 //注意此处,用被调函数负责恢复esp
以上面函数为例,参数q首先被压栈,然后是参数p(参数从右向左入栈),
然后利用call调用函数,
而在编译时,这个函数的名字被翻译成_funb@8,其中8代表参数为8个字节(2个int型变量)。
另外,stdcall可以用于类成员函数的调用,这种情况下唯一的不同就是,所有参数从右向左依次入栈后,this指针会最后一个入栈。下面给出示例。
class A
{
public:
A(int a)
{
this->val=a;
}
int _stdcall fun(int par) //类成员函数采用stdcall
{
return val-par;
}
private:
int val;
};
函数调用代码如下:
A t(3);
int d,e,f,g;
g=t.fun(4);
函数调用代码编译后为:
g=t.fun(4);
00DB4317 push 4 //参数4入栈
00DB4319 lea eax,[t]
00DB431C push eax //this指针入栈,下面会验证eax内容即为A的对象的地址
00DB431D call A::fun (0DB1447h)
00DB4322 mov dword ptr [g],eax
编译后的代码为:
int _stdcall fun(int par)
{
00DB3CF0 push ebp
00DB3CF1 mov ebp,esp
00DB3CF3 sub esp,0C0h
00DB3CF9 push ebx
00DB3CFA push esi
00DB3CFB push edi
00DB3CFC lea edi,[ebp-0C0h]
00DB3D02 mov ecx,30h
00DB3D07 mov eax,0CCCCCCCCh
00DB3D0C rep stos dword ptr es:[edi]
return val-par;
00DB3D0E mov eax,dword ptr [this]
00DB3D11 mov eax,dword ptr [eax]
00DB3D13 sub eax,dword ptr [par]
}
00DB3D16 pop edi
00DB3D17 pop esi
00DB3D18 pop ebx
00DB3D19 mov esp,ebp
00DB3D1B pop ebp
00DB3D1C ret 8 //由被调用函数负责恢复栈顶指针,由于参数为int型变量(4字节)和一个指针(32为,4字节),共8字节
下面验证入栈时eax中的内容为A对象的地址。
入栈时eax内容如下,为0x0035F808。
找到内存中0x0035F808的内容,为3,。
再看main函数中实例化对象的代码。
可见,this指针正是通过eax入栈。
由此可见,用于类成员函数时,唯一的不同就是在参数入栈完毕后,this指针会最后一个入栈。int function (int a ,int b) //不加修饰就是采用默认的C调用约定
int _cdecl function(int a,int b) //明确指出采用C调用约定
cdecl调用方式规定:
1、采用桟传递参数,参数从右向左依次入栈;
2、由调用者负责恢复栈顶指针;
3、在函数名前加上一个下划线前缀,格式为_function;
要注意的是,调用参数个数可变的函数只能采用这种方式(如printf)。
下面给出实例。
int _cdecl funa(int p,int q) //采用cdecl方式
{
return p-q;
}
调用处的代码编译为:
d=funa(3,4);
012C42E8 push 4
012C42EA push 3
012C42EC call funa (012C1064h) //调用funca
012C42F1 add esp,8 //调用者恢复栈顶指针esp
012C42F4 mov dword ptr [d],eax //返回值传递给变量d
函数编译后的代码为:
int _cdecl funa(int p,int q)
{
012C3D40 push ebp
012C3D41 mov ebp,esp
012C3D43 sub esp,0C0h
012C3D49 push ebx
012C3D4A push esi
012C3D4B push edi
012C3D4C lea edi,[ebp-0C0h]
012C3D52 mov ecx,30h
012C3D57 mov eax,0CCCCCCCCh
012C3D5C rep stos dword ptr es:[edi]
return p-q;
012C3D5E mov eax,dword ptr [p]
012C3D61 sub eax,dword ptr [q]
}
012C3D64 pop edi
012C3D65 pop esi
012C3D66 pop ebx
012C3D67 mov esp,ebp
012C3D69 pop ebp
012C3D6A ret //注意此处,被调函数没有恢复esp
因此,stdcall与cdecl的区别就是谁负责恢复栈顶指针和编译后函数的名称问题。
cedcal同样可以用于类成员函数的调用。此时,cdedl与stdcall的区别在于由谁恢复栈顶指针。
类定义如下:
class A
{
public:
A(int a)
{
this->val=a;
}
int _cdecl fun(int par) //采用cedcl方式
{
return val-par;
}
private:
int val;
};
调用代码编译如下:
g=t.fun(4);
013D4317 push 4
013D4319 lea eax,[t]
013D431C push eax //先入栈参数4,后入栈this指针
013D431D call A::fun (013D144Ch)
013D4322 add esp,8 //由调用者恢复栈顶指针
013D4325 mov dword ptr [g],eax
采用fasecall的函数声明方式为:
int __fastcall function(int a,int b)
fastcall调用约定意味着:示例代码如下:
int __fastcall func(int p,int q,int r) //采用fastcall
{
return p-q-r;
}
调用代码如下:
f=func(3,4,5);
00E74303 push 5 //第三个参数r压桟
00E74305 mov edx,4 //p q通过ecx和edx传递
00E7430A mov ecx,3
00E7430F call func (0E71442h)
00E74314 mov dword ptr [f],eax //调用者不负责恢复栈顶指针esp
函数编译后的代码如下:
int __fastcall func(int p,int q,int r)
{
00E73DC0 push ebp
00E73DC1 mov ebp,esp
00E73DC3 sub esp,0D8h
00E73DC9 push ebx
00E73DCA push esi
00E73DCB push edi
00E73DCC push ecx
00E73DCD lea edi,[ebp-0D8h]
00E73DD3 mov ecx,36h
00E73DD8 mov eax,0CCCCCCCCh
00E73DDD rep stos dword ptr es:[edi]
00E73DDF pop ecx
00E73DE0 mov dword ptr [q],edx
00E73DE3 mov dword ptr [p],ecx
return p-q-r;
00E73DE6 mov eax,dword ptr [p]
00E73DE9 sub eax,dword ptr [q]
00E73DEC sub eax,dword ptr [r]
}
00E73DEF pop edi
00E73DF0 pop esi
00E73DF1 pop ebx
00E73DF2 mov esp,ebp
00E73DF4 pop ebp
}
00E73DF5 ret 4 //恢复栈顶指针,由于只有一个参数r被压桟,因此esp+4即可
可以看到,fasecall利用寄存器ecx与edx传递参数,避免了访存带来的开销。适合少量参数提高效率的场合。
thiscall意味着:
1、采用桟传递参数,参数从右向左入栈。如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针