堆栈概念(来自维基)

引言

        使用维基百科需要注意一个地方,其自带的语言选择可以用,但不推荐。

        比如,查看“CallStack”(调用栈)的百科,英文版本和中文版本完全不一样,建议阅读英文版本,直接或使用网页翻译进行阅读。

        差异见下图(图一,中文百科;图二三,英文百科——网页翻译中文后):

 —————————————————中英文分隔线—————————————————

—————————————以下来自维基百科CallStack条目—————————————

调用栈

维基百科,自由的百科全书
跳转到导航跳转到搜索
这篇文章需要额外的引用来验证。请通过添加对可靠来源的引用来帮助改进本文。无法查证的内容可能被提出异议而移除。查找来源:“调用堆栈”  – 新闻·报纸·书籍·学者· JSTOR
         (2012 年 9 月)了解如何以及何时删除此模板消息

计算机科学中调用堆栈是一种堆栈 数据结构,用于存储有关计算机程序的活动子例程的信息。这种堆栈也称为执行堆栈程序堆栈控制堆栈运行时堆栈机器堆栈,通常简称为“堆栈”。尽管维护调用堆栈对于大多数软件的正常运行很重要,但在高级编程语言中,细节通常是隐藏的和自动的。许多电脑指令集提供了操作堆栈的特殊指令。

调用堆栈用于多种相关目的,但使用堆栈的主要原因是跟踪每个活动子例程在完成执行时应返回控制的点。活动子程序是已被调用但尚未完成执行的子程序,之后应将控制权交还给调用点。子程序的这种激活可以嵌套到任何级别(递归作为特殊情况),因此是堆栈结构。例如,如果一个子DrawSquare例程DrawLine从四个不同的地方调用一个子例程,则DrawLine必须知道它执行完成后从哪里返回。为此,跳转到的指令后面的地址返回地址DrawLine, 每次调用都会被推到调用堆栈的顶部。

描述[编辑]

由于调用堆栈被组织为一个堆栈,调用者将返回地址压入堆栈,被调用的子程序在完成时,从调用堆栈中拉出或弹出返回地址,并将控制权转移到该地址。如果一个被调用的子例程调用另一个子例程,它会将另一个返回地址推入调用堆栈,依此类推,信息按照程序的指示堆叠和卸载。如果推送消耗了为调用堆栈分配的所有空间,则会发生称为堆栈溢出的错误,通常会导致程序崩溃。将子程序的入口添加到调用堆栈有时称为“缠绕”;相反,删除条目是“展开”。

有通常与正在运行的程序(或更准确地,与每个相关联的恰好一个调用栈任务线程一个的处理),虽然附加的堆叠可以为创建信号处理或协作多任务处理(如用setcontext)。由于只有一个在这个重要的方面,它可以被称为所述堆(隐式“的任务的”); 然而,在第四编程语言数据栈参数堆比调用堆栈访问更明确地,并且通常被称为堆(见下文)。

高级编程语言中,调用堆栈的细节通常对程序员是隐藏的。它们只能访问一组函数,而不是堆栈本身的内存。这是一个抽象的例子。另一方面,大多数汇编语言要求程序员参与操作堆栈。编程语言中堆栈的实际细节取决于编译器操作系统和可用的指令集

调用堆栈的函数[编辑]

如上所述,调用堆栈的主要目的是存储返回地址。当子程序被调用时,调用程序可以在以后恢复的指令的位置(地址)需要保存在某处。与其他调用约定相比,使用堆栈来保存返回地址具有重要的优势。一个是每个任务都可以有自己的堆栈,因此子程序可以是线程安全的,即可以同时为不同的任务做不同的事情而处于活动状态。另一个好处是通过提供可重入递归自动支持。当函数递归调用自身时,需要为函数的每次激活存储一个返回地址,以便以后可以使用它从函数激活中返回。堆栈结构自动提供此功能。

根据语言、操作系统和机器环境,调用堆栈可能有其他用途,例如:

本地数据存储

子程序经常需要内存空间来存储局部变量的值,这些变量只在活动子程序中已知,返回后不保留值。为这种用途分配空间通常很方便,只需将堆栈顶部移动足够的空间来提供空间。与使用堆空间的动态内存分配相比,这非常快。请注意,子例程的每个单独激活在堆栈中都有自己单独的空间供本地人使用。

