目录
一:简介
很多朋友在学习函数的时候都会发出疑惑,函数是怎么样创建的怎么样销毁的,变量又是怎样创建销毁的,什么是函数栈帧,函数栈帧就是函数在调用过程中在栈帧中开辟的空间,这些空间是用来存放
函数参数和函数返回值
临时变量(包括函数产生非静态的其他变量)
保存上下文信息
为什么说形参是实参的一份临时拷贝,在这里我将同大家一起去了解函数和变量在内存中究竟是以为什么样的形式存在的,当出了生命周期又是如何销毁的。
二:寄存器
在我们开始了解函数变量是怎样创建销毁之前首要我们要知道寄存器这个概念,什么是寄存器呢,我们说是一个高速缓存器,我们说电脑的运行速度依次是硬盘——>内存——>缓存——>寄存器——>CPU。寄存器中有这样几个东西
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保留当前指令的下一条指令
要明白函数在内存中是怎样进行的我们还需要知道一些汇编命令
mov:数据转移指令
push:数据入栈——同时esp的栈顶也会发生变化
pop:数据出栈——esp也会发生变化
sub:减法命令
add:加法命令
call:函数调用。1;压入返回地址 2;转入目标函数
jump:通过修改eip,转入目标函数 ,进行调用
ret:恢复地址,压入eip,类似pop eip的指令
三:什么是栈帧
在计算机科学中,栈被定义为一种特殊的容器,用户可以将数据放入栈中——压栈(push),也可以把数据从栈中拿出来——出栈(pop),但这些都要遵守一个规定先入的后出,后入的先出。
四:main函数的创建会经历哪些
接下来我用一段代码为大家具体讲解
我们都知道在程序开始之前都需要写一个main函数,那么有咩有想过这个main函数哪里来的呢。这里我们用VS2019做例子,当我们开始调试的时候,按出调出堆栈,然后就可以看到main函数其实是由 invoke_main() 这个函数调用的
我们就知道了,当我们调用main函数之前,mian也是会有别人来调用的,至于invoke_main()这个函数又是谁调用的我们先不用管。invoke_main()是一个函数那么他也有自己的栈区,那么main和ADD也会有自己的栈区也都有ebp和esp区维护。
接下来这里我将着重为大家讲什么是ebp什么是esp。简单的来说,这两个东西是用来维护函数栈帧的地址的。而edp又被称为栈底指针,esp被称为栈顶指针。就比如说,我们写一个C程序的时候都是从main函数开始的,那么main函数的的这块空间的地址就是由ebp和esp来维护
然后按下键盘上的f10,右键点击代码转到反汇编,我们重反汇编的角度来分析一个函数重创建到销毁都会经历哪些步骤。
我们看到第一条
1:push ebp:这是什么意思呢,ebp是栈底,push是压栈,也就是说把 ebp寄存器的这个值给压进去
2: mov ebp,esp:这句话的意思是把esp寄存器中的值赋给ebp寄存器,此时移动之后的ebp和esp的值是相等的,我们可以看一下在监视器中,当我们执行第二个语句之后得到esp和ebp确实是相等的
3:sub esp,0E4h:这是说sub这个指令会让esp的地址减去一个十六进制的0xe4,产生新的esp,此时的esp就是维护main函数的esp,结合刚刚得到的ebp,就产生了一块新的地址,那么这块地址就是为main函数预开辟的一块空间
4:push ebx
push esi
push edi
这三个指令保存了三个寄存器在栈中的值,在函数调用的时候可能会修改其值,所以先保存一下
每压栈一次esp都会去到最顶部。
5:lea edi,【edp-24h】 把edp的位置减去24h之后放到edi的位置
6:mov ecx,9 把9放进ecx中
7:mov eax 0cccccccch 把0cccccccch放进eax中
8:rep stos dword ptr es:[edi] word表示两个直接d表示double连起来表示4个字节,这句话的意思就是把从(edp-0x2h)到ebp这一段的内存的每个字节都初始化为0xCC。
到了这里我们的main函数才算创建好了。
题外话:所以当我们有时候去打印一个没有初始化的数组得到一组烫烫烫烫这样的二进制就是因为这个数组刚好创建在了我们初始化为0xcc的位置
五:函数内部实现原理
走到这里我们终于完成了对main函数栈区的开辟,接下来就是在main开辟完的栈区变量又是怎么样创建出来的
mov dword ptr [ebp-8],2:这句话的意思是把2放在ebp-8的位置上,也就是把变量a再在这个位置
同理变量b的创建就是把它放在了[ebp-14h]的位置,变量c就创建在[ebp-20h]的位置。
六:函数的是如何传参和调用
当接下来就进入到函数的传参,首先我们看第一句
eax dword ptr [ebp-14h] 我们看到[ebp-14h]不就是我们b的位置吗,意思就是把b放入eax里面,然后我们在看下一句push eax,就是说把eax压入我们的栈区,此时的eax里面就是b的值
下面一条语句就是把[ebp-8]的值放入esi里面,然后在压入esi。
大家有没有发现这个过程很像函数的传参,实际上它就是在传参,并且函数传参是在函数调用之前从右至左开始传的。
接下来我们来到了call调用指令这里,当执行call语句的时候,回去调用ADD这个函数,但是在执行调用ADD函数之前会把call的下一条指令做一个压栈的操作。
然后我们就进入到了ADD的函数内部
当我们看到函数内部的汇编代码好像和main函数是一样的啊,都是先把ebp进行压栈,然后把esp的值带给ebp,再让esp减去一个地址指向另一个地方,在把ebi,esi,ebxpush进去,把ecx向下的3个整形初始化成0cccccccch,最后ebp和esp分别就形成了栈底和栈顶。
然后我们就看函数内部究竟是怎样实现这样一个功能的
然后就是变量z的创建,我们看到0这个值被放在了ebp-8的位置,实际上就是变量z放在了这里。
紧接着下面一句,mov eax dword ptr [ebp+8] 意思也就是把ebp+8位置的值赋值给eax,ebp+8的和ebp+0ch的位置不就正是我们刚刚把a,b传过来的位置吗。当把这两个值都放eax里面在执行add的时候此时a和b的值已经相加了,然后下面的一句就是把eax的值放在ebp-8的位置也就是变量z的位置。
终于来到我们最后的环节,此时的函数已经完成了对它的作用需要把计算的值带回来
有人会好奇问,这个return z 回来的时候Z已经出了生命期,应该被销毁了。我们看下一句,它说把ebp-8的值放在eax里面,eax是一个寄存器,寄存器不会被销毁。
七:函数的销毁
当函数执行完后返回之后,那么这个函数是怎么销毁的呢。
我们先看第一条,pop edi pop就是弹出的意思,把栈顶的edi弹出一下它还是edi,但是每弹出一次这个esp就会地址++一下,那个这个edi就不属于我们了
然后在把就是add esp+0cch,这也就是说我们开辟的ADD这一块函数空间就全部还给内存了。
最后来到我们的主函数里面,刚刚我们开辟空间的时候看到的是esp的值赋给了ebp,现在我们用完了它,需要释放main函数的空间了也就不需要ebp和esp来维护了,我们就可以把ebp的值给到esp,然后在pop 一下ebp那么此时的esp就指向了栈底,中间已经没有需要维护的栈区了。最后在把一个0返回给main函数内部。这样我们就大致清楚了函数变量从创建到销毁的完整过程了。
题外话:拓展了解
其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一 般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中 通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。