C语言过程调用机制的一个关键特性(大多数其他语言也是如此)在于使用了栈数据结构提供的后进先出的内存管理原则。在过程P调用过程Q的例子中,可以看到当Q在执行时,P以及所有在向上追溯到P的调用链中的过程,都是暂时被挂起的。当Q运行时,他只需要为局部变量分配新的存储空间,或者设置到另一个过程的调用。另一方面,当Q返回时,任何它所分配的布局变量存储空间都可以被释放。因此,程序可以用栈来管理它的过程所需要的存储空间,栈和程序寄存器存放着传递控制和数据,分配内存所需要的信息。当P调用Q时,控制和数据信息添加到栈尾。当P返回时,这些信息会释放掉。
如3.4.4节讲过的,x86-64的栈向低地址方向增长,而栈指针%rsp指向栈顶元素。可以用Pushq和popq指令将数据存入栈中或是从栈中取出。将栈指针减小一个适当的量可以为没有制定初始值的数据在栈上分配空间(栈向低地址方向增长,栈指针减小则为分配空间)。类似地,可以通过增加栈指针来释放空间。
当x86-64过程需要的存储空间超出寄存器能够放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧(stack fram)。
通用的栈帧结构(栈用来传递参数,存储返回信息,保存寄存器,以及局部存储。)
上图给出了运行时栈的通用结构,包括把它划分为栈帧。当前正在执行的过程的帧总是在栈顶。当过程P调用过程Q时,会把返回地址压入栈中,指明当Q返回时,要从P程序的哪个位置继续执行。我们把这个返回地址当作P的栈帧的一部分,因为他存放的是与P相关的状态。Q的代码会扩展当前栈的边界,分配它的栈帧所需的空间。这个空间中,他可以保存寄存器的值,分配布局变量空间,为它调用的过程设置参数。大多数过程的栈帧都是定长的,在过程的开始就分配好了。但是有些过程需要变长的帧,这个问题会在3.10.5中讨论。通过寄存器,P可以在调用Q之前在自己的栈帧里存储好这些参数。
为了提高空间和时间效率,x86-64过程只分配自己所需的栈帧部分。例如,许多过程有6个或者更少的参数,那么所有的参数都可以通。过寄存器传递。因此,图3-25中画出的某些栈帧部分可以省略。实际上,许多函数甚至根本不需要栈帧。当所有的局部变量都可以保存在寄存器中,而且该函数不会调用任何其他函数(有时称之为叶子过程,此时把过程调用看做树结构)时,就可以这样处理。例如,到目前为止我们仔细审视过的所有函数都不需要栈帧。
719