参数传递

子例程通常要求调用它们的代码为它们提供参数的值,并且在调用堆栈中布置这些参数的空间并不少见。一般如果只有几个小参数,会使用处理器寄存器来传递值,但如果参数多到这种方式无法处理,则需要内存空间。调用堆栈可以很好地作为这些参数的位置,特别是因为对子程序的每次调用都会有不同的参数值,将在调用堆栈上为这些值分配单独的空间。

评估堆栈

算术或逻辑运算的操作数通常放在寄存器中并在那里进行操作。但是,在某些情况下,操作数可能会堆叠到任意深度,这意味着必须使用的不仅仅是寄存器(这是寄存器溢出的情况)。这种操作数的堆栈,就像在RPN 计算器中的堆栈一样,称为评估堆栈,并且可能会占用调用堆栈中的空间。

指向当前实例的指针

一些面向对象的语言(例如,C++)在调用方法时将this指针与函数参数一起存储在调用堆栈中。在这个指针指向对象 实例与所述方法相关联的被调用。

封闭子程序上下文

一些编程语言(例如PascalAda)支持嵌套子程序的声明,允许访问其封闭例程的上下文,即外部例程范围内的参数和局部变量。这种静态嵌套可以重复 - 在函数内声明的函数内声明的函数......实现必须提供一种方法,使任何给定静态嵌套级别的被调用函数可以引用每个封闭嵌套级别的封闭框架。通常,此引用由指向最近激活的封闭函数实例的帧的指针实现,称为“下堆栈链接”或“静态链接”,以区别于引用直接调用者的“动态链接”(不需要是静态父函数)。

代替静态链接,对封闭静态帧的引用可以被收集到称为显示的指针数组中,该指针被索引以定位所需帧。例程词法嵌套的深度是一个已知常数,因此例程显示的大小是固定的。此外,要遍历的包含范围的数量是已知的,显示的索引也是固定的。通常,例程的显示位于其自己的堆栈帧中,但Burroughs B6500在支持多达 32 级静态嵌套的硬件中实现了这样的显示。

表示包含范围的显示条目是从调用者显示的适当前缀中获得的。递归的内部例程为每个调用创建单独的调用帧。在这种情况下,所有内部例程的静态链接都指向相同的外部例程上下文。

其他返回状态

除了返回地址之外,在某些环境中,当子程序返回时,可能还有其他机器或软件状态需要恢复。这可能包括特权级别、异常处理信息、算术模式等。如果需要,这可以像返回地址一样存储在调用堆栈中。

典型的调用堆栈用于返回地址、局部变量和参数(称为调用帧)。在某些环境中,分配给调用堆栈的函数可能更多或更少。例如,在Forth 编程语言中,通常只有返回地址、计数的循环参数和索引以及可能的局部变量存储在调用堆栈上(在该环境中称为返回堆栈),尽管可以临时放置任何数据只要尊重调用和返回的需要,就会使用特殊的返回堆栈处理代码;参数通常存储在单独的上数据栈参数栈,通常称为栈在 Forth 术语中,即使有调用栈,因为它通常被更明确地访问。一些 Forth 还具有用于浮点参数的第三个堆栈。

结构[编辑]

向上增长堆栈的调用堆栈布局

调用堆栈是由堆栈帧(也称为激活记录激活帧)。这些是包含子程序状态信息的机器相关ABI相关数据结构。每个堆栈帧对应于对尚未终止并返回的子例程的调用。例如,如果名为的子例程DrawLine当前正在运行,并已被子例程调用DrawSquare,则调用堆栈的顶部可能会像相邻的图片一样布局。

只要了解顶部的位置以及堆栈增长的方向,就可以在任一方向绘制这样的图。此外,与此无关,架构在调用堆栈是向更高地址增长还是向更低地址增长方面有所不同。该图的逻辑与寻址选择无关。

堆栈顶部的堆栈帧用于当前正在执行的例程。堆栈帧通常至少包括以下项目(按入栈顺序):

  • 传递给例程的参数(参数值)(如果有);
  • 返回给例程调用者的返回地址(例如,在DrawLine堆栈帧中,一个地址到DrawSquare的代码);和
  • 例程局部变量的空间(如果有的话)。

