目录
1.局部变量是怎么创建的?
2.为什么局部变量不进行初始化就会是随机值?
3.函数如何传参和传参的顺序?
4.形参和实参到底是怎样的关系?
5.函数如何进行调用的?
6.函数调用结束之后是如何返回的?
相信在学习的过程中,你对上面的问题或多或少都会有些困惑,今天的博客--函数栈帧的创建和销毁就可以帮助你解决这些困惑;
这些都是和函数的栈帧的创建和销毁有关,这个函数栈帧在不同版本的编译器有关,略有差异但是大致相同,我们使用的编译器版本越高,越不容易观察找这个函数栈帧的过程,我们这里使用的是vs2013为例:
1.关于寄存器你应该知道的
我们之前学习的时候了解过,这个寄存器有eax,ebx,ecx,edx等等,我们在学习函数栈帧的时候,经常使用的两个寄存器就是ebp和esp,这两个寄存器存放的是地址,存放的这两个地址用来维护函数的栈帧;
2.函数栈帧的初步理解
每一个函数的调用,都要在栈区开辟空间,在栈区里面,我们会优先使用高地址,再使用低地址;我们的main函数开始执行之后,就会开辟main函数的函数栈帧,ebp esp分别指向的就是main函数的函数栈帧的边界(如图所示);我们可以把这个函数栈帧创建的过程理解为一个盖房子的过程,我们就会从低向高处盖房子,我们的ebp指针也被称为栈底指针,esp也被称为栈顶指针,我们现在维护的是main函数的函数栈帧,当调用其他的函数的时候,这两个指针就会维护其他的函数的栈帧空间;
我们还需要了解的就是main函数其实也是被其他的函数调用的,那么main函数是被_tmainCRTStartup函数调用的,_tmainCRTStartup函数也是被mainCRTSartup函数调用的,我们了解就可以了,这样就可以让我们对于main函数的理解提高一个等级,而不是简简单单的只是一个主函数的概念;
3.简单的了解反汇编
上面展示的就是一些基本的反汇编代码,我们同样需要了解一些的,这样才方便我们对与函数栈帧创建和销毁的过程的理解;
我们对于栈这个空间,我们需要了解的预备知识就是压栈和出栈这两个专业术语:
push压栈:就是在栈的顶部放上数据;
pop出栈:就是从栈的顶部删除数据;
(1)上面的反汇编语言的第一句就是push ebp这句代码的意思就是把ebp进行压栈的处理,像下面的图片展示的那样,我们的ebp就放到了这个栈的顶部
(2)第二句move肯定就是移动的意思呗,那么移动什么,如何进行移动呢?我们这里是把esp的值给ebp,我们的ebp原来是指向栈的底部的(不是下面的图片,下面的图片展示的是最后的结果),就是我们的ebp不是压栈了吗,每次进行压栈,我们的esp都要进行移动,因为esp是栈顶指针,那么把esp赋值给ebp就相当于是ebp指向esp的位置,也就是说我们的ebp指向了新的栈底
(3)第三行的esp减去oE4h这个就是在给main函数创建栈帧,我们esp减去一个值就是向上移动,移动到一个新的位置,移动的距离就是0E4h,这个时候ebp和esp各自指向了新的栈底和新的栈顶,我们这个时候就完成了main函数栈帧的创建;
(4)接下来反汇编里面是3个push,这三个都是进行压栈的操作,压栈完成之后的栈帧情况如下图所示(我们不需要了解这3个东西是什么,只需要了解这个过程就可以了),显然,这个过程里面每次进行压栈,esp都是同步进行移动的;
(5)接下来进行的是下面的这四行代码,
lea的全称是load effective address翻译之后就是下载有效地址空间;
dword是double word翻译过来就是双字,一个word是2个字节,double就是两倍的意思,double word就是4个字节;
两个move是在进行一个什么样的操作呢?39h代表的是次数,0cccccccc表示的是复制内容,经历过38h次之后,我们的main函数的栈帧里面放置的全部都是ccccccccccc这些内容(像图片里面展示的那样)
(6)接下来到了变量的创建,我们在图里也已经标注了出来,我们创建的三个变量abc分别初始化的数值就是10,20,0,图片里面就已经进行了初始化;
这里我们就已经可以解释一个上面的问题了,为什么我们强调好的代码风格是一定要进行变量的初始化的,因为我们初始化之后,我们初始化的数值就覆盖掉了原来的cccccccccccccc,但是如果我们不进行初始化而是直接进行打印,打印的结果像“烫烫烫烫烫烫烫烫”这种,相信在初学的时候你也一定遇到过这种情况,这些“烫烫烫烫”的实质就是ccccccccccccccccccc,如果我们的进行变量的初始化工作,就不会打印产生这样的随机值;
4.函数调用的传参过程
(1)我们走到add函数这一步之后,执行的操作是mov+push+mov+push这几步骤就是把bep-14h这个位置的值(就是我们的b)传递给eax,然后push这个eax,就是进行压栈,相当于把b=20这个数据压栈到了栈的顶部,下面的两行相信你也已经猜到了,就是把我们的a=10压到栈的顶部,压栈完成之后的情况如下图所示:
(2)接下来我们的就要进入这个call指令,call指令的作用就是记住call指令的下一行指令的地址00C21450为什么要记住这个地址呢,因为从下一步开始,我们就要进入add函数的内部了,经历了一系列的操作之后,我们就要回来吧,回到那一步指令呢?肯定是回到call指令的下一步指令,我们记住这个指令的地址,就可以调用结束的时候找到我们要执行的指令00C21450;
(3)接下来就进入了add函数的里面,里面的指令如下图所示:这个里面我们可以发现这些指令和我们的main函数当时的指令是一样的,就是进行add函数栈帧的创建;虽然过程是一样的,我们还是进行回忆强化一下:还是进行ebp和esp的移动,指向栈顶和栈底,把ebx esi edi进行压栈(这个过程和main函数栈帧的创建的过程是一样的),然后就是进行初始化,全部初始化为cccccccccccccccccc;
(4)接下来通过指令我们可以看到z这个变量被创建,初始化为0,但是我们的z=x+y,但是这个过程中x和y好像并没有出现,这个时候我们上一步压栈的a和b就要发挥作用了;
我们通过下面的指令也可以知道,就是z=x+y那一行里面,实际上这个ebp+8找到的就是我们当时压栈的a=10;ebp+0Ch实际上找到的就是b=20这个值,我们的ebp+8就是10放到eax里面,这个时候的eax就是10,然后我们的ebp+0Ch就是b=20放到eax里面,eax里面原来就有10这个时候把20放进去,就可以得到我们想加之后的结果30这个实际上就已经进行了我们的相加的操作,实现了这个函数的功能;
(5)下面展示的就是add函数栈帧创建之后的情况,这个里面是已经被全部初始化为cccccccc了
5.函数返回值到底是如何返回的
(1)return z指令的后面的ebp-8实际上就是我们的计算结果30,我们把这个数据存到eax这个寄存器里面,这个寄存器是不会随着add函数栈帧的销毁而消失的;
(2)返回之后的指令是,pop就是我们前面已经铺垫的出栈的操作,edi esi ebx全部出栈,这个时候我们的add函数的栈帧就应该被回收掉了,下面的一句指令指令就可以回收掉这个空间;
esp,ebp的意思就是把ebp的值给esp相当于是esp直接来到了栈底,这样就可以把这个add函数的空间回收掉了;
下面的就是add栈帧回收之后压栈情况:
(3)接下来我们的栈顶ebp是main函数的,这个时候ebp就找到main函数的栈底,这个时候的栈顶就变成了00C21450,这个指令还记得是什么吗,就是我们调用函数之前记住的指令,方便我们对调用完之后找到下一个指令,这个时候就发挥了作用;
(4)接下来的指令esp,8代表的是esp加上8,就相当于是放掉了ecx=10,eax=20这两个东西;
ebp-20其实就是我们的main函数里面的c这个变量,eax刚刚存放的是我们的z的30,这个时候把eax里面的30给了c就相当于是实现了参数的传递,接下来就是main函数空间的回收(同add函数相似);
6.总结回答开始的问题
(1)我们了解到了局部变量时怎么创建的:就是覆盖掉了原来的cccccccccc,我们的形参是压栈的,而且x和y是在main函数的栈帧里面,add使用的时候是到main函数栈帧里面找到这两个值,计算完之后把这个值存到eax里面,最后这个eax再赋值给main函数里面的c;
(2)我们的函数调用的时候,形参xy是压栈的,和我们的实际的xy并不在一个地方,因为xy是在栈帧里面创建之后压栈上去的,所以形参是实参的临时拷贝,形参的修改不会影响实参(因为我们的add函数使用的是压栈的xy并不是我们最开始创建的xy);
(3)我们的函数调用完成之后,会找到栈顶存放的call指令的下一条指令,进行后续的过程,这个地址的存储使用,返回值整个过程都是十分严谨的,既可以调用函数,调用完之后还是可以回到原来指令的下一步的。