函数栈帧的创建与销毁

前言

每当函数调用时,编译器都会为其在栈区上开辟一段空间,并为函数内部的变量定义。但是函数是如何在栈区开辟空间的呢?在调用结束后又是如何销毁的呢?

在进行介绍之前,先了解几个小知识:

1.寄存器分为eax、ebx、ecx、edx、ebp与esp。其中ebp与esp存放的是地址,用于维护函数栈帧。ebp存放高地址处地址,但称为栈底指针;而esp存放低地址处地址,但称为栈顶指针。这是由于栈空间是从高地址向低地址使用的。

2.在编译过程中,main函数也是被其他函数调用的。在main函数被调用之前,先会为这个调用main函数的函数在栈上开辟空间。

正文

 函数栈帧的创建

 接下来,我们用这段简单的代码来说明函数栈帧的创建。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 10;
    int b = 20;
    int c = 0;
    c = Add(a, b);
    printf("%d", c);
    return 0;
}

 在调式时,右键转到反汇编,即可进入反汇编。

 接下来,我会一一解释这些反汇编指令的作用。

1. push ebp

push的作用是压栈,在栈顶存放一个ebp,这个ebp是调用main函数的函数的ebp内存放的地址,仅仅为一个值。与此同时,esp往上移动一位。

 

 可以看到,在执行这步之后,esp的值向低位移动了一位。

 2.mov ebp,esp

mov指令的含义是将esp的值赋给ebp,这也说明要给main函数开辟空间了。

 

 3.sub esp,0E4h

sub指令是把esp减去一个0E4h(十进制228),而此时esp与ebp形成的空间就是编译器为main函数开辟的空间。

 4.push ebx、push esi、push edi

这三个push就是往栈顶压进三个元素,每一次push后,esp的值都会随之改变,共计移动了三个位置。

5.lea    edi,[ebp+FFFFFF1Ch]

lea指令是将后面这个有效地址加载到edi中。(若开启显示符号名,会发现后面的地址为[ebp-0E4h],即为最开始(三连push前) 时esp存放的地址,edi寄存器中存放的内容变为了这个地址。

6.mov ecx,39h        mov eax,0xCCCCCCh         rep stos dword ptr es:[edi]

这三句汇编指令包含:将39h存放到ecx中,将0xCCCCCCCCh的值存放到eax中,并且将从上一步edi存储的地址开始向下的39h次的dword(4字节)全部改为0xCCCCCCCCh。

此时函数栈帧大概是这样。

 8.int a = 10; mov  dowrd ptr[ebp-8],0Ah
将0A这个值(10)放进ebp-8地址处。

下面两句同理。

 此时来到调用Add函数。

c = Add(a,b);

9.mov eax,dword ptr[ebp - 14h]              push eax

这句是把地址[ebp - 14h]处的元素(也就是刚刚存放进去的20)存放在寄存器eax中,并将eax压栈;而下面两句的意思是将10存放于寄存器ecx中,并且将ecx压栈。并且每次压栈,都会导致esp的向上移动。

10.call 00C210E1

call指令意为调用 ,真正调用Add函数并且将call指令的下条指令的地址进行压栈。因为在Add调用结束后,仍需要执行call后的指令,所以在此处把其记录下来,非常巧妙。

可以看到,call指令下一条指令的地址已经被存放了。 

在call指令处F11,即可进入Add的反汇编代码。

 

在调用Add时,有没有发现其实和main函数差不多?其实他在开辟空间时,和main函数的开辟大同小异,在这里不过多赘述。

 

 11. int z = 0;      mov dword ptr[ebp-8],0

把0赋值给ebp-8处。

12.z = x+y;

     mov eax,dword ptr [ ebp+8]

     add  eax,dword ptr [ebp+0Ch]

     mov  dword  ptr  [ebp-8],eax 

第一句的含义是将ebp+8处的地址赋值给eax中,而ebp是Add函数的栈底指针,它+8指向的是main函数的空间内,因此,这个形参其实是在main函数内,通过指针偏移找到的数值,这个形参就是a。

第二句是将ebp+0Ch处的值加到eax中,而ebp+0Ch找到的值便是之前压栈进去的20,也就是变量b,此时,eax的值变为了30.

第三句是将eax中的30再次返还到ebp-8,也就是在Add函数内创建的变量z中,此时,这个z的值变为30.

 函数栈帧的销毁

13. return z;

          mov  eax,dword  ptr[ebp-8]

          pop  edi

          pop  esi

          pop  ebx

          mov    esp,ebp

          pop  ebp

          ret

        第一句:将ebp-8地址处的值赋值给eax。因为eax是寄存器不会被销毁,所以z在出函数Add时,其内的值得以保存,不会随之销毁。

        第二、三、四句:pop的作用是弹出栈顶元素,并将其值放入后面给出的变量或寄存器中。此时栈顶的三个寄存器被弹出,并将自己各自的值原封不动。与此同时,esp也向下移动三位。

        第五句:将ebp的值存放到esp中,此时ebp中的值是Add函数的栈底,esp此时指向了他。而随着esp的改变,其上面的空间还给了操作系统。

        第六句:pop再次弹出栈顶元素,并将其放入ebp中。注意:此时的栈顶元素为之前存好的main函数的ebp位置,所以ebp又跑去维护了main函数的栈底。

        第七句:ret,在Add函数执行后,应回到之前call指令的下一条指令,而这条指令的地址我们已经提前存放至栈中,程序的巧妙得以体现。ret指令就是从栈顶弹出call指令的下一条指令的地址,并且转到这个地址处。

此时,我们已经成功返回到main函数的反汇编代码。

14.add   esp,8

此举意为将esp的值加上8,esp将把之前创建的两个形参销毁。

15.mov      dword   ptr[ebp-20],eax

将eax存放的值(30)存放到ebp-20(c)处。

此时,函数的栈帧如图所示。

main函数结束时,大致和Add函数一致,在此也不重复了。

到此为止,函数栈帧的开辟与销毁也就大致结束了。 

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值