堆栈和帧指针[编辑]

当堆栈帧大小可能不同时,例如在不同函数之间或在特定函数的调用之间,从堆栈中弹出帧不会构成堆栈指针的固定递减量。在函数返回时,堆栈指针被恢复到帧指针,即函数被调用之前堆栈指针的值。每个堆栈帧都包含一个堆栈指针,指向紧接在其下方的帧的顶部。堆栈指针是一个在所有调用之间共享的可变寄存器。给定函数调用的帧指针是调用函数之前的堆栈指针的副本。[1]

帧中所有其他字段的位置可以定义为相对于帧的顶部,作为堆栈指针的负偏移量,或相对于下方帧的顶部,作为帧指针的正偏移量。帧指针本身的位置必须固有地定义为堆栈指针的负偏移量。

将地址存储到调用者的框架中[编辑]

在大多数系统中,堆栈帧有一个字段来包含帧指针寄存器的先前值,即调用者执行时的值。例如,堆栈帧的DrawLine内存位置将保存所DrawSquare使用的帧指针值(上图中未显示)。该值在进入子程序时保存并在返回时恢复。在堆栈帧中的已知位置具有这样的字段使代码能够在当前执行的例程的帧下连续访问每个帧,并且还允许例程在返回之前轻松地将帧指针恢复到调用者的帧。

词法嵌套例程[编辑]

更多信息:嵌套函数非局部变量

支持嵌套子例程的编程语言在调用帧中也有一个字段,该字段指向最紧密封装被调用者的过程的最新激活的堆栈帧,即被调用者的直接范围。这称为访问链接静态链接(因为它在动态和递归调用期间跟踪静态嵌套)并提供例程(以及它可能调用的任何其他例程)在每个嵌套级别访问其封装例程的本地数据。一些架构、编译器或优化案例为每个封闭级别(不仅仅是直接封闭的)存储一个链接,因此访问浅层数据的深度嵌套例程不必遍历多个链接;这种策略通常被称为“展示”。[2]

当内部函数不访问封装中的任何(非常量)本地数据时,可以优化访问链接,例如纯函数仅通过参数和返回值进行通信的情况。一些历史计算机,例如Burroughs 大型系统,有特殊的“显示寄存器”来支持嵌套函数,而大多数现代机器(例如无处不在的 x86)的编译器只是根据需要在堆栈上为指针保留几个字。

重叠[编辑]

出于某些目的,子程序的栈帧与其调用者的栈帧可以被认为是重叠的,重叠部分包括参数从调用者传递到被调用者的区域。在某些环境中,调用者将每个参数压入堆栈,从而扩展其堆栈框架,然后调用被调用者。在其他环境中,调用者在其堆栈帧的顶部有一个预先分配的区域,用于保存它提供给它调用的其他子例程的参数。该区域有时称为传出参数区域标注区域。在这种方法下,该区域的大小由编译器计算为任何被调用子程序所需的最大大小。

使用[编辑]

呼叫站点处理[编辑]

通常在调用子例程的位置所需的调用堆栈操作是最少的(这很好,因为每个要调用的子例程可以有许多调用点)。实际参数的值在调用站点进行评估,因为它们特定于特定调用,并且根据所使用的调用约定被推入堆栈或放入寄存器。然后通常会执行实际的调用指令,例如“分支和链接”,以将控制权转移到目标子程序的代码。

子程序入口处理[编辑]

在被调用的子例程中,执行的第一个代码通常称为子例程序言,因为它在例程语句的代码开始之前执行必要的内务处理。

对于用于调用子例程的指令将返回地址放入寄存器而不是将其压入堆栈的指令集体系结构,序言通常会通过将值压入调用堆栈来保存返回地址,尽管如果被调用子例程不会调用任何其他例程,它可能会将值留在寄存器中。类似地,可以压入当前堆栈指针和/或帧指针值。

如果正在使用帧指针,序言通常会从堆栈指针中设置帧指针寄存器的新值。然后可以通过增量更改堆栈指针来分配局部变量的堆栈空间。

第四编程语言可以明确的调用堆栈(被称为有“返回堆栈”)的绕组。

退货处理[编辑]

