函数栈帧的创建和销毁

函数栈帧的创建和销毁

本文浅述函数栈帧的创建和销毁。

在我们前期的学习过程中,可能会遇到很多困惑比如,

  • 局部变量是怎么创建的?
  • 为什么局部变量是随机值?
  • 函数是怎么传参的?传惨的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用结束后是怎么返回的?

如果知道函数栈帧的创建和销毁后就都清楚了。在不同的编译器下,函数栈帧的创建和销毁稍有差异,但大体相同。

基础知识介绍

寄存器的种类与功能

寄存器名称功能
eax累加寄存器,相对于其他寄存器,在运算方面比较常用。
ebx基地址寄存器,在内存寻址时存放基地址。
ecx计数寄存器,用于循环操作,比如重复的字符存储操作,或者数字统计。
edx作为EAX的溢出寄存器,总是被用来放整数除法产生的余数。
esi源变址寄存器,主要用于存放存储单元在段内的偏移量。通常在内存操作指令中作为“源地址指针”使用。
edi目的变址寄存器,主要用于存放存储单元在段内的偏移量。
eip控制寄存器,存储CPU下次所执行的指令地址(存放指令偏移地址)
esp栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。是CPU机制决定的,push、pop指令会自动调整esp的值。
ebp基址指针,指栈的栈底指针。基址指针寄存器(extended base pointer),一般与esp配合使用,可以存取某时刻的esp,这个时刻就是进入一个函数内后,CPU会将esp的值赋给ebp,此时就可以通过ebp对栈进行操作,比如获取函数参数,局部变量等。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

内存模型

从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

这次演示所使用的环境是windows 10、编译环境 vs2013(debug、Win32)。
在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
友情提示:
不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。
演示函数栈帧的创建销毁过程

首先来看下这次演示使用的代码:

// 为了能够观察全部的细节,所以把代码拆的足够细。

#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\n", c);
	return 0;
}

按下F10,在视图中打开调用堆栈窗口,我们发现main()函数被调用了。

但是main()函数被谁调用了呢?

当我们接着调试到return 0;之后,再按F10,我们发现程序跳转到了调用main()函数的函数内

原来main()函数是被__tmainCRTStartup函数调用的,而 __tmainCRTStartup又是被mainCRTStartup调用的。

接下来分步骤演示函数栈帧的创建和销毁的过程。

栈帧创建的具体过程:

  1. push(ebp),压栈。就是把上个函数栈帧的ebp储存的地址压入栈顶。push完后,esp会向低地址处走,地址会-4。因为函数栈帧从高地址流向低地址。
  2. mov(esp, ebp),传递地址。就是把esp的地址给到ebp。意思就是ebp不再指向原来的函数栈底,而是指向原来函数的栈顶,(也是新函数的栈底)。
  3. sub(esp),给esp减去一个八进制的数字,让其指向更低地址处。改变新函数的栈顶。

这样,就为新函数预开辟好了空间。

随后push:ebx, esi, edi.

  1. lea (edi, [ebp-0E4h]) – (load effective ):把[ebp-0E4h]这个地址传给edi

  2. mov (ecx,39h) :

  3. mov (eax,0CCCCCCCCh)

  4. rep stos (dword ptr es: [edi])

    4,5,6,7的作用是:从edi开始,39h次 个double word (4字节)全部初始化成0CCCCCCCCh,就是全部变成ccccccccc

    刚刚开辟的那一块为函数开辟的栈帧全部变成了cccccccc,就是初始化main函数好了。

  5. 假设下一个操作时int a = 10; , mov (dword, ptr [ebp-8], 0Ah), 把0Ah的值给到[ebp-8]的位置处。如果不初始化,而只是给到变量,int a;,那么内存中存放的就是 0cccccccch就是烫烫烫烫烫烫

  6. 假设下一个操作时int b = 20; , mov (dword, ptr [ebp-14], 14h), 把014h的值给到[ebp-14]位置处。

  7. 假设下一个操作是简单的Add函数,其进行的操作为

    最一开始就进行传参)

    • mov (eax, dword ptr [ebp-14h]) ,把ebp-14h的地址给到eax,就是把20这个值给到eax

    • push (eax),eax压栈,把20压进去了。传参b

    • mov (ecx, dword ptr [ebp-8h]),把ebp-8h的地址给到ecx,就是把10这个值给到ecx

    • push (ecx),ecx压栈,把10压进去了。传参a

    • call (一个地址px),调用函数,也就是call指令的下一个地址变成了px

    • 随后真正来到了px地址,也就是调用Add函数

      • push (ebp),把ebp的值压栈

      • mov (ebp,esp) 把esp给到ebp,ebp现在指向了esp的地址

      • sub (esp, 0CCh), esp减去0cch的值,指向更低的地址

        以上三个步骤,开辟add函数栈帧

      • push -ebp, -esi, -edi, 将三个寄存器地址push进栈帧

      • lea (edi, [ebp+FFFFFF34h] )

      • mov (ecx, 33h)

      • mov (eax, 0CCCCCCCCh)

      • rep stos (dword ptr es:[edi]),把add栈帧全部初始化为cccccccc

      • mov (dword ptr [ebp-8], 0) – int z = 0

      • Z = x + y;

        • mov (eax, dword ptr [ebp+8] ), 找到原来push过来的a的值,挪到eax处
        • add (eax,dword ptr [ebp+0Ch]),把ebp+12的值加到eax里面,就是得到了30
        • mov (dword ptr [ebp-8] eax),把eax的值放到ebp-8里面去。放到z里面去了。
      • return z;

        • mov (eax, dword ptr [ebp-8]),把ebp-8的值放到eax寄存器里,(相当于用全局的寄存器储存了一个变量,否则一会就销毁了)

        • pop -edi, -esi, -ebx;

        • mov (esp,ebp),把ebp赋给esp,esp指向ebp指向的位置

        • pop (ebp),把栈顶元素删除,就是把ebp里面存的main函数地址删除,这样ebp不在指向栈顶,而是回到原来的指向,即栈底,main函数的栈底,而esp也不在指向原来ebp的指向位置,而是向下一个位置,因为栈顶元素pop掉了,需要下移一个。

        • ret ,是返回call指令的下一条指令的地址

    • add (esp, 8), 把esp的地址+8,也就是把a和b两个形参释放

    • mov (dword ptr [ebp-20h], eax) , 把eax的值给到ebp-20,eax原本是我们存的add返回值,这样我们就把返回值带回到ebp-20了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值