函数栈帧的创建和销毁

以该代码为例,对函数栈帧的创建和销毁进行讲解 :

#include<stdio.h>
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
    int a = 20;
    int b = 10;
    int c = 0;

    c = Add(a,b);
    printf("c = %d\n",c);
    return 0;
}

main函数

将函数进入调试状态,并对调用堆栈进行观察。

01b4d757bc03435eb174feda63534df1.png

我们发现,即使是main函数程序的入口,也是被调用的函数。关于main函数,其实是被__tmainCRTStartup函数所调用,而该函数也是被mainCRTStartup函数所调用。

分析

 vs2013中存在很多寄存器,类似于eax,ebx,ecx等,以及ebp和esp,这两个寄存器存放地址,用来维护函数栈帧。

接下来,让我们先找到该c语言程序所对应的汇编代码。 

int Add(int x, int y)
{
00C213C0  push            ebp
00C213C1  mov             ebp,esp
00C213C3  sub             esp,0CCh
00C213C9  push            ebx
00C213CA  push            esi
00C213CB  push            edi
00C213CC  lea             edi,[ebp+FFFFFF34h]
00C213D2  mov             ecx,33h
00C213D7  mov             eax,0CCCCCCCh
00C213DC  rep stos        dword ptr es:[edi]
    int z = 0;
00C213DE  mov             dword ptr [ebp-8],0
    z = x + y;
00C213E5  mov             eax,dword ptr [ebp+8]
00C213E8  add             eax,dword ptr [ebp+0Ch]
00C213EB  mov             dword ptr [ebp-8],eax
    return z;
00C213EE  mov             eax,dword ptr [ebp-8]  
}
00C213F1  pop             edi
00C213F2  pop             esi
00C213F3  pop             ebx
00C213F4  mov             esp,ebp
00C213F6  pop             ebp
00C213F7  ret

int main()
{
00C21410  push            ebp
00C21411  mov             ebp,esp
00C21413  sub             esp,0E4h
00C21419  push            ebx
00C2141A  push            esi
00C2141B  push            edi
00C2141C  lea             edi,[ebp-0E4h]
00C21422  mov             ecx,39h
00C21427  mov             eax,0CCCCCCCh
00C2142C  rep stos        dword ptr es:[edi]
    int a = 20;
00C2142E  mov             dword ptr [ebp-8],14h
    int b = 10;
00C21435  mov             dword ptr [ebp-14h],0Ah
    int c = 0;
00C2143C  mov             dword ptr [ebp-20h],0

    c = Add(a,b)
00C21443  mov             eax,dword ptr [ebp-14h]
00C21446  push            eax
00C21447  mov             ecx,dword ptr [ebp-8]
00C2144A  push            ecx
00C2144B  call            00C210E1
00C21450  add             esp,8
00C21453  mov             dword ptr [ebp-20h],eax
}  

 在main函数调用之前,我们会先对调用main函数的__tmainCRTStartup函数申请空间。

寄存器ebp(栈底指针)和esp(栈顶指针)分别指向栈底和栈顶。

同时,根据栈区空间的使用习惯,我们将从高地址向低地址使用空间。

04a4fd99adf24e4abc00bffc9b47c7e9.png

接下来进入到汇编代码的分析:

1.

00C21410  push          ebp

 进行压栈(push)操作,在当前栈区的可使用位置压入ebp。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ebp。

fad8879759e84397bcba2f86970123c9.png

 2.

00C21411  mov          ebp,esp

需要修改ebp的指向,将ebp的指向修改成与esp相同。

eaf758fcfd6849b88f7a30c56a45ff94.png

3.

00C21413  sub          esp,0E4h

 将esp的地址减去0E4h,相当于esp向上移动,与ebp申请维护一块新的内存空间。

06d51f1b22014faf9442c1af5112605e.png

4.

00C21419  push          ebx

进行压栈(push)操作,在当前栈区的可使用位置压入ebx。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ebx。

fb91b47d5bc944d4b769b0c6bf869dd6.png

5.

00C2141A  push          esi

