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