修炼内功之函数栈帧的创建与销毁
- 一 前置知识
- (1)栈
- (2)相关寄存器和汇编指令
- 二 函数栈帧
- 三 代码演示函数栈帧的创建
- (1)代码演示
- (2)函数栈帧逐帧分析
- 四 对开篇问题的解答
相信来CSDN的伙伴们,或多或少对编程都有一定的了解,那本菜鸟来考考你,关于编程的绝命六连问,看看你能答对几个?
1.局部变量是如何创建的?·
2.为什么局部变量不初始化内容是随机的?
3.有些时候屏幕上输出的"烫烫烫"是怎么来的?
4.函数调用时参数时如何传递的?传参的顺序是怎样的?。
5.函数的形参和实参分别是怎样实例化的?
6.函数的返回值是如何返回的?
或许这些问题一出,有些小伙伴们可能就懵了,我们都知道函数,函数调用,我们也知道局部变量,形参,实参等等,这些我们每次写代码都在用的东西,看似简单,深入思考之后,发现别有洞天,这些告诉我们,学习编程,不可以浮于表面。
要解决这些问题,我们就得深入理解函数栈帧的创建与销毁。
一 前置知识
(1)栈
一般我们在学习 C/C ++语言的时候.我们会关注内存中的三个区域:栈区、堆区、静态区。
1.局部变量,函数参数是放在内存的栈区
2.全局变量,静态变量是放在内存的静态区
3.堆区是用来动态内存管理的
由此,我们知道函数是在栈中开辟空间的,那什么是栈呢?
栈:在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈, push) ,也可以将已经压入栈中的数据弹出(出栈, pop ),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈 (First ln Last 0ut, FIFO) 。就像叠成一叠的书,先上去的书在最下面,因此要最后才能取出。
在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。栈顶由成为 esp 的寄存器进行定位的。
(2)相关寄存器和汇编指令
1.相关寄存器
eax :通用寄存器,保留临时数据,常用于返回值ebx :通用寄存器,保留临时数据
ebp :栈底寄存器
esp :栈顶寄存器
eip :指令寄存器,保存当前指令的下一条指令的地址
2.相关汇编指令
mov :数据转移指令
push :数据入栈,同时 esp 栈顶寄存器也要发生改变
pop :数据弹出至指定位置,同时 esp 栈顶寄存器也要发生改变
sub :减法命令
add :加法命令
cal l :函数调用, I. 压入返回地址 2.转入目标函数jump :通过修改 eip,转入目标函数,进行调用
ret :恢复返回地址,压入 eip ,类似 pop eip 命令
二 函数栈帧
概念:每一次函数的调用,操作系统都会在内存的栈区上开辟一块空间,称为栈帧。函数调用建立栈帧,栈帧中存储局部变量,参数等。
学习函数的栈帧,就必须要了解ebp,esp(ebp 存放的是栈底的地址,esp 存放的是栈顶的地址,),专门用于维护函数栈帧的。
三 代码演示函数栈帧的创建
(1)代码演示
#include <stdio.h>
int sum(int x, int y)
{int sum = 0;sum = A + B;return sum;
}
int main()
{int a = 3;int b = 5;int s = 0;s = Add(a, b);printf("%d\n", ret);return 0;
}
首先,我先简单的声明一下,main函数也是被其他函数调用起来的,在vs2019中, main 函数调用之前,是由invoke-main 函数来调用 main 函数。在 invoke-main 函数之前的函数调用我们就暂时不考虑了。那我们可以确定, invoke-main 函数应该会有自已的栈帧, main 函数和 Add 函数也会维护自已的栈帧,每个函数栈帧都有自已的 ebp 和 esp 来维护栈帧空间。
那接下来我们从 main 函数的栈帧创建开始讲解:
(2)函数栈帧逐帧分析
1.转到反汇编
2.逐帧分析如图
四 对开篇问题的解答
1.局部变量是如何创建的?
局部变量的创建是在局部变量所在的函数的栈帧创建完成并初始化后,然后在该栈帧内为局部变量分配空间的。
2.为什么局部变量不初始化其内容是随机的?
因为编译器在创建函数栈帧后会在栈帧空间里面放入一个值,而这个值是随机的。
3.有些时候屏幕上输出的"烫烫烫"是怎么来的?
#include<stdio.h>
int main()
{
char arr[20];
printf("%s\n",arr);
return 0;
}
调试输出“烫烫烫……”的原因,是因为main函数调用时,在栈区开辟的空间的其中每一个字节都被初始化为0xCC,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。
4 ,函数调用时参数时如何传递的?传参的顺序是怎样的?
我们在调用函数之前,就会在栈顶上从右向左依次压入需要传递的参数,在创建好被调函数的函数栈帧后通过指针的偏移量来使用传递过去的参数,而不是在被调函数的函数栈帧内创建形参。
5 .函数的形参和实参的关系是什么?
形参是实参的一份临时拷贝,二者的存储亻立置不同,形参的改变不会影响实参。
6 .函数的返回值是如何带回的?
函数的返回值通过 eax寄存器带回。
(完)