函数,一般都有返回值,函数名,参数,再下来还有什么mian函数,函数写出来就是要被调用的,上面图片上的代码,main函数和myadd函数,都要在自己的栈结构什么形成自己的栈,可以帮我们理解局部变量,为什么是临时,栈上开辟空间,栈的地址是向下增长,堆也学了,堆向上增长,每调一个函数,就是形成栈帧的一个过程,返回函数是栈帧被释放的一个过程,释放不是真正的释放,而是让这块空间无效,下一次创建栈帧的时候可以覆盖掉。
局部变量为什么具有临时性,是因为在对应的函数内栈帧上面创建的,函数被返回时,栈帧结构都被释放了,赖以生存的环境都没有了,所以变量也被释放了。
今天就来看看栈帧的形成与释放。
把一个函数的生命周期理解,其他都好理解了,因为从main函数开始都是函数调用,一个函数的生命周期搞定了,整个生命周期都搞定了。
我直接从myadd开始,就预设main函数有自己的栈帧了,只要myadd理解透了,其他的都搞定了。
先来了解一下东西寄存器和一些简单的汇编一下命令
寄存器最重要的就是后三个,ebp、esp,有了这两个寄存器,就可以有效指向一块内存,eip保存着当前指令的下一条指令的地址,本质就是衡量我们指向到了某个位置
先把代码和图画出来,画出内存分布图,我们栈帧结构主要研究栈区,所以把栈区放大,栈区地址是往小的增的。
代码启动调试之后一路f10,进来 ,看图片的右上角,出现了几个隐藏的函数,所以main函数也是一个函数。main函数是被谁调用的。
是被_tmianCRTStartup()调用的,但是它也是函数也谁调用它
它也是被 mainCRTStartup调用,只是做了一下cookie
mainCRTStartup它也是函数呀,它被谁调用?记住一件事,要被执行,第一步加载到内存,第二步就要开始执行,开始之前永远都是,操作系统来做,调mainCRTStartup,这里就是调用边界了。
所以我先研究add的栈帧,就是那个绿色的方格,只要一个清楚了其他的都清楚了,好,准备工作完成。
我重新画一张图,除了栈,还有cpu和三个寄存器,前面说了
esp和ebp指向main的起始位置和结束的位置,也就是地址,
eip先代表着执行者main的代码
好前期工作完成 ,启动反汇编,
直接来到这里 ,意思是esp-8个字节定位的位置吧0Ah,放到我们的栈帧当中,一条汇编做了两件事,开辟空间,完成初始化
看eip寄存器指向的位置,还没有开始执行,
程序从main开始执行,这一步完成之后,ebp-8就是栈底向上偏移8,所以x的位置就在这里,就是下面这样的,ebp-8是这块栈内存里面的一个偏移。x区域的起始地址,在上面位置因为内存排布是有低到高的 ,但是这里的内存是反着的,看地址排布,
起始地址是往上减少的,栈空间就往底地址增长的。
所以有一点要注意的是的起始地址是在上面
eip的是执行当前main函数的代码
然后下一条,[ebp-14h],ebp不变,减14,把y放到栈上
这一步完成之后,是这样的但是有一个注意的地方就是黑色的线是esp-14的位置,但是内存指向的话应该是指向最最小的内存,起始地址是从小的开始,所以y的起始地址,在上面位置因为内存排布是有低到高的 ,但是这里的内存是反着的,看地址排布,
下一步,f10,把0,放到ebp-20的这个位置,eip指向了1883的地址处,
执行完之后,然后z也入栈了
但是会发现一个问题就是y和z是靠着的,x和y是分开的,这是和编译器有关的。因为空间是随机的,所以也有可能是镂空的,应该是防止程序员猜测一些地址。
在接下来。到了,myadd的位置,可以发现一条C语言的指令,可能是有多条汇编指令
接下来,一个是eax是一个寄存器,还有一个是ebp-14
下面可以看到,ebp-14是y的地址,也是说要将y的里面的值放到eax的寄存器里面
来看一下,这里要把内存窗口打开,当我们按一下f10,到了push这里,push就要进栈,看一下esp的位置,也就是栈顶,
按一下f10,注意到esp的位置右8c变成了88,减少了4个字节,并且0xB已经在88位置,也发现了一个问题ebp的88已经变成新的栈了,B值已经压到了栈上了
所以,栈顶的位置,由内存图可以看到,是直接压在原来栈顶之上的位置,栈顶的位置也发生了改变,也压进来了。
相当于,把y的值拿到了寄存器里面,再从寄存器放到下一个位置。好像就是y的一份拷贝
下一步,这里和上面一样,需要一个寄存器ecx,ebp-8的位置是x的地址,也是把x的值放到ecx里面。
中途遇到了点问题,重启之后,地址变了。
中途遇到了点问题,重新了调试按一下f10,注意到esp的位置右c8变成了c4,减少了4个字节,也可以看一下ECX的寄存器值是00000A了,放进去寄存器里面了并且0xA已经在c4位置,也发现了一个问题ebp的c4已经变成新的栈了,A值已经压到了栈上了,
所以,栈顶的位置,由内存图可以看到,是直接压在原来栈顶之上的位置,栈顶的位置也发生了改变,也压进来了。 相当于,把X的值拿到了寄存器里面,再从寄存器放到下一个位置。好像就是X的一份拷贝
所以下面这四个动作,是做了一个临时拷贝
两个总结:
1.临时变量的形成是在函数正式被调用之前就形成了的
2.形参实例化的顺序是从右到左的
接着下来,到了call了,call的作用就是函数调用,1.压入返回值地址 2.转入目标函数
call调用函数,只需要修改eip的值就可以了,但是调完之后我还要返回,所以要不保存返回值地址,返回到call后面的add,为什么要压入返回值地址:根本原因就是函数掉完,之后需要返回。
看图片,解析,ESP会保存00f110f0de1这个跳转地址,并且把add的地址压栈,压到c0位置
按f11,发现返回值已经把栈的位置已经压入了,这里的地址压进去了,eip也变成了00f110f0,
这里要jmp, jmp就是跳转到函数,eip上的地址就是jmp前的地址,jmp后eip的地址就要变成00方18f0,
jmp后,eip就是00f118f0的地址就是,就是已经进来函数里面了,从现在就正式进来了,可以知道00f118f0就是函数的入口
完成后, eip位置就不再是mian了而是mydd了,已经进到myadd里面了
到这里,就已经完成了函数调用前的准备了, 形参列表初始化,形参列表完成,返回值入栈,eip跳转函数,怎么运行,下面准备开始myadd函数,接下来重点就是前三个。这三个就是栈帧最核心的内容。
第一步, push ebp是什么意思, ebp是什么?ebp不就是main函数的栈底吗,所以这里要做的就是把main函数的栈底入栈,
按一下f10,看到了把。已经是压进去了,
所以图就变成这个样子了,进去之后,esp的位置一定要改变,
第二步mov ebp,esp,意思就是把esp里面的内容,放到ebp里面去,esp原本是指向是main函数的栈顶,那如果直接放进去不会覆盖原来的吗,那到时候怎么找回来,不用担心,因为ebp的值在刚刚已经入栈了,
按f10 ,看到esp和ebp一样了,所以是是放进去了
所以,ebp不在执行main函数的栈底,所以栈顶和栈底都指向栈顶
sub是减的意思, 0CCh,意思是栈顶esp减去0CCh这个空间,然后把结构放到esp,0cch这里这里减多大,是和我自己当前的函数规有关系的,多变量就减的比较大,反之就少。
目前esp是00A2F794,按f10,就变成了00A2F6C84
所以esp减少了,是不是就指向了一段新的空间,ebp不变,变成如下这个样子
esp减去了一段范围,所以就变成了这个样子了 ,形成了myadd的栈区,esp和ebp也不是main的栈了
所以回到这张图,main有自己的栈帧,myadd有自己的栈帧
所以接下来直接到int c = 0这里,中间那些事完成一些初始化的问题,就不看了。
C是myadd里面定义的一个变量,和前面一样, 在ebp-8的地方把c放进去,ebp-8就是以ebp为参照物减去8个字节。
所以0就进来了
接下来,下一条指令,就是eax,dword ptr [ebp+8],是什么意思呢?
ebp-8就是以ebp为参考减去8个字节向上走,那加就是反着走,所以ebp+8就是刚刚找到a的值,并且放到eax里面,
重点:内存取出来的一定是连续地址最小的哪一个
add eax,dword ptr [ebp+0Ch],就是找到ebp+12的值和eax的想加,那这个位置是谁,刚好是b的值,eax刚好把a的值拿出来,最后两个相加,然后结果放回到eax
重点:内存取出来的一定是连续地址最小的哪一个
ea寄存器已经是15了,15是16进制,转回10进制就是21.
所以下面这一步是将eax的值写回到ebp-8的位置上,而前面ebp-8,就是c的位置
所以执行后是这个样子的
到此栈帧已经创建结束了,下一步就是return 返回了
经过前面分析知道函数栈帧是自己形成的,但是这里的sub减多少是谁决定的?答案是编译器决定的,只要你定义变量就要有类型,只要知道类型了编译器就知道给你开多大了,所以sizeof是在之前就计算好的了
开始return返回,如何理解return,放回值,都是通过eax和ecx等等这种通用这些寄存器返回的,
我只看这四条指令,中间那些pop只是配置一些寄存器的和上面的push只是一样的,重点是圈起来这四个
mov eax,dword ptr [ebp-8]这个很简单,就是把值放到了eax上面
下面三条才是重点
mov esp, ebp就是把ebp的值放到esp里面,ebp是myadd栈底,所以把myadd的栈底放到esp里面
相当于esp和ebp都是向了同一个地方,,所以相当于就是把myadd的空间给释放了,所以只需要让esp执行ebp的地方myadd的整个空间就释放了,
接下来就到了这个了 pop ebp 把当前栈顶的值弹出去,弹到ebp哪里去,
弹出去后,栈顶就指向了下一个地方了。
弹出后,栈顶就指向了返回值哪里,代表着什么
ebp指向原来的main栈底了
ret就是将返回值地址返回到eip,这个返回值地址就是但是call的下一条指令地址,因为和pop一样,所以栈顶也要退回下一个所以变成这个样子
下图可以发现ret之后栈顶是变大了
可以分享已经返回到了这里
打×的地方就是栈已经释放了
那还有两个怎么办?可以这里 add esp,8,意思是说栈顶的位置向下加8个字节
就这样就把两个空间给释放了
所以这就回到了main栈帧,main函数了,所以一次完整栈帧创建和释放