术语 Terminology
堆栈指针(stack pointer)
指CPU中的一个寄存器,该寄存器始终指向栈的顶部,同时也指向当前函数活动记录的顶部。在X86架构下,该寄存器是esp,在MIPS架构下,该寄存器是sp,即MIPS的32个通用寄存器中的29号寄存器(从0开始编号)。
同时需要指出的是,对堆栈指针的操作,在属于复杂指令集(CISC)的X86下,除了使用加减指令(ADD,SUB)之外,还有专门的指令POP,PUSH,在属于精简指令集的计算机(RISC)的MIPS下,则只能使用通用的加减指令(ADD,SUB)。
帧指针(frame pointer)
帧指针是存储函数活动记录中固定地址的寄存器。其实就是CPU中的一个寄存器,该寄存器始终指向函数活动记录的一个固定位置,不会随这个函数的执行而变化(注意,并不是指向活动记录的最底部位置)。在X86架构下,该寄存器是ebp,在MIPS架构下,该寄存器是s8,即MIPS的32个通用寄存器中的30号寄存器(从0开始编号)。
堆栈帧(stack frame)/活动记录(active record)
指由栈保存的一个函数调用所需维护的信息。一般包括:函数返回地址和参数,临时变量和局部变量,某些寄存器的值。
调用惯例(call convention)
函数调用方和被调用方对于函数如何调用的一个约定。一般包括:函数参数的传递顺序和方式,栈的维护方式,名字修饰策略等
调用惯例 ―― cdecl
cdecl这个调用惯例是C语言默认的调用惯例。首先,对C语言中常见的几种调用惯例进行一个总结性的比较。然后就其中一些特别的地方做一个说明。
调用惯例 出栈方 参数传递 名字修饰
cdecl 函数调用方 从右向左入栈 下划线+函数名
stdcall 函数本身 从右向左入栈 下划线+函数名+@+参数字节数
fastcall 函数本身 寄存器+从右向左入栈 @+函数名+@参数字节数
pascall 函数本身 从左向右入栈 较复杂
cdecl调用惯例的参数传递为从右向左,参数入栈。这是C语言函数实现可变长参数的基础。最常见的可变长参数函数是printf();
cdecl调用惯例的出栈方是函数调用方,这是和其他3种调用惯例不一样的地方,它们的出栈方都是函数本身。
活动记录:过程的一次执行所需要的信息用一块连续的存储区来管理,这块存储区叫做活动记录。
活动记录的各个域的用途:
1、临时数据域:如计算表达式出现的中间结果,若寄存器不足以存放所有这些中间结果时,可以把它们存放在临时数据域中
2、局部数据域:保存局部于过程执行的数据
3、机器状态域:保存刚好在过程调用前的机器状态信息,包括机器计数器的值和控制从这个过程返回必须恢复的机器寄存器的值
4、控制链:用来指向调用者的活动记录,控制链也称动态链
5、参数域:用来存放调用过程提供的实在参数
6、返回值域:用来存放被调用过程返回给调用过程的值
活动记录其实比没有包含过程一次执行所需的全部信息,比方说非局部数据就不在活动记录中,另外,过程运行时生成的动态变量也不在活动记录中,对它们通常采用堆式分配
因此必须以某种方式跟踪每个函数的返回地址,以便将控制返回到它的调用者。函数调用栈是处理这个信息的绝佳数据结构。每次调用函数时,就会将一个项压入栈中。这个项称为栈帧(stack frame)或活动记录,它包含被调用函数返回到调用函数时所需的返回地址。如果被调函数返回,则会弹出这个函数调用的帧栈,且控制会转移到被弹出的帧栈所包含的返回地址。
调用栈的亮点在于每个被调函数都能够在调用栈的顶部找到返回到它的调用者时所需要的信息。而且,如果一个函数调用了另一个函数,则这个新函数的栈帧也会被简单地压入调用栈。因此,新的被调函数返回到它的调用者所需要的返回地址,就位于栈的顶部。
帧栈还有另外一个重要责任。大多数函数都有自动变量,包括参数及他说声明的所有局部变量。自动变量需要在函数执行时存在。如果函数调用了其他函数,则他们仍然需要保持活动状态。但是当被调函数返回到他的调用者后,它的自动变量需要“消失”。被调函数的帧栈是保存它的自动变量的理想场所。只要被调函数处于活动状态,它的帧栈就会存在。当函数返回时(此时不在需要它的局部自动变量),它的帧栈就从栈弹出,而这些局部变量不再为程序所知。
当然,计算机中的内存容量是有限的,因此只要一定数量的内存能够用于在函数调用栈上保存活动记录。如果发生的函数调用超出了函数调用栈上能容纳的活动记录,就会发生栈溢出(stack overflow)的错误。
为什么要将返回地址入栈?
因为在普林斯顿体系下数据和代码是分离的,栈中不仅维护了数据还维护了代码当前执行的位置。因此发生函数调用时,需要讲当前函数的下一步需要执行的代码地址,即函数返回后继续执行的代码地址入栈。函数返回后可以继续执行。
为什么需要将函数的返回值入栈?
因为函数返回后,有一次赋值操作,将函数计算结果保存在临时变量中,所以需要将返回值入栈,当被调函数出栈后,调用方可以使用函数结果。