摘要:语言深层理解函数中栈帧的创建与销毁引言引言问题一引言问题二引言问题三一栈的简单认识内存的简单了解栈的简单了解栈的定义栈的结构二寄存器与简单的汇编指令寄存器的定义寄存器的分类简单的汇编指令三栈帧的创建于销毁调试调用堆栈调
我们在学习C语言的过程中,一定会经历过或者思考过下面的问题:
①当我们C语言中进行printf操作时,有时会出现"烫烫烫"的字眼,那么为什么会出现"烫烫烫"这样的字眼呢?
②我们在学习与使用函数时,当我们进行函数的值传递时,我们被告知当被调函数中,形参的改变,并不会改变传参变量(实参)的数据内容,那么为什么不会改变传递的参数的内容呢?
③我们在第二个问题中,还会被告知,当进行参数值传递时,在被调函数中,其实那些参数的值是实参的一份令时拷贝的数据,那么为什么是临时拷贝的数据呢?
下面我们就来彻底理解这些情况的真实原因与过程。
我们在初期学习C语言时,会学到各种变量,有的是可变变量,有的是不可修改的常量,又会接触到一些栈,堆的概念,下面的图例,是为了我们便于我们了解函数栈帧的创建与销毁的简单内存图解:
定义:栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。
①进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则
②压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
③出栈:栈的删除操作叫做出栈。出数据也在栈顶。
①定义:寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,存器有累加器(ACC)。
寄存器的基本单元是 D触发器,
按照其用途分为基本寄存器和移位寄存器
基本寄存器是由 D触发器组成,在 CP 脉冲作用下,每个 D触发器能够寄存一位二进制码。在 D=0 时,寄存器储存为 0,在 D=1 时,寄存器储存为 1。在低电平为 0、高电平为 1 时,需将信号源与 D 间连接一反相器,这样就可以完成对数据的储存。
需要强调的是,目前大型数字系统都是基于时钟运作的,其中寄存器一般是在时钟的边缘被触发的,基于电平触发的已较少使用。(通常说的CPU的频率就是指数字集成电路的时钟频率)
移位寄存器按照移位方向可以分为单向移位寄存器和双向移位寄存器。单向移位寄存器是由多个 D 触发器串接而成,在串口 Di 输入需要储存的数据,触发器 FF0 就能够储存当前需要储存数据,在 CP 发出一次时钟控制脉冲时,串口 Di 同时输入第二个需要储存是的数据,而第一个数据则储存到触发器 FF1 中。双向移位寄存器按图中方式排列,调换连接端顺序,可以控制寄存器向左移位,增加控制电路可以使寄存器右移,这样构成双向移位寄存器。
②特点:
寄存器又分为内部寄存器与外部寄存器,所谓内部寄存器,其实也是一些小的存储单元,也能存储数据。但同存储器相比,寄存器又有自己独有的特点:
a、寄存器位于CPU内部,数量很少,仅十四个
b、寄存器所能存储的数据不一定是8bit,有一些寄存器可以存储16bit数据,对于386/486处理器中的一些寄存器则能存储32bit数据
c、每个内部寄存器都有一个名字,而没有类似存储器的地址编号。
③用途:
1.可将寄存器内的数据执行算术及逻辑运算
2.存于寄存器内的地址可用来指向内存的某个位置,即寻址
3.可以用来读写数据到电脑的周边设备。
寄存器 | 用途 |
---|---|
eax | 累加寄存器,相对于其他寄存器,在运算方面比较常用 |
ebx | 基地址寄存器,作为内存偏移指针使用 |
edi | 在内存操作指令中作为“目的地址”使用 |
esi | 在内存操作指令中作为“源地址指针”使用 |
ecx | 计数器,用于特定的技术 |
edx | 作为EAX的溢出寄存器,(除法产生的余数) |
esp | 指针的寄存器,用于堆栈操作。被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。 |
ebp | 基址指针,指栈的栈底指针 |
汇编指令 | 对应操作 |
---|---|
push | 压栈 |
pop | 出栈 |
move | move A,B 将A移动到当前B的位置 |
call | 将程序的执行交给其他代码段(即函数的调用) |
lea | 加载有效地址 |
ret | 子程序返回指令 |
sub | 减法操作 |
add | 加法操作 |
经过上面关于栈与寄存器的简单解释,我们开始本文章的重点,对函数中的栈帧的创建与销毁的理解
为了方便我们理解,我们用下面的代码进行讲解:
#include 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("%d/n", c); return 0;}
这里我们使用后的VS2013编译器进行讲解,当我们使用不同的编译器去执行代码与进行理解时,会有一些偏差
①我们首先按F10进行调试,进入调试之后,我们按照下面的操作,调用堆栈窗口:
②当我们进入堆栈窗口之后,我们在程序中anF10进行程序,当程序进行结束之后,我们的堆栈窗口出现下图:上图的内容是告诉我们:
main函数在VS2013中任然是被调函数,main函数首先被_tmainCRTSartup()函数调用,而_tmainCRTSartup()函数被mainCRTSartup函数调用;
③根据上述内容,我们可以得出关于当前程序的栈帧简图:
①现在我们为了深入了解我们的栈帧是如何创建与销毁的过程,我们按照下图步骤进行操作:
此时我们获取了程序的反汇编语言,通过反汇编语言,我们才能够深入了解程序的进行过程与栈帧的创建与销毁过程,也正是通过对反汇编的分析,我们才能够解决我们引言中的问题;
①这里我们在进行对main函数的反汇编语言进行分析前,我们先对补充一些内容的讲解:
a、在函数的栈帧中,ebp和esp这两个寄存器是存放地址的,也是这两个地址用来维护函数的栈帧;
b、在esp和ebp这两个寄存器中,esp寄存器存放的是函数的栈顶地址,也就是栈顶指针;ebp寄存器存放的是函数的栈底地址,也就是栈底指针;
举例(如图):
②我们对main函数的反汇编语言进行分析:
压栈操作:将ebp移动到当前栈的栈底的位置,然后esp指针会自动上移,指向压栈进入栈的ebp
也就是将t_mainSRTSartup函数的ebp的值存放到栈顶,占用一个内存空间;
举例(如图):
③我们对main函数的反汇编语言进行分析:
将ebp指针指向当前esp指针指向的位置
通过监视窗口,从地址进行观察,可以确定,ebp指向了esp指向的位置,此时两个指针的地址值相同
④我们对main函数的反汇编语言进行分析:
将esp的值减少0E4h(也就是将esp指针上移)
⑤我们对main函数的反汇编语言进行分析:
实现步骤同上,从栈顶压入三个元素,分别为ebx、esi、edi(我们暂时不用理会这三个寄存器)
⑥我们对main函数的反汇编语言进行分析:
我们勾选lea(load effective address)加载有效地址,将ebp-0E4h这个地址上存储的内容加载到edi中
⑦我们对main函数的反汇编语言进行分析:
mov操作,将"39h"存储到ecx中;将"0CCCCCCCCh"存储到eax中
⑧我们对main函数的反汇编语言进行分析:
从edi开始,向下39h个内存空间的数据全部转化为0CCCCCCCCh
引言问题的解决:
在这里我们回顾初学C语言时,我们当时创建一个变量,却没有对其赋值时,或者在打印字符串时,没有找到’/0’时,我们却将其打印,得到结果中含有"烫烫烫"的字样,这种情况的出现,其实就是打印的初始化值"0CCCCCCCCh"
⑨我们对main函数的反汇编语言进行分析:
mov操作的执行,将"14h"存储到ebp-8的位置、将"0Ah"存储到ebp-4h的位置、将"0"存储到ebp-20h的位置
①我们对Add函数的反汇编语言进行分析:
将ebp-14h位置上的值移动到eax的位置上,即将ebp-14h位置上的值传递给eax;
压栈操作,将eax从栈顶压入;
将将ebp-8位置上的值移动到ecx的位置上,即将ebp-8位置上的值传递给ecx;
压栈操作,将ecx从栈顶压入;
同时这里操作,也是我们Add函数传参的操作
②我们对Add函数的反汇编语言进行分析:
call操作的执行,我们会压栈压入call指令的下一条指令的地址
这一步执行的原因,是当我们Add函数执行完之后, 我们返回结束时,要继续执行我们的下一条指令,所以我们进行记录我们Add函数的下一条指令的地址
我们在这里按F11进入Add函数,显示如下:
我们对Add函数的反汇编语言进行分析:
当我们进入Add函数之后,我们发现Add函数和main函数步骤相同,需要先为Add函数进行初始化,ebp、esp的函数栈帧维护等,也就是需要创建Add函数所需要的栈帧,数据类型初始化。
①压栈,压入ebp,其中这个ebp就是当前main函数的ebp
②mov操作,将esp的值传递给ebp,那么ebp就指向当前的esp指向的位置
③sub操作,将esp减去0CCh,使esp指针上移;push操作,压栈压入ebx、esi、edi三个元素
④lea、mov、mov、rep stos的执行步骤与main函数的初始化步骤相同
这时,我们对代码的内容近一步分析
这里的两个地址,我们通过图解分析,可以得出,之前的ecx、eax就是实现我们的传参步骤,分别为形参a’,b’;
⑤此时,我们将ebp+8位置的值传递给eax,此时eax = 20;然后再将ebp+0Ch位置的值添加到eax中,此时eax = 30;再然后,将eax的值传递到ebp-8的位置;
引言问题的解决:
形参是实参的一份临时拷贝,当我们在还没有在main函数中执行到Add()函数时,我们已将b,a的数值压栈进入栈中,而这两个压栈进入栈中的数据,就是b、a的一份临时拷贝,当我们执行到Add函数时,我们就找回了之前压栈存储的b、a的值,所以我们说形参是实参的一份临时拷贝
当我们在函数中改变这份临时拷贝的数值时,对我们原本的实参并不会改变,因为函数中改变的只是实参的一份临时拷贝,所以我们说值传递的形参改变,并不会改变实参
⑥将ebp-8的值传递给eax中
⑦pop操作,将栈顶元素弹走;这三行代码的执行效果为,依次将栈顶的元素弹到edi寄存器中、esi寄存器中,ebx寄存器中
此时的栈顶情况如图:
即将edi元素弹回,存储到edi寄存器中,esi、ebx同理。
执行三次pop操作之后
此时,我们的Add函数的任务已经执行结束,获得的return值也已经存储到eax中,那么这个时候,Add函数的函数栈帧就没有存在的必要了,我们需要对这一段空间进行回收
⑧让esp指针指向ebp的位置
⑨将当前栈顶的元素弹出,弹到ebp寄存器中
而当前的栈顶元素存储的内容就是main函数的ebp,那么效果就相当于
此时,Add函数的栈帧已经收回,我们又回到了main函数的栈帧中
⑩前面的过程经历之后,我们的call指令就已经执行完毕,但我们任然需要继续call函数之后的步骤,而此时ret操作的执行,就是让我们从call指令执行完之后,返回到我们之前存储的call指令的下一个指令地址的地方。继续执行call指之后的指令。
现在我们又回到了call指令之后此时main函数指针中的形参20、10就没有用处了,那么我们就要将这两个空间进行回收
将"esp"+8,即就是将esp指针下移八个字节
此时,也就是形参销毁的真实时刻
将eax的值传递给ebp-20h中
我们回顾main函数中的栈帧
那么将eax的值传递给ebp-20h中,则
这里我们可以得出,当我们调用返回函数时,返回值都是先存放到寄存器中,然后当我们真的返回到调用函数中时,再从寄存器中读取这个返回值
到此,余下的部分关于main函数的栈帧空间的销毁与Add函数相同,就不在此叙述了,执行的步骤与前面分析内容形似。
以上就是我对函数中栈帧的创建与销毁的个人理解
上述内容如果有错误的地方,还麻烦各位大佬指教【膜拜各位了】【膜拜各位了】
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/120822.html
摘要:这里分块讲解六函数栈帧的销毁过程一解析的作用是将栈顶的数据弹出,弹出数据储存到相应寄存器中。 ?前言? 读完这篇博客,你可以明白什么? ①局部变量到底是怎么在栈上创建的? ②为什么局部变量不初始化为随机值? ③函数是怎么传参的?传参的先后顺序是什么? ④形参和实参是什么关系? ⑤函数调用是怎...
摘要:目录前言由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教前言由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教作者新晓故知作者新晓故知那些代码背后的故事那些代码背后的故事通过 目录 前言:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚...
阅读 2891·2021-10-14 09:42
阅读 1244·2021-09-24 10:32
阅读 2950·2021-09-23 11:21
阅读 2839·2021-08-27 13:10
阅读 3327·2019-08-29 18:41
阅读 2194·2019-08-29 15:16
阅读 1193·2019-08-29 13:17
阅读 892·2019-08-29 11:22