进行压栈(push)操作,在当前栈区的可使用位置压入esi。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧esi。

8aa74fce39e74ace9b00f45e58945bce.png

6.

00C2141B  push          edi

 进行压栈(push)操作,在当前栈区的可使用位置压入edi。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧edi。

9d3bfcc0a4a842c2a3d41f896d2f78d6.png7.

00C2141C  lea          edi,[ebp-0E4h]//显示了符号名

lea表示load effective address 加载有效地址。找到ebp-0E4h的地址,并且标记。0E4h刚好是esp刚才申请的内存空间大小。

9a558a74aff44a7c897837e8e6e4b852.png

8.

00C21422  mov          ecx,39h
00C21427  mov          eax,0CCCCCCCh
00C2142C  rep stos     dowrd ptr es:[edi]

 把从edi开始向下的39h次(ecx)这么多个dword数据,全部改成0CCCCCCCh。

11a144ebe1f443e89dbeaaef09956d03.png

 

9. 

    int a = 20;
00C2142E  mov          dword ptr [ebp-8],14h

在ebp-8的位置,开辟一块空间存放14h,也就是将a赋值20。

5866b5e22ab14f76b319bc7deda00a35.png

10.

    int b = 10;
00C21435  mov          dword ptr [ebp-14h],0Ah

 在ebp-14h的位置,开辟一块空间存放0Ah,也就是将b赋值10。

a9ee83e43342436687920205dc2fa2ed.png 11.

    int c = 0;
00C2143C  mov          dword ptr [ebp-20h],0

在ebp-20h的位置,开辟一块空间存放0,也就是将c赋值0。

551b3b37fd5f49c0a2c8ee132c727f10.png 12.

    c = Add(a,b)
00C21443  mov             eax,dword ptr [ebp-14h]

将地址ebp-14h处的值(10)赋值给寄存器eax。

13.

00C21446  push            eax

进行压栈(push)操作,在当前栈区的可使用位置压入eax。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧eax。

 3ccd1267de54435bb0fc5676922f0723.png

14. 

00C21447  mov             ecx,dword ptr [ebp-8]

 将ebp-8的值(20)赋值给寄存器ecx。

15.

00C2144A  push            ecx

进行压栈(push)操作,在当前栈区的可使用位置压入ecx。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ecx。

ba37b1b62e7d45d08a878fe841eb84d6.png

16.

00C2144B  call            00C210E1
00C21450  add             esp,8

call调用函数,进入Add函数,同时需要在退出函数后继续进行下一条指令,所以会在执行call时压入call指令的下一条指令的地址。

a282523e27c0495696fb3feee571924f.png

17.

00C213C0  push            ebp

进行压栈(push)操作,在当前栈区的可使用位置压入ebp。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ebp。该ebp被销毁可返回main前的ebp。

7106573fcc0d41b982ff43f579013562.png

18.

00C213C1  mov             ebp,esp

需要修改ebp的指向,将ebp的指向修改成与esp相同。

a9b8bb7fb7c949d480c54228dd8acd86.png

19.

00C213C3  sub             esp,0CCh

将esp的地址减去0CCh,相当于esp向上移动,与ebp申请一块新的内存空间。

9b08c7214506461cafbeb7bc8647d2b6.png20.

00C213C9  push            ebx
00C213CA  push            esi
00C213CB  push            edi

进行压栈(push)操作,在当前栈区的可使用位置分别压入ebx,esi,edi。同时,因为压入新的栈帧,esp作为栈顶指针需要指向新栈帧ebx,esi,edi,最后指向edi。

045d0d90d9684574924bf799dfe67a06.png

21.

00C213CC  lea             edi,[ebp+FFFFFF34h]
00C213D2  mov             ecx,33h
00C213D7  mov             eax,0CCCCCCCh
00C213DC  rep stos        dword ptr es:[edi]

与第8步相同,通过加载有效地址找到ebp+FFFFFF34h的地址,把从edi开始向下的33h次(ecx)这么多个dword数据,全部改成0CCCCCCCh。

39a79bd218e24234b1d4b75aa1c35624.png

