函数栈帧的创建与销毁

前言

在之前的学习中,我们了解到每一次函数调用都会为本次函数调用分配内存空间(开辟内存空间),为本次函数分配的空间被称为该函数的栈帧空间。

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机值?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参的关系是什么?
  • 函数是怎么进行调用的?
  • 函数调用结束后是怎么返回的?
    带着这些疑惑,我们开始本章 “函数栈帧的创建与销毁”的学习。

1.☀️🍕寄存器的介绍

🐇简介: 寄存器是中央处理器的组成部分,是有限存储容量的高速存储部件,可以存储数组、指令、地址。寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。
我们可以了解到有eax、ebx、ecx、edx、ebp、esp等寄存器,其中ebp、esp这两个寄存器是用来维护函数栈帧的。

esp:栈指针寄存器(extended stack pointer),其内存存放着一个指针,该指针永远指向系统最上面一个栈帧的栈顶(简称栈顶指针)。
edp:扩展基指针(extended base pointer),其内存存放着一个指针,该指针永远指向系统最上面一个栈帧的底部(简称栈底指针)。

要想了解更多浏览器的知识,可以查找相关书籍和网站搜索寄存器,在这里就不详细介绍了。

2.⭐⭐⭐函数栈帧的创建

🥘🎉🍭C语言编译的程序占的内存分别占栈区(stack)、堆区(heap)、全局区(静态区(static))、文字常量区、程序代码区五个区,而我们学习的函数栈帧的创建与销毁发生在栈区里面。

我们使用的编译器是vs2013(vc6.0也可以),不要使用太高级的编译器,太高级的编译器封装更复杂,考虑到各种问题,太高级的编译器反而越不容易观察和学习。同时在不同的编译器下,函数栈帧的创建略有差异,大体上是一致的,具体细节取决于不同的编译器。
使用的代码:

#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;
}

2.1观察函数的操作步骤

①按F10进入调试阶段,选择“调试”栏目,点击“窗口”选择“调用堆栈”。
在这里插入图片描述
②一直按F10直到跳转,该界面可以发现main函数也是被调用的,那么它是被谁调用的呢?
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
③通过观察我们可以发现,mian函数被一个叫__tmainCRTStartup函数调用,而__tmainCRTStartup函数又被mainCRTStartup函数调用,最终mian函数的返回值被mainret接收。

2.2分析汇编代码的含义

在这里插入图片描述
由上面的图我们可以看出现在esp、ebp现在维护着main函数的空间,那么这两个指针之前是维护__CRTStartup函数以及CRTStartup函数的,以及接下来它是怎么维护main函数和add函数的呢?我们继续学习哈!🎃🎄🎃
main函数栈帧的开辟
在这里插入图片描述
在这里插入图片描述
汇编代码:
在这里插入图片描述

图形理解:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
执行完上述步骤之后,main函数开辟的内存空间完毕。
局部变量的创建:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
从中我们可以看出,局部变量不是紧挨着存放的,而是隔着两个整型空间存放的,具体隔着多少空间取决于编译器。
add函数中x、y传参
在这里插入图片描述
在这里插入图片描述
esp的变化过程:
在这里插入图片描述

在这里插入图片描述
程序执行到这的时候,按F11执行完call指令后,跳转至add函数开始执行处🎇🎆✨
call 00C210E1

执行call调用函数指令,跳转到汗珠开始执行处00C210E1的位置。

在这里插入图片描述
汇编代码: jmp 00C213C0

JMP 为无条件转移指令,执行后跳转到指定的目标地址,从目标地址开始执行指令。

在这里插入图片描述
在这里插入图片描述
通过观察可以发现,执行了call指令后,将call指令的下一条指令的地址push(压栈),esp指向的位置向上移动。这一步其实为add函数栈帧的销毁完后,可以跳回到main函数继续执行下一条指令做准备,具体是怎么操作的呢?我们接着继续探索。🎠🎨🎪继续按F11进到以下界面:
在这里插入图片描述
add函数栈帧的开辟:
在这里插入图片描述
add函数初始化:
在这里插入图片描述
分析汇编代码:
lea edi,[ebp+FFFFFF34h]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]

执行lea指令把[ebp+FFFFFF34h]的地址加载到ebp中。
执行mov指令把33h给ecx寄存器,执行mov指令CCCCCCCC给eax寄存器。
执行rep指令,目的是重复上面的指令,ecx的值是重复的次数。
执行stos指令的作用是将eax中的值拷贝到dword ptr es:[edi]指向位置4个字节的内存空间。

动图分解:
在这里插入图片描述
到这里add函数的栈帧开辟完成,那么add函数内部是如何运算的呢?让我们继续分析吧!🎠🪢👓
在这里插入图片描述

在这里插入图片描述

这时候add函数里面的运算完成了,从汇编代码中我们可以看出:

①在执行指令call之前(add函数栈帧开辟之前)参数已经传好了。
②函数传参是从右往左传的(add函数中先传b’再传a’).
③形参存储在main函数的栈帧里面。
④形参和实参在值上相同,空间上是独立的。
⑤改变形参而实参不会改变,所以形参是实参的一份临时拷贝

图形理解:
在这里插入图片描述

3.🥂🍗函数栈帧的销毁

在这里插入图片描述

