struct MyStruct
{
int value1;
__int64 value2;
bool value3;
};
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,"Courier New",courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); }
这时,在调用foo函数时参数的入栈过程会有所不同,如下图所示:
图10
caller会在压入最左边的参数后,再压入一个指针,我们姑且叫它ReturnValuePointer,ReturnValuePointer指向 caller局部变量区的一块未命名的地址,这块地址将用来存储callee的返回值。函数返回时,callee把返回值拷贝到 ReturnValuePointer指向的地址中,然后把ReturnValuePointer的地址赋予EAX寄存器。函数返回后,caller通过 EAX寄存器找到ReturnValuePointer,然后通过ReturnValuePointer找到返回值,最后,caller把返回值拷贝到负 责接收的局部变量上(如果接收返回值的话)。
你或许会有这样的疑问,函数返回后,对应的堆栈帧已经被销毁,而ReturnValuePointer是在该堆栈帧中,不也应该被销毁了吗?对的,堆栈帧是被销毁了,但是程序不会自动清理其中的值,因此ReturnValuePointer中的值还是有效的。
堆栈帧的销毁
当函数将返回值赋予某些寄存器或者拷贝到堆栈的某个地方后,函数开始清理堆栈帧,准备退出。堆栈帧的清理顺序和堆栈建立的顺序刚好相反:(堆栈帧的销毁过程就不一一画图说明了)
1)如果有对象存储在堆栈帧中,对象的析构函数会被函数调用。
2)从堆栈中弹出先前的通用寄存器的值,恢复通用寄存器。
3)ESP加上某个值,回收局部变量的地址空间(加上的值和堆栈帧建立时分配给局部变量的地址大小相同)。
4)从堆栈中弹出先前的EBP寄存器的值,恢复EBP寄存器。
5)从堆栈中弹出函数的返回地址,准备跳转到函数的返回地址处继续执行。
6)ESP加上某个值,回收所有的参数地址。
前面1-5条都是由callee完成的。而第6条,参数地址的回收,是由caller或者callee完成是由函数使用的调用约定(calling convention )来决定的。下面的小节我们就来讲解函数的调用约定。
函数的调用约定(calling convention)
函数的调用约定(calling convention)指的是进入函数时,函数的参数是以什么顺序压入堆栈的,函数退出时,又是由谁(Caller还是Callee)来清理堆栈中的参数。有2个办法可以指定函数使用的调用约定:
1)在函数定义时加上修饰符来指定,如
void __thiscall mymethod();
{
...
}
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,"Courier New",courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); } 2)在VS工程设置中为工程中定义的所有的函数指定默认的调用约定:在工程的主菜单打开Project|Project Property|Configuration Properties|C/C++|Advanced|Calling Convention,选择调用约定(注意:这种做法对类成员函数无效)。
常用的调用约定有以下3种:
1)__cdecl 。这是VC编译器默认的调用约定。其规则是:参数从右向左压入堆栈,函数退出时由 caller清理堆栈中的参数。这种调用约定的特点是支持可变数量的参数,比如printf方法。由于callee不知道caller到底将多少参数压入 堆栈,因此callee就没有办法自己清理堆栈,所以只有函数退出之后,由caller清理堆栈,因为caller总是知道自己传入了多少参数。
2)__stdcall 。所有的Windows API都使用__stdcall。其规则是:参数从右向左压入堆栈,函数退出时由callee自己清理堆栈中的参数。由于参数是由callee自己清理的,所以__stdcall不支持可变数量的参数。
3) __thiscall 。类成员函数默认使用的调用约定。其规则是:参数从右向左压入堆栈,x86 构架下this指针通过ECX寄存器传递,函数退出时由callee清理堆栈中的参数,x86构架下this指针通过ECX寄存器传递。同样不支持可变数 量的参数。如果显式地把类成员函数声明为使用__cdecl或者__stdcall,那么,将采用__cdecl或者__stdcall的规则来压栈和出 栈,而this指针将作为函数的第一个参数最后压入堆栈,而不是使用ECX寄存器来传递了。
反编译代码的跟踪(不熟悉汇编可跳过)
以下代码为和foo函数对应的堆栈帧建立相关的代码的反编译代码,我将逐行给出注释,可对照前文中对堆栈的描述:
main函数中 int result=foo(3,4); 的反汇编:
008A147E push 4 //b=4 压入堆栈
008A1480 push 3 //a=3 压入堆栈,到达图2的状态
008A1482 call foo (8A10F5h) //函数返回值入栈,转入foo中执行,到达图3的状态
008A1487 add esp,8 //foo返回,由于采用__cdecl,由Caller清理参数
008A148A mov dword ptr [result],eax //返回值保存在EAX中,把EAX赋予result变量
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,"Courier New",courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); }.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,"Courier New",courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); }
下面是foo函数代码正式执行前和执行后的反汇编代码
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas,"Courier New",courier,monospace; background-color: rgb(255, 255, 255); }.csharpcode pre { margin: 0em; }.csharpcode .rem { color: rgb(0, 128, 0); }.csharpcode .kwrd { color: rgb(0, 0, 255); }.csharpcode .str { color: rgb(0, 96, 128); }.csharpcode .op { color: rgb(0, 0, 192); }.csharpcode .preproc { color: rgb(204, 102, 51); }.csharpcode .asp { background-color: rgb(255, 255, 0); }.csharpcode .html { color: rgb(128, 0, 0); }.csharpcode .attr { color: rgb(255, 0, 0); }.csharpcode .alt { background-color: rgb(244, 244, 244); width: 100%; margin: 0em; }.csharpcode .lnum { color: rgb(96, 96, 96); }008A13F0 push ebp //把ebp压入堆栈
008A13F1 mov ebp,esp //ebp指向先前的ebp,到达图4的状态
008A13F3 sub esp,0E4h //为局部变量分配0E4字节的空间,到达图5的状态
008A13F9 push ebx //压入EBX
008A13FA push esi //压入ESI
008A13FB push edi //压入EDI,到达图7的状态
008A13FC lea edi,[ebp-0E4h] //以下4行把局部变量区初始化为每个字节都等于cch
008A1402 mov ecx,39h
008A1407 mov eax,0CCCCCCCCh
008A140C rep stos dword ptr es:[edi]
...... //省略代码执行N行
......
008A1436 pop edi //恢复EDI
008A1437 pop esi //恢复ESI
008A1438 pop ebx //恢复EBX
008A1439 add esp,0E4h //回收局部变量地址空间
008A143F cmp ebp,esp //以下3行为Runtime Checking,检查ESP和EBP是否一致
008A1441 call @ILT+330(__RTC_CheckEsp) (8A114Fh)
008A1446 mov esp,ebp
008A1448 pop ebp //恢复EBP
008A1449 ret //弹出函数返回地址,跳转到函数返回地址执行 //(__cdecl调用约定,Callee未清理参数)
参考
Debug Tutorial Part 2: The Stack
http://msdn.microsoft.com/zh-cn/library/46t77ak2(VS.80).aspx
摘自:http://www.cnblogs.com/Binhua-Liu/archive/2010/08/24/1803095.html