下面通过一个例子来看看函数调用堆栈的过程
#include <stdio.h>
int Sum(int lhs,int rhs)
{
int tmp=lhs+rhs;
return tmp;
}
int main()
{
int a=10;
int b=20;
int rt=0;
rt=Sum(a,b);
return 0;
}
将上面代码转到反汇编:
main函数
10: {
0040D780 push ebp //将ebp入栈
0040D781 mov ebp,esp //将esp指向ebp
0040D783 sub esp,4Ch //给main函数开辟4ch大的栈帧空间
0040D786 push ebx // 将ebx入栈
0040D787 push esi // 将esi入栈
0040D788 push edi // 将edi入栈
0040D789 lea edi,[ebp-4Ch] //将ebp-4Ch放到edi中
0040D78C mov ecx,13h //将13h放到ecx中,ecx是计数器
0040D791 mov eax,0CCCCCCCCh //将0CCCCCCCCh放到eax中,eax是累加器
0040D796 rep stos dword ptr [edi] //从edi开始,向高地址部分进行字节拷贝,每次拷贝4个字节,拷贝eax的内容,拷贝13h次,上面这几步给开辟的空间初始化
11: int a=10;
0040D798 mov dword ptr [ebp-4],0Ah //将10存入ebp-4的位置
12: int b=20;
0040D79F mov dword ptr [ebp-8],14h //将20存入ebp-8的位置
13: int rt=0;
0040D7A6 mov dword ptr [ebp-0Ch],0 //将0存入ebp-12的位置
14: rt=Sum(a,b);
0040D7AD mov eax,dword ptr [ebp-8]
0040D7B0 push eax //把ebp-8的值赋给eax寄存器,将eax寄存器入栈
0040D7B1 mov ecx,dword ptr [ebp-4]
0040D7B4 push ecx //把ebp-4的值赋给eax寄存器,将ecx寄存器入栈
0040D7B5 call @ILT+10(Sum) (0040100f) //将下一条指令的地址入栈,并跳转到Sum函数的栈帧
0040D7BA add esp,8
0040D7BD mov dword ptr [ebp-0Ch],eax //将eax的值存入ebp-12
15: return 0;
0040D7C0 xor eax,eax
16: }
Sum函数
4: {
0040D750 push ebp //将main函数的ebp入栈
0040D751 mov ebp,esp //将esp指向ebp
0040D753 sub esp,44h //给Sum函数开辟4ch大的栈帧空间
0040D756 push ebx // 将ebx入栈
0040D757 push esi // 将esi入栈
0040D758 push edi // 将edi入栈
0040D759 lea edi,[ebp-44h] //将ebp-44h放到edi中
0040D75C mov ecx,11h //将11h放到ecx中,ecx是计数器
0040D761 mov eax,0CCCCCCCCh //将0CCCCCCCCh放到eax中,eax是累加器
0040D766 rep stos dword ptr [edi] //从edi开始,向高地址部分进行字节拷贝,每次拷贝4个字节,拷贝eax的内容,拷贝11h次,上面这几步给开辟的空间初始化,和main函数中的过程是一样的
5: int tmp=lhs+rhs;
0040D768 mov eax,dword ptr [ebp+8] //将ebp+8的值给eax
0040D76B add eax,dword ptr [ebp+0Ch] //将ebp+12的值给eax,并与之前eax中的值相加
0040D76E mov dword ptr [ebp-4],eax //将eax的值存入ebp-4
6: return tmp;
0040D771 mov eax,dword ptr [ebp-4] //将ebp-4的值给eax
7: }
0040D774 pop edi
0040D775 pop esi
0040D776 pop ebx
0040D777 mov esp,ebp
0040D779 pop ebp
0040D77A ret
由上面的过程可以画出如下图:
ebp:栈底指针寄存器,指向当前函数的栈底位置
esp:栈顶指针寄存器,指向当前函数的栈顶位置
pc:下一行指令寄存器,保存下一条指令的物理地址
通过上面的过程,可得出开栈需要以下几个步骤:
1、压入实参,形参初始化;
2、压入下一行指令的地址,当函数调用结束后,继续执行下一行指令;
3、压入调用方栈底地址,被调用方结束后回退到调用方的栈帧;
4、移动ebp到被调用方栈底
5、被调用方开辟活动空间并初始化为cccc cccc
清栈和上面的过程相反。
1、形参开辟内存吗?
形参开辟内存,由调用方开辟。
2、形参和实参的在入栈顺序?
实参按照从左到右的顺序入栈,形参按照从右到左的顺序入栈
3、返回值由谁带出来?
返回值<4个字节,由eax寄存器带出来
4个字节<返回值<8个字节,由eax寄存器和edx寄存器带出来
返回值>8个字节,由临时量带出来,临时量存储在内存上
4、被调用方结束后怎么知道回退到调用方?
被调用方保存了调用方的栈底指针地址
5、函数调用结束后,怎么知道继续执行下一行指令?
被调用方保存了下一行指令的地址,当被调用方清栈时得到这个地址返回调用方的下一行指令的地址,继续执行下一行指令