当一个子程序准备好返回时,它会执行一个尾声,撤消序言的步骤。这通常会从堆栈帧中恢复保存的寄存器值(例如帧指针值),通过更改堆栈指针值将整个堆栈帧从堆栈中弹出,最后跳转到返回地址处的指令。在许多调用约定下,由结语从堆栈中弹出的项包括原始参数值,在这种情况下,调用者通常不需要进行进一步的堆栈操作。但是,对于某些调用约定,调用者有责任在返回后从堆栈中删除参数。

展开[编辑]

从被调用函数返回会将栈顶帧弹出,可能会留下一个返回值。从堆栈中弹出一个或多个帧以恢复程序中其他地方的执行的更一般行为称为堆栈展开,并且必须在使用非本地控制结构时执行,例如用于异常处理的控制结构。在这种情况下,函数的堆栈帧包含一个或多个指定异常处理程序的条目。抛出异常时,堆栈将展开,直到找到准备处理(捕获)抛出异常类型的处理程序。

某些语言具有需要一般展开的其他控制结构。Pascal允许全局goto语句将控制从嵌套函数转移到先前调用的外部函数中。此操作需要展开堆栈,根据需要移除尽可能多的堆栈帧以恢复正确的上下文,从而将控制转移到封闭外部函数内的目标语句。类似地,C 具有充当非本地 goto的setjmp和longjmp函数。Common Lisp允许通过使用unwind-protect特殊运算符控制堆栈展开时发生的情况。

应用延续时,堆栈(逻辑上)展开,然后与延续的堆栈重绕。这不是实现延续的唯一方法;例如,使用多个显式堆栈,延续的应用程序可以简单地激活其堆栈并环绕要传递的值。当调用延续时,Scheme 编程语言允许在控制堆栈的“展开”或“回绕”的指定点执行任意thunk

检查[编辑]

有时可以在程序运行时检查调用堆栈。根据程序的编写和编译方式,堆栈上的信息可用于确定中间值和函数调用跟踪。这已被用于生成细粒度的自动化测试,[3]以及在 Ruby 和 Smalltalk 等情况下,以实现一流的延续。例如,GNU 调试器(GDB) 对正在运行但已暂停的 C 程序的调用堆栈进行交互式检查。[4]

对调用堆栈进行定期采样对于分析程序的性能很有用,因为如果一个子程序的指针多次出现在调用堆栈采样数据上,则很可能是代码瓶颈,应该检查是否存在性能问题。

安全[编辑]

主条目:堆栈缓冲区溢出

在具有自由指针或非检查数组写入的语言中(例如在 C 中),影响代码执行的控制流数据(返回地址或保存的帧指针)和简单程序数据(参数或返回值)的混合)在调用堆栈是一个安全风险,可能利用的通过堆缓冲区溢出作为最常见的类型的缓冲区溢出

其中一种攻击涉及用任意可执行代码填充一个缓冲区,然后溢出相同或某个其他缓冲区以使用直接指向可执行代码的值覆盖某个返回地址。结果,当函数返回时,计算机执行该代码。使用W^X可以轻松阻止这种攻击。[需要引用]即使启用了 W^X 保护,类似的攻击也可以成功,包括返回到 libc 攻击或来自面向返回编程的攻击。已经提出了各种缓解措施,例如将数组存储在与返回堆栈完全不同的位置,就像 Forth 编程语言中的情况一样。[5]

另见[编辑]

参考文献[编辑]

  1. “了解堆栈”cs.umd.edu。2003-06-22。原始存档于 2013-02-25 。检索2014-05-21。
  2. ^ 替代微处理器设计
  3. ^ 麦克马斯特, S.; 梅蒙,A.(2006 年)。用于 GUI 测试套件缩减的调用堆栈覆盖率(PDF)。第 17 届软件可靠性工程国际研讨会 ( ISSRE '06)。第 33-44 页。CiteSeerX 10.1.1.88.873doi10.1109/ISSRE.2006.19国际标准书号   0-7695-2684-5.
  4. “使用 GDB 调试:检查堆栈”chemie.fu-berlin.de。1997 年 10 月 17 日。检索2014-12-16。
  5. ^ 道格·霍伊特。“第四种编程语言 - 为什么你应该学习它”

进一步阅读[编辑]

外部链接[编辑]

猜你喜欢

转载自blog.csdn.net/qq_23958061/article/details/121852668