热门问题
时间线
聊天
视角
呼叫堆叠
用于存储运行程序子程序的栈数据结构 来自维基百科,自由的百科全书
Remove ads
调用堆栈(英语:Call stack,台湾称呼叫堆叠)别称有:执行栈(execution stack)、控制栈(control stack)、运行时栈(run-time stack)与机器栈(machine stack),是电脑科学中存储有关正在执行的子程式的讯息的堆叠。英文有时直接简称“栈”(the stack),但堆叠中不一定仅存储子程式讯息。几乎所有电脑程式都依赖于呼叫堆叠,而高阶语言一般将呼叫堆叠的细节隐藏至后台。
此条目可参照英语维基百科相应条目来扩充。 (2020年7月28日) |
呼叫堆叠最经常被用于存放子程式的返回位址。在呼叫任何子程式时,主程式都必须暂存子程式执行完毕后应该返回到的位址。因此,如果被呼叫的子程式还要呼叫其他的子程式,其自身的返回位址就必须存入呼叫堆叠,在其自身执行完毕后再行取回。在递回程式中,每一层次递回都必须在呼叫堆叠上增加一条位址,因此如果程式出现无限递回(或仅仅是过多的递回层次),呼叫堆叠就会产生堆叠溢位。
Remove ads
功能
呼叫堆叠的主要功能是存放返回位址。除此之外,呼叫堆叠还用于存放:
结构

调用栈由栈帧(stack frame)组成,它们也叫做“活动记录”或“活动帧”。它们是机器依赖并且ABI依赖的数据结构,包含了子例程状态信息。每个栈帧对应于一个子例程的仍未通过返回来完成的一次调用[1]。在堆栈顶部的栈帧对应当前执行例程。例如,一个叫做DrawLine的子例程当前正在运行,它被子例程DrawSquare所调用,右侧图示中给出了调用栈的顶部状况。
在栈帧大小可以不同的时候,比如在不同的函数之间或在特定函数的多次调用之间,从堆栈弹出一个帧不导致栈指针(stack pointer)减少固定大小。在函数返回之时,指针转而被复原为帧指针(frame pointer),即这个函数被调用前最近的栈指针的值。每个栈帧都包含一个帧指针,指向下面最近的帧的顶部。栈指针是在所有调用之间共享的可变的寄存器。一个函数的给定一次调用的帧指针,是这个函数被调用前的栈指针的复制品[2]。
一个例程可以直接变更栈指针,或执行入栈指令和出栈指令间接改变栈指针。在入栈指令自动递增栈指针所指地址的调用栈中,在一个帧之中的所有其他局部的字段的位置都可定义为,相对于帧指针即下面最近帧的顶部的正数偏移量。在x86架构的调用约定下它们定义为,相对于帧指针RBP的负数偏移量。
出于某些目的,一个子例程的栈帧和它的调用者的栈帧可以视为有所交叠(overlap),交叠由从调用者传递给被调用者的参数值所在区域组成。在调用结束时清理参数值的责任由被调用者实行的情况下,比如Pascal式调用约定,交叠自然地划分在被调用者栈帧之中。在入栈指令自动递增栈指针所指地址的调用栈中,调用者传递给一个例程的参数值的位置定义为,相对于帧指针即下面最近帧的顶部的负数偏移量。在x86架构的调用约定下它们定义为,相对于帧指针RBP的正数偏移量。
在一些环境中,调用者将每个实际参数(argument)压入堆栈之上,从而扩展了它自己栈帧,接着调用被调用者。在其他的环境中,调用者在它的栈帧的顶部有一个预先分配的区域,用来持有它要提供给其所调用的子例程的实际参数。这个区域有时称为“出去(outgoing)实参区域”或“调出(callout)区域”,可以对应ALGOL 60的术语拟制块。在这种途径下,由编译器计算出的这个区域的大小应当满足任何被调用的子例程的最大所需。
Remove ads
栈解绕

从被调用函数返回会弹出堆栈的顶部帧,并可能留下返回的一个值。更一般性的动作是从堆栈弹出一个或多个帧,从而恢复在程序中其它某处的执行,这叫做栈解绕(unwind),并且在使用了非局部控制结构的时候必须进行,比如例外处理。在这种情况下,一个函数的栈帧包含了指定例外处理器的一个或多个入口(entry)。在一个例外被抛出的时候,堆栈被解绕直到找到了准备处理(接住)这个抛出例外类型的一个处理器。
一些语言有要求一般性解绕的其他控制结构。Pascal允许使用全局性的goto语句,将控制从嵌套的函数传送出来并进入此前调用的外面的函数。这个操作要求堆栈被解绕,为了恢复正确的上下文,从而将控制传送给包围它的外面函数中的目标语句,需要移除多少栈帧就移除多少。与之类似,C语言有setjmp与longjmp函数充当非局部跳转,它们分别保存和恢复:栈指针、程序计数器、由被调用者保存的寄存器和ABI要求的任何其他内部状态。在编写可移植的C++代码之时,不应信赖执行非局部跳转会发生正常的堆栈解绕来进行所要的清理动作。
Common Lisp允许通过使用unwind-protect特殊算子,控制在堆栈被解绕时都要做些什么。在支持续体的编程语言比如Scheme和新泽西Standard ML中,在应用续体的时候,堆栈在逻辑上被解绕并重绕(rewind)上这个续体的堆栈,接着盘绕(wind)上要传递的那一个值。实现续体可以采用多种策略,例如通过使用多个显式的堆栈,应用一个续体可以简单的停用或舍弃当前堆栈并激活这个续体的堆栈。Scheme语言提供了对续体调用进行“保护”的dynamic-wind,只要控制离开或进入它所界定范围,包括通过应用续体这种非局部跳转方式,在分别对控制栈“解绕”或“重绕”的特定点上,都会执行任意指定的thunk。
在支持续体的编程语言和执行栈可以在运行时检查并修改的编程语言比如Smalltalk和Cilk中,在必须支持续体的时候,一个函数的局部变量在这个函数返回时不能被销毁:一个保存了的续体可以在往后时重入(re-enter)这个函数,并且不仅期望其中的变量不变动,而且期望整个调用序列的栈帧都存在使得这个函数能再次返回。要解决这个问题,栈帧可以在父指针树结构中动态分配,并在不再有续体引用的时候,将其留作垃圾回收。这种类型的结构还解决了上行和下行的函数参数问题,因为在这种基底上很容易实现头等词法闭包。由栈帧链接起来而实现的这种实际运行栈,也被称为“面条式堆栈”[3],它包含了变量绑定和其他环境特征。
Remove ads
安全性
在较底层语言(如组合语言与C语言中),程式控制讯息与资料可能一同被存入呼叫堆叠中,因此造成安全隐患,可能允许恶意程式通过栈缓冲区溢出(stack buffer overflow)来获取程式的控制权。
参见
引用
延伸阅读
Wikiwand - on
Seamless Wikipedia browsing. On steroids.
Remove ads