mov eax,dword ptr [ebp-8]
把[ebp-8]指向的内容(这里是指z=30的值)给eax。
pop edi
pop为出栈指令,弹出esp指向的元素放到edi中,esp指向的位置向下跳一格。
pop esi
pop为出栈指令,弹出esp指向的元素放到esi中,esp指向的位置向下跳一格。
pop ebx
pop为出栈指令,弹出esp指向的元素放到ebx中,esp指向的位置向下跳一格。
mov esp,ebp
把ebp的值给esp,esp和ebp指向同一内存单元,esp向下跳,所跳过的内存单元还给操作系统。
pop ebp
pop为出栈指令,弹出esp指向的元素放到ebp中,esp指向的位置向下跳一格(此时esp指向的位置存放的正是维护mian栈底的ebp的值)。
ret
弹出esp指向的call指令下一条指令的地址,给回操作系统,然后汇编代码跳转到mian函数中继续执行。🥛🍽️🍊

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
分析汇编代码:

add esp ,8
esp的值增加8,esp指向的位置向下跳8格,这时候esp所跳过内存空间还给操作系统了。
mov dword ptr [ebp-20h],eax
把eax的值(形参x、y之和30)赋值给[ebp-20h](C的位置)指向的4个字节的内容。

动图分解:
在这里插入图片描述
至此,add函数栈帧的销毁已经结束,之后便是打印、和main函数栈帧的销毁了(和add函数销毁原理一样),好奇的朋友可以操作一下。🎉🎉🎉

总结

现在我们已经学习完函数的创建与销毁的了,就让我们解决最开始的疑惑吧!

①局部变量是怎么创建的?
答:以mian函数里面的局部变量为例,在mian函数栈帧创建完成并且初始化完成的时候,通过执行mov指令把a、b、c的值传给main函数分配的内存空间。
②为什么局部变量是随机值?
以vs2013编译器为例,mian函数的函数栈帧在创建完会进行初始化,如上面的汇编代码把main函数栈帧空间的内容初始化为CCCCCCCC。有时候我们声明局部变量而未初始化的时候,会输出“烫烫烫烫烫烫烫烫”之类的内容,就是这个有原因。具体初始化什么取决于编译器,在这里也提醒大家,在创建局部变量的时候一定要养成初始化的好习惯。
③函数在栈帧中是怎么传参的?传参的顺序是怎么样的?
答:通过push(压栈)的操作,把x、y放到函数的栈顶上;函数栈帧传参的顺序是从右向左。注意:形参不在被调用的函数栈帧里面,而在调用函数的栈帧里面。
④形参和实参的关系是什么?
答:形参和实参在值上相同,而在空间上独立;改变形参,不会影响实参;可以说形参是实参的一份临时拷贝。
⑤函数是怎么调用的?
答:mian函数通过执行call指令,把call下一条指令的地址push(压栈)后,然后跳转至add函数开始执行处。
⑥函数调用是怎么返回的?
答:add函数中把维护mian函数的edp的值push放到栈顶,然后在函数栈帧销毁的过程中,执行pop(出栈)操作弹出存放的属于维护mian的edp的值放到现在维护add函数edp的值里面,然后edp重新维护mian的栈底;至于esp,在add函数执行ret 操作后,弹出esp指向的(call下一条指令的地址)的元素还给操作系统后,esp向下跳一格重新维护main函数的栈顶,汇编代码跳转到call指令的下一步继续执行!

我们今天所学到的汇编代码指令有:

add–加法指令,目的操作数加上源操作数,两个操作数之和替换目的操作数。
sub–减法指令,目的操作数减去源操作数,两个操作数只差替换目的操作数。
push–压栈指令,给栈顶上放一个元素(也可以说是从esp指向的位置上放一个元素),同时esp指向的位置向上跳一格。
pop–出栈指令,从栈顶删去一个元素(也可以说是从esp指向的位置删去一个元素),同时esp指向的位置向下跳一格。
mov–传送指令,用源操作数替换掉目的操作数,即把源操作数传到目的操作数所在的空间,目的操作数必需是一个空间,可以是内存单元、寄存器。
lea–有效地址传送指令,将存储操作数的有效地址传送至寄存器。与mov指令不同,mov可以传送操作数、地址,而lea指令只能传送有效地址。
call–调用子程序指令,执行call指令后程序跳转调用函数的开始处执行。
jmp–无条件转移指令,执行jmp指令使程序跳转到目标地址,让程序从目标地址开始执行。
rep–无条件重复前缀指令,可以是串指令(stos、lods、movs、scas、cmps)的前缀,串指令不执行时结束。
ret–子程序返回条件指令(转移指令),执行ret指令后返回到call指令下一条指令继续执行。
esp寄存器跳过位置的内存空间会被操作系统回收,以保证esp永远指向栈顶

☀️☀️☀️上述的解答仅仅是在我们一起学习“函数栈帧的创建和销毁”的过程中,我自己的理解,希望对你认识“函数栈帧的创建与销毁”有些许帮助!当然,最好自己实操一遍,加深自己的理解,毕竟“纸上得来终觉浅”嘛!最后,如有不对,欢迎纠正,若有讲解不到位,也可以提出建议!最后的最后,祝大家在新的一年里惊喜不断,好运连连!

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值