上学期初略地了解了一下堆栈知识,还以为暂时够用了,没想到这学期assignment2遇到了内存管理专项训练,才发现漏洞百出,知之甚少。这几天查阅了大量的相关资料,为了确保我确实是学到了,整理凝练一下,防止日后要用时又要重复查阅。
一、首先是堆栈的概念
C/C++编译的程序内存占用方式大致如下图:
数据结构:
1.栈区(stack)
由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于中的栈,其地址有高地址向低地址延伸,即栈底的地址最高,而栈顶是浮动的,具体出入栈情况后面再总结。
2.堆区(heap)
一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。常见的在堆上申请内存的方式有malloc,calloc.realloc,new等,堆的地址是由低地址向高地址增长。
int a[100] = {0} // 这是在栈中的
int a[100] = malloc(sizeof(int)*100); // 这是在堆中的
3.全局区(静态区)(static)
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,
未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。-程序结束后由系统释放。(提一句:静态局部变量出了作用域后仍然占用内存,不过只能在函数内被访问)
4.文字常量区
常量字符串就是放在这里的。 程序结束后由系统释放
5.程序代码区
存放函数体的二进制代码
二、栈帧
1.概念
借用百度百科,C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。栈帧又名活动过程记录,通俗来讲,栈帧就是在栈上的用来表示程序的函数调用记录的一种数据结构。
2.ebp与esp
是两个寄存器,ebp指向当前栈帧的底部,esp指向当前栈帧的顶部,需要注意,底部地址高而顶部地址低。每一个函数栈帧的形成正是通过系统操作这两个寄存器来完成的。
3.过程详解
下面我们就来看一下栈帧形成的具体过程:
选用这次作业第一题的代码,经过优化后如下:
为了让这个栈看起来更符合物理定律┌(。Д。)┐一些,我决定将上上图的倒过来:
首先是main函数栈帧,因为main中只有foo函数,所以main的栈帧中没有参数入栈,ebp指向目前栈帧(main栈帧)底部,esp通过执行esp-4向低地址处移动,腾出空间,放下下一个指令的地址(如果下一个函数有返回就是其返回地址,如果没有就是下一条语句的地址)每一次入栈,都会有esp-的活动,以腾出空间。
随后执行
push ebp
//即将当前(main)函数的ebp入栈,目的是进入下一个栈帧前留一个上一栈帧的栈底地址,这样才有路找回来。
mov ebp,esp
//将ebp挪到esp的位置,开始构建下一个栈帧了(上图已经跳过了这一步骤)。
现在我们进入了foo函数的栈帧,而首先入栈的是函数的参数,需要注意的是,在C与C++中,参数的入栈顺序是从右向左的,即是d-c-b-a,参数入栈完毕后即是局部变量入栈,局部变量入栈的顺序就是按照语句的自然顺序了,即e-f。
按照这个构想,如上图所示,地址的大小应该是d\>c\>b\>a\>e\>f
的,我们执行以下代码看一看:
结果符合预期,不过有一点,我们发现a和e的地址在g++ -32bit-release的编译环境下相差了14个字节,我十分好奇这14个字节的储存内容,同样查阅了网络上的资料,杨小卫先生的文章:
http://blog.csdn.net/tdgx2004/article/details/5985531
在函数参数与函数局部变量之间存在类似与两个栈帧之间的保存下一地址和上一栈底的行为
但是按照这个说法应该是占据8个字节(32位下地址占4个字节),这就十分奇怪了,因为在栈上出现地址差不为4的倍数是很奇怪的,还有6个Byte存储的是什么?
三、后续
我将问题挂到了SegmentFault上,在几周后得到了回答
EngineWorld:
“这个问题跟平台和采用的调用惯例以及编译器都有关系。i386平台,默认的_cdecl调用惯例条件下,在运行时栈上,函数参数和函数局部变量中间还有1: 返回地址(pc after call instruction), 2: caller的栈帧基址(ebp), 3: callee save registers(不同平台个数不同),4:为了处理异常而在栈上增加的信息(不同编译器可能实现不同,gcc就会增加东西)“
等学了汇编之后会自己测试一下,现贴上来也希望有dalao能看看栈帧具体情况。