【发布时间】:2012-04-20 21:29:27
【问题描述】:
似乎我在编程语言设计中得到了调用堆栈的想法。但是我找不到(可能是我搜索得不够努力)关于什么是 stack frame 的任何体面的解释。
所以我想请人用几句话向我解释一下。
【问题讨论】:
标签: callstack
似乎我在编程语言设计中得到了调用堆栈的想法。但是我找不到(可能是我搜索得不够努力)关于什么是 stack frame 的任何体面的解释。
所以我想请人用几句话向我解释一下。
【问题讨论】:
标签: callstack
堆栈帧是被压入堆栈的数据帧。在调用堆栈的情况下,堆栈帧将表示函数调用及其参数数据。
如果我没记错的话,函数返回地址首先被压入堆栈,然后是局部变量的参数和空间。它们一起构成了“框架”,尽管这可能取决于架构。处理器知道每个帧中有多少字节,并在帧被压入和弹出堆栈时相应地移动堆栈指针。
高层调用栈和处理器调用栈有很大区别。
当我们谈论处理器的调用堆栈时,我们谈论的是在汇编或机器代码中处理字节/字级别的地址和值。在谈论高级语言时存在“调用堆栈”,但它们是由运行时环境管理的调试/运行时工具,因此您可以记录程序出现的问题(在较高级别)。在这个级别,诸如行号、方法和类名之类的东西通常是已知的。当处理器得到代码时,它对这些东西完全没有概念。
【讨论】:
如果您非常了解堆栈,那么您将了解内存在程序中的工作方式,如果您了解内存在程序中的工作方式,您将了解函数如何在程序中存储,如果您了解函数如何在程序中存储,您将了解递归函数工作,如果你了解递归函数是如何工作的,你就会明白编译器是如何工作的,如果你了解编译器是如何工作的,你的头脑就会像编译器一样工作,你会很容易地调试任何程序
让我解释一下堆栈的工作原理:
首先你必须知道函数在堆栈中是如何表示的:
堆存储动态分配的值。
堆栈存储自动分配和删除值。
让我们通过例子来理解:
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 语句时,它都会从堆栈中删除当前帧。
从堆栈返回时,值将按照与它们在堆栈中分配的原始顺序相反的顺序返回。
【讨论】:
hello() 递归调用了 hello(),然后(再次)递归调用 hello(),以及全局框架是调用第一个hello()的原始函数?
快速总结一下。也许有人有更好的解释。
调用堆栈由 1 个或多个堆栈帧组成。每个堆栈帧对应于对尚未以返回终止的函数或过程的调用。
为了使用栈帧,一个线程需要保存两个指针,一个叫做栈指针(SP),另一个叫做帧指针(FP)。 SP 始终指向堆栈的“顶部”,而 FP 始终指向帧的“顶部”。此外,线程还维护一个程序计数器 (PC),它指向要执行的下一条指令。
以下存储在堆栈中:局部变量和临时变量、当前指令的实际参数(过程、函数等)
关于堆栈的清理有不同的调用约定。
【讨论】:
“调用栈是由栈帧组成的……”——Wikipedia
堆栈帧是您放入堆栈的东西。它们是包含要调用的子例程信息的数据结构。
【讨论】:
程序员可能对堆栈帧有疑问,不是广义的(它是堆栈中的一个单一实体,只服务一个函数调用并保留返回地址、参数和局部变量),而是狭义的——当术语stack frames 在编译器选项的上下文中被提及。
无论问题的作者是否有意,但从编译器选项方面的堆栈框架的概念是一个非常重要的问题,这里的其他回复没有涵盖。
例如,Microsoft Visual Studio 2015 C/C++ 编译器有以下与stack frames相关的选项:
GCC 有以下几点:
英特尔 C++ 编译器有以下内容:
具有以下别名:
Delphi 有以下命令行选项:
在这个特定的意义上,从编译器的角度来看,堆栈帧只是例程的入口和退出代码,它将一个锚点推送到堆栈 - 它也可以用于调试和用于异常处理。调试工具可能会扫描堆栈数据并使用这些锚点进行回溯,同时在堆栈中定位call sites,即按分层调用的顺序显示函数的名称。对于 Intel 架构,入口是push ebp; mov ebp, esp 或enter,出口是mov esp, ebp; pop ebp 或leave。
这就是为什么对于程序员来说,在编译器选项方面了解堆栈帧的位置非常重要——因为编译器可以控制是否生成此代码。
在某些情况下,编译器可以省略堆栈帧(例程的进入和退出代码),变量将直接通过堆栈指针(SP/ESP/RSP)而不是方便的基指针访问(BP/ESP/RSP)。 省略栈帧的条件,例如:
省略堆栈帧(例程的进入和退出代码)可以使代码更小更快,但它也可能对调试器回溯堆栈中的数据并将其显示给程序员的能力产生负面影响。这些是编译器选项,用于确定函数应在哪些条件下具有进入和退出代码,例如:(a) 始终、(b) 从不、(c) 需要时(指定条件)。
【讨论】:
堆栈帧是与函数调用相关的打包信息。此信息通常包括传递给函数的参数、局部变量以及终止时返回的位置。激活记录是堆栈帧的另一个名称。堆栈帧的布局由制造商在 ABI 中确定,每个支持 ISA 的编译器都必须符合此标准,但是布局方案可以取决于编译器。通常堆栈帧大小不受限制,但有一个称为“红色/保护区”的概念,以允许系统调用...等在不干扰堆栈帧的情况下执行。
总有一个 SP,但在某些 ABI(例如 ARM 和 PowerPC)上,FP 是可选的。需要放入堆栈的参数只能使用 SP 进行偏移。是否为函数调用生成堆栈帧取决于参数的类型和数量、局部变量以及通常如何访问局部变量。在大多数 ISA 上,首先使用寄存器,如果参数多于专用于传递参数的寄存器,则将它们放置到堆栈中(例如 x86 ABI 有 6 个寄存器来传递整数参数)。因此,有时,有些函数不需要将栈帧放入堆栈,只需将返回地址压入堆栈即可。
【讨论】: