这是一篇在自己写了很久的文章了,因为要清理电脑,放在博客上当备份。
写在前边:
栈帧结构对于初学C语言着,可能有些陌生。但是不能否认的是栈帧在C语言中扮演着很重要的角色,下边就栈帧分析下内存。如果真对C语言感兴趣的请耐心的看下边的叙述。
栈帧说的简单点就是调用函数的过程中,为这个函数开辟一个栈空间,用来保存这个函数中的临时变量。而这个存储函数临时变量的空间就被称为栈空间。而这个栈空间就被称为栈帧。
*******************************************************
索引:
在分析栈帧前还需要简述几个简单的知识点。(在栈帧的分析中我们会用到的)
EIP:EIP又称为PC指针寄存器。EIP中存储的是CPU正在执行指令的下一条指令。指令寄存器(extended instruction pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。 可以说如果控制了EIP寄存器的内容,就控制了进程——我们让eip指向哪里,CPU就会去执行哪里的指令。eip可被jmp、call和ret等指令隐含地改变(事实上它一直都在改变)(ret指令就是把当前栈顶保存的返回值地址 弹到eip中)
esp:栈顶寄存器,存放了指向函数栈帧栈顶的地址。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
Ebp:栈地寄存器(又可称基址寄存器),存放了指向栈帧栈底的地址。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
在这块就需要简单说一下函数的内存图了:
(&&对这就个图的介绍):内存中地址由小到大,先可以简单的化成上边的这样一个区域,此次主要研究的是堆和栈区,下边的内存暂不仔细去研究(下边内存并不全面,只是取了几个代表)。
栈区和堆区是相对而生的,堆区是向上生长的,栈区是向下生长的。堆区和地址空间的大小排列一样,而这快容易出问题的应该算栈区了。而我们下边会仔细去叙述一下堆区的。
而这里需要强调的一点是堆区和栈区之间有一段空白,堆区和栈区不是相连的区域。
上边说了这么多的话语了,现在简单粗暴一点,直接上代码。
我们运行的代码在VC6.0环境下边,原因只是因为编译器环境简单。
定义一个main函数,定义两个参数a,b。在main函数中调用一个函数使得两个数相加,暂且将这个函数称为Add,最终返回到main函数中。打印出结果。
函数如下
暂时定义a和b为这样,为了等会更直观的展现出来。代码很简单现在进行分析。
程序执行进入汇编语言过程中。(汇编语言的指令不详细介绍了)
栈向下生长,因此ebp栈底反而在地址大的地方,esp栈顶在地址小的地方。
这张图片所描述的是栈区的放大空间。
栈是向下生长的,因此这块需要注重的一个细节是ebp栈底指针反而指向地址大的地方,esp栈顶指针指向在地址小的地方。
调用出汇编代码,函数从main开始,前边的预处理暂时不仔细去研究,简单来描述,预处理给main分配了一个空间,并全部初始化了。如果真对这段汇编代码感兴趣,可以查阅相关的汇编语言的书籍。
当代码进行到add时时在栈空间中已经进去了a,b,以及定义的ret。在这块需要注意的是mov指令将a移动到【ebp-4】即ebp指针下移指向a,将a的地址放进去。b移动到【ebp-8】中即ebp指针指向了b的地址。
这块需要说的和之前一样栈中的生长方式是向下生长的,因此地址由高到低,一次存放。即实际上就是给a先开辟空间,再给b开辟空间。
此处的push即压栈的过程,而eax是cup里的通用寄存器,此时的eax里放的是esp的地址。即栈帧的压栈过程是往栈顶压栈,通用寄存器里边放的是esp的地址。此时就是最关键的地方了。在理论上的栈先进后出,就在这用直观的代码显示出来了。
(esx,ebx,ecx,edx都是通用寄存器,在CPU中起一个存储临时变量的作用)
eax存储的是【ebp-8】,即b的地址,即从汇编语言来看就是将,b的地址压栈进,esp顶部。而a也是这个原理根据汇编代码可以分析出,a在b的下边。即根据汇编代码可以分析画出下边的图。
此时的esp指向了压栈a的地址空间。
这个过程返回到编辑器,调用一下寄存器,与内存就可以清晰的展现在面前。
此时的调用esp栈顶的地址,a,b的地址展现的很清楚了。而这块还需要注意的是栈是由上向下生长的。
这块需要知道一个汇编语言的命令。
Call命令:(两个作用)
1、将当前执行指令的下一条指令的下一条地址压入栈中。
2、随机call就会跳转(jmp)至指定函数代码块去执行。
此时的界面是调用玩call指令之后的图,即现在的栈顶将add这个地址压栈进去。可看见esp的最上端是add的地址。
上图就是调用到add函数的代码。跟main有异曲同工之处。
此时的eip开始调用add()函数中的值。
即在空间图上可以分析为在空间上在分配一个空间用来存放add函数的临时变量。
Add函数就是跟上边的main函数一样的存放在属于它自己的栈帧空间里的。具体的就不详细描述了。可以跟着上边的main对比来看一下。
而在这块 add函数返回的这块的仔细的剖析一下。
在函数add中,a,b的和赋值给z,而将两个数所加之和赋值给,通用寄存器eax中,暂时存放起来。
在函数的结束部分,有这样一段汇编代码。这个过程就是栈的释放过程。
将ebp的地址赋给了esp.然后释放了ebp的空间,即main:ebp被释放。Ebp返回到main函数中的ebp地址中去。而原来的main:ebp空间是空的所以向上自动跳一格。
而最后的ret这块需要强调一下。
Ret命令的两个作用:
1、弹出pop栈顶
2、弹出栈顶的地址,并且将返回值放入EIP中。
则此时的ebp,esp又返回到之前的地址。到现在,栈的形成与销毁就完成了。
得出一个函数的栈空间都是自己开辟的,使用完后销毁释放空间。