【问题标题】:Explain the concept of a stack frame in a nutshell简述栈帧的概念
【发布时间】:2012-04-20 21:29:27
【问题描述】:

似乎我在编程语言设计中得到了调用堆栈的想法。但是我找不到(可能是我搜索得不够努力)关于什么是 stack frame 的任何体面的解释。

所以我想请人用几句话向我解释一下。

【问题讨论】:

    标签: callstack


    【解决方案1】:

    堆栈帧是被压入堆栈的数据帧。在调用堆栈的情况下,堆栈帧将表示函数调用及其参数数据。

    如果我没记错的话,函数返回地址首先被压入堆栈,然后是局部变量的参数和空间。它们一起构成了“框架”,尽管这可能取决于架构。处理器知道每个帧中有多少字节,并在帧被压入和弹出堆栈时相应地移动堆栈指针。

    编辑:

    高层调用栈和处理器调用栈有很大区别。

    当我们谈论处理器的调用堆栈时,我们谈论的是在汇编或机器代码中处理字节/字级别的地址和值。在谈论高级语言时存在“调用堆栈”,但它们是由运行时环境管理的调试/运行时工具,因此您可以记录程序出现的问题(在较高级别)。在这个级别,诸如行号、方法和类名之类的东西通常是已知的。当处理器得到代码时,它对这些东西完全没有概念。

    【讨论】:

    • “处理器知道每个帧中有多少字节,并在帧被压入和弹出堆栈时相应地移动堆栈指针。” - 我怀疑处理器对堆栈一无所知,因为我们通过 subbing(分配)、push 和 pop 来操作它。所以这里有一些调用约定来解释我们应该如何使用堆栈。
    • 处理器有栈帧指针,寄存器包含函数帧的地址。
    【解决方案2】:

    如果您非常了解堆栈,那么您将了解内存在程序中的工作方式,如果您了解内存在程序中的工作方式,您将了解函数如何在程序中存储,如果您了解函数如何在程序中存储,您将了解递归函数工作,如果你了解递归函数是如何工作的,你就会明白编译器是如何工作的,如果你了解编译器是如何工作的,你的头脑就会像编译器一样工作,你会很容易地调试任何程序

    让我解释一下堆栈的工作原理:

    首先你必须知道函数在堆栈中是如何表示的:

    堆存储动态分配的值。
    堆栈存储自动分配和删除值。

    让我们通过例子来理解:

    def hello(x):
        if x==1:
            return "op"
        else:
            u=1
            e=12
            s=hello(x-1)
            e+=1
            print(s)
            print(x)
            u+=1
        return e
    
    hello(4)
    

    现在了解该程序的部分内容:

    现在让我们看看什么是堆栈,什么是堆栈部分:

    栈的分配:

    记住一件事:如果任何函数的返回条件得到满足,无论它是否加载了局部变量,它都会立即从堆栈中返回它的堆栈帧。这意味着只要任何递归函数满足基本条件并且我们在基本条件之后放置一个返回,基本条件就不会等待加载位于程序“else”部分的局部变量。它将立即从堆栈中返回当前帧,随后下一帧现在在激活记录中。

    在实践中看到这一点:

    块的释放:

    所以现在每当函数遇到 return 语句时,它都会从堆栈中删除当前帧。

    从堆栈返回时,值将按照与它们在堆栈中分配的原始顺序相反的顺序返回。

    【讨论】:

    • 堆栈向下增长,堆向上增长,您在图表中将它们颠倒过来。 CORRECT DIAGRAM HERE
    • @Rafael 很抱歉混淆了,我在谈论增长方向我不是在谈论堆栈增长方向。生长方向和堆栈生长方向是有区别的。看这里stackoverflow.com/questions/1677415/…
    • 拉斐尔是对的。还有第一张图是错的。用其他东西替换它(在谷歌图片中搜索“堆堆栈”)。
    • 因此,如果我理解正确,在您的第三张图中,有 3 个堆栈帧,因为 hello() 递归调用了 hello(),然后(再次)递归调用 hello(),以及全局框架是调用第一个hello()的原始函数?
    • 链接将我们引向何方??出于安全考虑,应尽快删除这些链接。
    【解决方案3】:

    快速总结一下。也许有人有更好的解释。

    调用堆栈由 1 个或多个堆栈帧组成。每个堆栈帧对应于对尚未以返回终止的函数或过程的调用。

    为了使用栈帧,一个线程需要保存两个指针,一个叫做栈指针(SP),另一个叫做帧指针(FP)。 SP 始终指向堆栈的“顶部”,而 FP 始终指向帧的“顶部”。此外,线程还维护一个程序计数器 (PC),它指向要执行的下一条指令。

    以下存储在堆栈中:局部变量和临时变量、当前指令的实际参数(过程、函数等)

    关于堆栈的清理有不同的调用约定。

    【讨论】:

    • 不要忘记子程序的返回地址在堆栈上。
    • 帧指针也是 x86 术语中的基指针
    • 我想强调一个帧指针指向当前活动过程化身的堆栈帧的开始。
    【解决方案4】:

    “调用栈是由栈帧组成的……”——Wikipedia

    堆栈帧是您放入堆栈的东西。它们是包含要调用的子例程信息的数据结构。

    【讨论】:

    • 对不起,我不知道我是怎么在 wiki 上错过这个的。谢谢。我是否理解正确,在动态语言中,帧的大小不是一个常数值,因为函数的局部变量并不完全清楚?
    • 框架的大小和性质在很大程度上取决于机器的架构。事实上,调用堆栈的范式是特定于体系结构的。据我所知,它总是可变的,因为不同的函数调用会有不同数量的参数数据。
    • 请注意,堆栈帧的大小必须在被处理时为处理器所知。发生这种情况时,数据的大小已经确定。动态语言像静态语言一样被编译为机器代码,但通常是即时完成的,这样编译器可以保持动态,处理器可以使用“已知”的帧大小。不要将高级语言与机器代码/汇编混淆,这是实际发生的地方。
    • 好吧,但是动态语言也有它们的调用堆栈,不是吗?我的意思是,如果说,Python 想要执行某个过程,那么关于这个过程的数据存储在某个 Python 解释器的结构中,对吗?所以我的意思是调用堆栈不仅存在于低级别。
    • 在阅读了一些维基百科文章后,我得到了纠正(有点)。在编译时,堆栈帧的大小可能是未知的。但是当处理器使用堆栈+帧指针时,它必须知道大小是多少。大小可以是可变的,但处理器知道大小,这就是我想说的。
    【解决方案5】:

    程序员可能对堆栈帧有疑问,不是广义的(它是堆栈中的一个单一实体,只服务一个函数调用并保留返回地址、参数和局部变量),而是狭义的——当术语stack frames 在编译器选项的上下文中被提及。

    无论问题的作者是否有意,但从编译器选项方面的堆栈框架的概念是一个非常重要的问题,这里的其他回复没有涵盖。

    例如,Microsoft Visual Studio 2015 C/C++ 编译器有以下与stack frames相关的选项:

    • /Oy(帧指针省略)

    GCC 有以下几点:

    • -fomit-frame-pointer(不要将帧指针保存在不需要的函数的寄存器中。这避免了保存、设置和恢复帧指针的指令;它还提供了一个额外的寄存器可用在许多功能中)

    英特尔 C++ 编译器有以下内容:

    • -fomit-frame-pointer(确定EBP是否用作优化中的通用寄存器)

    具有以下别名:

    • /Oy

    Delphi 有以下命令行选项:

    • -$W+(生成堆栈帧)

    在这个特定的意义上,从编译器的角度来看,堆栈帧只是例程的入口和退出代码,它将一个锚点推送到堆栈 - 它也可以用于调试和用于异常处理。调试工具可能会扫描堆栈数据并使用这些锚点进行回溯,同时在堆栈中定位call sites,即按分层调用的顺序显示函数的名称。对于 Intel 架构,入口是push ebp; mov ebp, espenter,出口是mov esp, ebp; pop ebpleave

    这就是为什么对于程序员来说,在编译器选项方面了解堆栈帧的位置非常重要——因为编译器可以控制是否生成此代码。

    在某些情况下,编译器可以省略堆栈帧(例程的进入和退出代码),变量将直接通过堆栈指针(SP/ESP/RSP)而不是方便的基指针访问(BP/ESP/RSP)。 省略栈帧的条件,例如:

    • 该函数是叶函数(即不调用其他函数的终端实体);
    • 没有 try/finally 或 try/except 或类似结构,即不使用异常;
    • 没有使用堆栈上的传出参数调用例程;
    • 函数没有参数;
    • 该函数没有内联汇编代码;
    • 等等……

    省略堆栈帧(例程的进入和退出代码)可以使代码更小更快,但它也可能对调试器回溯堆栈中的数据并将其显示给程序员的能力产生负面影响。这些是编译器选项,用于确定函数应在哪些条件下具有进入和退出代码,例如:(a) 始终、(b) 从不、(c) 需要时(指定条件)。

    【讨论】:

      【解决方案6】:

      堆栈帧是与函数调用相关的打包信息。此信息通常包括传递给函数的参数、局部变量以及终止时返回的位置。激活记录是堆栈帧的另一个名称。堆栈帧的布局由制造商在 ABI 中确定,每个支持 ISA 的编译器都必须符合此标准,但是布局方案可以取决于编译器。通常堆栈帧大小不受限制,但有一个称为“红色/保护区”的概念,以允许系统调用...等在不干扰堆栈帧的情况下执行。

      总有一个 SP,但在某些 ABI(例如 ARM 和 PowerPC)上,FP 是可选的。需要放入堆栈的参数只能使用 SP 进行偏移。是否为函数调用生成堆栈帧取决于参数的类型和数量、局部变量以及通常如何访问局部变量。在大多数 ISA 上,首先使用寄存器,如果参数多于专用于传递参数的寄存器,则将它们放置到堆栈中(例如 x86 ABI 有 6 个寄存器来传递整数参数)。因此,有时,有些函数不需要将栈帧放入堆栈,只需将返回地址压入堆栈即可。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-08-06
        • 2021-04-29
        • 2014-05-29
        • 1970-01-01
        • 2011-07-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多