关于函数的调用堆栈有如下几个问题:
函数调用函数的栈桢开辟及回退过程是什么?
主调函数在调用被调函数的中间过程做了什么?
被调函数执行完成后是怎样回到主调函数的?
在回到主调函数后是怎么知道要运行哪一条代码(指令)?
要解决这些问题,我们就要从汇编的角度切入。通过汇编代码能够使我们更加清晰地掌握函数的堆栈调用。
汇编分为两种形式inter x86 (从右向左看) 和 AT&T unix(从左向右看),我们要学习的主要是inter x86下的汇编代码。
例:
//main.c
int sum(int a, int b)
{
int temp = 0;
temp = a + b;
return temp;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = sum(a, b);
printf("ret = %d\n", ret);
return 0;
}
函数在开辟栈桢空间的时候是通过两个指针即栈顶指针esp和栈底指针ebp来完成的。main.c的汇编代码如下:
1: #include<stdio.h>
2:
3: int sum(int a, int b)
4: { //开辟0x44大小的函数栈桢空间并循环赋值为0xcccccccc
0040D760 push ebp
0040D761 mov ebp,esp
0040D763 sub esp,44h
0040D766 push ebx //三个保存sum函数现场的寄存器ebx、 esi、edi
0040D767 push esi
0040D768 push edi
0040D769 lea edi,[ebp-44h]
0040D76C mov ecx,11h
0040D771 mov eax,0CCCCCCCCh
0040D776 rep stos dword ptr [edi]
5: int temp = 0; //局部变量申请空间并初始化
0040D778 mov dword ptr [ebp-4],0
6: temp = a + b; //将运算结果保存在局部变量中
0040D77F mov eax,dword ptr [ebp+8]
0040D782 add eax,dword ptr [ebp+0Ch]
0040D785 mov dword ptr [ebp-4],eax
7: return temp; //返回局部变量
0040D788 mov eax,dword ptr [ebp-4] //通过寄存器eax返回
8: } //函数栈帧空间的回退
0040D78B pop edi
0040D78C pop esi
0040D78D pop ebx
0040D78E mov esp,ebp //释放sum函数栈桢空间
0040D790 pop ebp //将ebp重新指向main函数栈底,并使得esp向下偏移
0040D791 ret
9:
10: int main()
11: { //开辟0x4c大小的函数栈桢空间并循环赋值为0xcccccccc
00401050 push ebp
00401051 mov ebp,esp
00401053 sub esp,4Ch
00401056 push ebx //三个保存main函数现场的寄存器ebx、 esi、edi
00401057 push esi
00401058 push edi
00401059 lea edi,[ebp-4Ch]
0040105C mov ecx,13h
00401061 mov eax,0CCCCCCCCh
00401066 rep stos dword ptr [edi] //局部变量申请空间并初始化
12: int a = 10;
00401068 mov dword ptr [ebp-4],0Ah
13: int b = 20;
0040106F mov dword ptr [ebp-8],14h
14: int ret = 0;
00401076 mov dword ptr [ebp-0Ch],0
15: ret = sum(a, b); //调用sum函数
0040107D mov eax,dword ptr [ebp-8] //压实参
00401080 push eax
00401081 mov ecx,dword ptr [ebp-4]
00401084 push ecx
00401085 call @ILT+0(_sum) (00401005) //跳转到sum函数入口
0040108A add esp,8 //清理实参空间
0040108D mov dword ptr [ebp-0Ch],eax
16:
17: printf("ret = %d\n", ret); //调用printf函数
00401090 mov edx,dword ptr [ebp-0Ch]
00401093 push edx
00401094 push offset string "ret = %d\n" (0042201c)
00401099 call printf (004010d0)
0040109E add esp,8
18: return 0;
004010A1 xor eax,eax
19: } //清理主函数栈桢空间
004010A3 pop edi
004010A4 pop esi
004010A5 pop ebx
004010A6 add esp,4Ch //释放main函数栈桢空间
004010A9 cmp ebp,esp
004010AB call __chkesp (00401150)
004010B0 mov esp,ebp
004010B2 pop ebp
004010B3 ret
其开辟运行过程可用如下图来表示:
回退过程如下:
通过汇编代码,堆函数的堆栈调用做出了如下总结:
- 栈通过两个指针栈顶指针和栈底指针来完成堆栈桢空间的开辟,另外栈底指针保存的是调用main函数的函数栈顶地址(可以自己去了解),栈顶指针通过偏移量来开辟栈桢空间的大小,开辟完成后通过edi和esi寄存器来对栈桢空间附初始值0xcccccccc,通过栈底指针偏移将局部变量压入栈桢空间。在遇到调用函数的地方首先通过寄存器将实参入栈,然后调用call指令,call指令在执行时做了两件事:首先,为保证函数调用完成后能够回到当前位置继续执行于是将下一行指令的地址压入栈顶;另外,通过地址偏移跳转到被调函数(sum)入口。在进入被调函数前将原来ebp入栈,然后将ebp移动到栈顶以同样的方式对被调(sum)函数开辟栈桢空间和赋初始值。为局部变量temp申请空间通够ebp的偏移取值和eax寄存器保存计算结果并赋值给temp完成函数功能。最后的结构也是有eax寄存器带出(这里我们就不分析printf函数的堆栈调用过程)。在完成函数功能后就是函数栈桢空间的回退,也是有ebp和esp两个指针来完成的,其中的pop操作为将esp向下偏移和对ebp重新赋值,值就为该空间中保存的地址,也就是重新将ebp指向该地址处。