22. 

    int z = 0;
00C213DE  mov             dword ptr [ebp-8],0

为0开辟一块空间在ebp-8的位置。

8fe5b8dd745c49d59c3b00d0282184d2.png

23. 

    z = x + y;
00C213E5  mov             eax,dword ptr [ebp+8]

将ebp+8位置的数据赋值到寄存器eax中。

24.

00C213E8  add             eax,dword ptr [ebp+0Ch]
00C213EB  mov             dword ptr [ebp-8],eax

将ebp+0Ch中的数据与eax中的数据相加,得到eax = 30,再将eax中的数据存放到ebp-8中,也就是将z赋值为30。

25.

    return z;
00C213EE  mov             eax,dword ptr [ebp-8]

将ebp-8中的值存放到寄存器eax中。

26.

00C213F1  pop             edi

进行出栈(pop)操作,在当前栈区的最低地址可使用位置处将edi出栈。同时,因为edi栈帧出栈,esp作为栈顶指针需要指向edi的上一个栈帧。

27.

00C213F2  pop             esi

进行出栈(pop)操作,在当前栈区的最低地址可使用位置处将esi出栈。同时,因为esi栈帧出栈,esp作为栈顶指针需要指向esi的上一个栈帧。

28.

00C213F3  pop             ebx

进行出栈(pop)操作,在当前栈区的最低地址可使用位置处将ebx出栈。同时,因为ebx栈帧出栈,esp作为栈顶指针需要指向ebx的上一个栈帧。

29.

00C213F4  mov             esp,ebp

将esp的位置指向改变成ebp的位置指向,已知ebp此时正指向Add函数栈帧的起始位置,将esp位置修改为ebp,使得Add函数失去了esp的维护。将Add函数栈帧的内存回收给操作系统。

30.

00C213F6  pop             ebp

进行出栈(pop)操作,在当前栈区的最低地址可使用位置处将ebp出栈。同时,因为ebp栈帧出栈,esp作为栈顶指针需要指向ebp的上一个栈帧。因为ebp出栈,所以ebp的指向将直接变回指向main函数的起始位置指向。

31.

00C213F7  ret

进行返回操作,执行call调用函数的下一条指令,因为此时esp正指向call指令下一条指令的地址,所以可以直接进行下一步操作。函数执行重新回到main函数之中。

32.

00C21450  add             esp,8

进行加法操作,将8个字节大小的地址加到esp之中,esp地址变大,箭头向下移动8个字节大小的位置。

33.

00C21453  mov             dword ptr [ebp-20h],eax

将寄存器eax中的值赋值给ebp-20地址处,已知ebp-20出是变量c的所在区域,也就是将eax中的30赋值给c,完成了c = Add(a,b)的操作。

思考

局部变量的初始化

当局部变量没有进行初始化操作,就对其进行打印时,往往会出现的乱码打印是为什么?

解: 

在运行过程中,当我们对main函数进行调用的时候,main函数的函数栈帧开辟过程中,已经在main函数之中添加了很多随机值0CCCCCCCh,所以当局部变量在main函数中没有进行初始化操作时,会对随机值0CCCCCCCh进行打印。 

形参只是实参的一份临时拷贝

为什么说形式参数只是实际参数的一份临时拷贝?

 

解:

该程序的形式参数在程序运行过程中只是起到了传递值的效果,通过介质(寄存器eax,ecx)完成对应地址的加法操作,在该Add函数结束,eax和ecx进行出栈操作时,无法对实际参数产生影响,所以说形参只是实参的一份临时拷贝。 

总结

函数栈帧的创建和销毁存在很多的细节,例如main函数的调用、call调用语句的地址存储等,将整个程序都紧密联系在了一起。同时esp和ebp寄存器指向的变动来起到维护函数栈帧的操作也十分灵活,能快速开辟空间,也可以快速将空间回收给操作系统。本次程序运行是在vs2013的环境下实现的,汇编代码表达足够清晰,便于我自身的理解,也对我在未来的程序编写中起到了很大的益处。 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值