原文地址:http://blog.sina.com.cn/s/blog_bad31d930102wuk4.html(这里备注下:可能上面还有一个原文链接,是csdn转载的文章类型自动加上去的,我在csdn没加这个功能之前,也会先把转载的原文地址列在最前面,这里忽略csdn的更改,保持习惯吧。)
以下是文章内容:
研究下_cdecl _stdcal _thiscall 等调用约定,以及成员函数与普通函数调用上的区别.
前奏:
先说下几个asm寄存器以及命令:
EAX一般用来存放返回值
ECX一般用来存放this
rep指令会将他后面的指令重复N次, N = ECX
stos dword ptr es:[edi] 将EAX里的内容复制到es:[edi]指向的内存空间,完毕后edi += 4
如果设置了direction flag, 那么edi会在该指令执行后 edi -= 4,
call 标号 该指令将 IP(或CS+IP) 压栈(ESP -=4),然后转移到标号出执行
ret指令 从栈中恢复IP(ESP +=4),
ret 8 指令,从栈中恢复IP(ESP +=4),然后再将栈中8个字节弹出(ESP +=8),也就是说
此命令使ESP自增12
我们先写一段简单的控制台代码,再反编译成汇编,通过阅读汇编代码,可以彻底看明白
上述区别。源代码很简单:
InClass_CallType.h (C++ Code)
#pragma once
class CallType {
public:
int DefaultCall(int a, int b) {
this->data = a + b;
return this->data;
}
int _cdecl CdeclCall(int a, int b) {
this->data = a + b;
return this->data;
}
int _stdcall StdCall(int a, int b) {
this->data = a + b;
return this->data;
}
int _fastcall FastCall(int a, int b) {
this->data = a + b;
return this->data;
}
int __thiscall ThisCall(int a, int b) {
this->data = a + b;
return this->data;
}
private:
int data;
};
OutClass_CallType.h (C++ Code)
#pragma once
int DefaultCall(int a, int b) {
return a + b;
}
int _cdecl CdeclCall(int a, int b) {
return a + b;
}
int _stdcall StdCall(int a, int b) {
return a + b;
}
int _fastcall FastCall(int a, int b) {
return a + b;
}
CallType.cpp (C++ Code)
// CallType.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "InClass_CallType.h"
#include "OutClass_CallType.h"
int main()
{
CallType callType;
callType.DefaultCall(1, 2);
callType.CdeclCall(3, 2);
callType.StdCall(1, 3);
callType.ThisCall(2, 2);
callType.FastCall(2, 3);
DefaultCall(1, 2);
CdeclCall(3, 2);
StdCall(1, 3);
FastCall(2, 3);
return 0;
}
分别定义了几种常见的调用约定,涵盖类内的跟类外的。在分析调用约定之前,先借汇编代码复习下
函数的调用过程。
asm
StdCall(1, 3);
00B619EA push 3
00B619EC push 1
00B619EE call StdCall (0B61032h)
StdCall:
int _stdcall StdCall(int a, int b) {
00B61890 push ebp
00B61891 mov ebp,esp
00B61893 sub esp,0C0h
00B61899 push ebx
00B6189A push esi
00B6189B push edi
00B6189C lea edi,[ebp-0C0h]
00B618A2 mov ecx,30h
00B618A7 mov eax,0CCCCCCCCh
00B618AC rep stos dword ptr es:[edi]
return a + b;
00B618AE mov eax,dword ptr [a]
00B618B1 add eax,dword ptr [b]
}
00B618B4 pop edi
00B618B5 pop esi
00B618B6 pop ebx
00B618B7 mov esp,ebp
00B618B9 pop ebp
00B618BA ret 8
调用者首先将两个参数按照从右往左的顺序压入栈中,然后call StdCall;
函数(StdCall)的前两行一定是:
push ebp
mov ebp,esp
第一行(push ebp )汇编执行完毕后,栈空间的状态是这样的:
(执行完第二行),ebp+8即为参数a在栈中的位置,ebp+0xC即为参数b在栈中的位置。
ESP指向栈顶:距离参数a的距离为8(隔了一个ebp)
执行完第二行后,EBP = ESP,则EBP(的值)距离参数a的距离为也8。
接下来的三行是ebx、edi、esi三个寄存器的入栈。
再接下来的四行是初始化临时变量空间,全部初始化成0X0ccccccc
(debug模式下都这么干,据说跟int3中断有关)
然后是计算 a+b、然后将返回值存入eax
然后将esi、edi、ebx三个寄存器出栈。
接下来恢复esp、ebp、返回
可以这样来看上面的代码:”对称“
4、5 跟 21、22对称
7、8、9 跟 18、19、20对称
这两处对称保证了在调用函数前跟调用函数后的栈一致的。
接下来的几行,其作用是用0X0CCCCCCC初始化临时变量空间,rep以及stos命令的用法见本文开头。
接下来回到正题,看_cdecl 与 _stdcall的区别:
tmpfdsa.asm * (C++ Code)
CdeclCall(3, 2);
00B619DE push 2
00B619E0 push 3
00B619E2 call CdeclCall (0B61244h)
00B619E7 add esp,8
StdCall(1, 3);
00B619EA push 3
00B619EC push 1
00B619EE call StdCall (0B61032h)
_cdecl的函数,由调用者清理栈中参数——换句话说,由调用者负责还原esp——在函数调用结束后使用:
add esp,8 (该命令在调用者调用完毕后的一行)
命令恢复esp;
_stdcall的函数,由函数自身清理栈中参数——换句话说,由函数(被调用者)负责还原esp——ret 8(该命令在函数结尾处)
接下来看下类内的函数调用(成员函数):
tmpfdsa.asm * (C++ Code)
callType.DefaultCall(1, 2);
00B6198E push 2
00B61990 push 1
00B61992 lea ecx,[callType]
00B61995 call CallType::DefaultCall (0B610F5h)
int DefaultCall(int a, int b) {
00B617B0 push ebp
00B617B1 mov ebp,esp
00B617B3 sub esp,0CCh
00B617B9 push ebx
00B617BA push esi
00B617BB push edi
00B617BC push ecx
00B617BD lea edi,[ebp-0CCh]
00B617C3 mov ecx,33h
00B617C8 mov eax,0CCCCCCCCh
00B617CD rep stos dword ptr es:[edi]
00B617CF pop ecx
00B617D0 mov dword ptr [this],ecx
this->data = a + b;
00B617D3 mov eax,dword ptr [a]
00B617D6 add eax,dword ptr [b]
00B617D9 mov ecx,dword ptr [this]
00B617DC mov dword ptr [ecx],eax
return this->data;
00B617DE mov eax,dword ptr [this]
00B617E1 mov eax,dword ptr [eax]
}
00B617E3 pop edi
00B617E4 pop esi
00B617E5 pop ebx
00B617E6 mov esp,ebp
00B617E8 pop ebp
00B617E9 ret 8
发第四行将this指针存入ECX,在第14行将ECX入栈,第19行出栈、开始使用。
从第20行开始,代码变得不是那么容易理解了:感觉第20行、第24行完全是多余的。。。
抛去这点,将this指针存入ECX寄存器还是很容易看出来的。
最后一行ret 8, 说明_thiscall也是由函数本身清理参数栈。