分配新的调用栈

8
我认为这个问题很有可能是重复的或者已经在这里得到了解答,但由于“堆栈分配”和相关术语的干扰,搜索答案很困难。
我有一个玩具编译器,用于一种脚本语言。为了能够在脚本执行过程中暂停并返回到主程序,它有自己的堆栈:一个简单的内存块,带有一个“堆栈指针”变量,使用正常的 C 代码操作进行增量等等。到目前为止都不那么有趣。
目前我编译成 C 代码。但我也有兴趣研究编译成机器码 - 同时保留第二个堆栈和在预定义控制点返回到主程序的能力。
所以...我想我的代码中使用传统的堆栈寄存器可能不会有问题,我假设在那里发生的寄存器情况是我的事情,只要完成时恢复所有内容即可(如果我在这一点上错了,请纠正我)。但是...如果我希望脚本代码调用一些其他库代码,离开这个“虚拟堆栈”安全吗?还是必须为此目的交还原始堆栈?

答案中的 this onethis one 表明栈不是传统的内存块,而是依赖于特殊的、与页面错误有关的系统行为。

所以:

  • 将堆栈指针移动到其他内存区域是否安全?堆栈内存并不是“特别的”吗?我想线程库一定会做类似的事情,因为它们创建更多的堆栈……
  • 假设使用堆栈寄存器和指令操纵任何内存区域都是安全的,只要已知调用深度的任何函数都可以调用,(即没有递归、没有函数指针),只要虚拟堆栈上有足够的空间,这会有什么问题吗?对吗?
  • 在正常代码中,堆栈溢出显然是一个问题,但是在这样一个系统中,堆栈溢出是否会产生任何额外灾难性的后果?
这显然并不是必要的,因为简单地返回真实堆栈的指针就足够了,或者说一开始就不滥用它们,只接受较少的寄存器,并且我可能根本不应该尝试这样做(尤其是因为显然已经超出了我的能力范围)。但无论如何,我仍然很好奇。想知道这些东西是如何工作的。
编辑:抱歉,当然,我应该说的是。我正在使用x86(32位为我的机器),Windows和Ubuntu。没有什么特别的。

1
这些问题的答案将高度依赖于特定平台。请说明您正在使用的平台。 - Oliver Charlesworth
你能具体说明一下你想要做什么吗?或者提供一个示例,说明你所说的虚拟堆栈指针是什么意思,以及你打算如何将控制返回到主机进程? - Roasted Yam
虚拟堆栈指针只是一个普通的C变量,它指向内存块中的某个位置。 "返回主程序"字面意思就是从脚本函数返回(因此,如果我们在寄存器上进行了操作,那么返回之前必须将它们设置回原来的状态)。本质上与解释器中所具有的相同;解释器对象维护其自己独立的程序状态。 - Alex Celeste
1
你是否考虑过在单独的线程中运行解释器(或其他程序)?这样它将自动获得一个良好的、由操作系统管理的堆栈,并避免您上述的任何问题! - Oliver Charlesworth
3个回答

4

所有这些答案都基于“常见的处理器架构”,由于它涉及生成汇编代码,因此必须是“目标特定”的 - 如果您决定在处理器X上执行此操作,该处理器具有某些奇怪的堆栈处理方式,则下面的内容显然不值得一提。对于x86而言,下面的内容通常适用,除非另有说明。

is it safe to move the stack pointers into some other area of memory?

Stack memory isn't "special"? I figure threading libraries must do something like this, as they create more stacks...

这段内存本身并不特殊。但是,这一假设是建立在不使用堆栈段限制堆栈用法的x86架构上的。尽管这是可能的,但在实现中很少见到。我知道几年前诺基亚有一个特殊的操作系统,在32位模式下使用段。就我所知,那是我接触到的唯一一个使用堆栈段的x86分段模式的操作系统。

假设任何内存区域都可以使用堆栈寄存器和指令进行安全操作,只要在虚拟堆栈上有足够的深度,我想不出为什么调用任何已知调用深度的函数会有问题(即没有递归、没有函数指针)。对吗?

正确。只要您不希望能够返回其他函数而不切换回原始堆栈。如果堆栈足够深,有限级别的递归也是可接受的[某些类型的问题确实很难在没有递归的情况下解决——例如二叉树搜索]。

在正常代码中,堆栈溢出显然是一个问题,但在这样的系统中是否会有任何额外灾难性后果?

确实,如果你运气不好,这将是一个难以解决的错误。

我建议您使用调用VirtualProtect()(Windows)或mprotect()(Linux等)来标记“堆栈末尾”为不可读和不可写,以便如果您的代码意外走出堆栈,则会崩溃,而不是一些其他更微妙的未定义行为[因为不能保证下面(低地址)的内存不可用,所以如果它走出堆栈,您可能会覆盖其他有用的东西,这将导致一些非常难以调试的错误]。

添加一些代码,定期检查堆栈深度(您知道您的堆栈从哪里开始和结束,因此检查特定堆栈值是否“超出范围”应该不难[如果在您保护的“我们死了”的区域之上(堆栈顶部)和“碎屑区域”之间留下一些“额外缓冲空间”,如果那是碰撞中的汽车,他们会称其为“折叠区域”]。您还可以使用可识别模式填充整个堆栈,并检查其中有多少是“未触及”的。


任何递归算法都可以转换为非常相似的迭代算法,通常可以轻松地使用堆分配的栈结构(在数据结构意义上的“栈”)来实现。 - Oliver Charlesworth
是的,我同意。但是虽然从技术角度来看相对容易,但它确实给一些本来非常简单的代码增加了一层复杂性。一个(平衡的)二叉树可以在20个递归层中搜索100万个条目 - 假设每个递归层的存储数据不是太大,那么即使是在一个相当小的堆栈中也完全可以处理。 - Mats Petersson
答案看起来很好很清楚,谢谢!就递归而言,我打算让第二个堆栈足够大,所以并不担心小的有限递归情况。 - Alex Celeste
嗯,无限递归显然很少是解决任何问题的好方法。如果您的递归层数“相当可预测且不超过几十层”,那么递归可能不是该问题的正确解决方案。是的,使堆栈足够大也可以避免许多其他麻烦。 - Mats Petersson

2
通常情况下,在x86上,只要不发生以下情况,您可以毫无问题地使用现有的堆栈:
- 溢出堆栈 - 不使用popadd esp,positive_value/sub esp,negative_value增加堆栈指针寄存器的值,使其超过代码开始的值(如果这样做,则使用堆栈的中断、异步回调(信号)或任何其他活动会破坏其内容) - 不引起任何CPU异常(如果这样做,则异常处理代码可能无法将堆栈展开到最近的可以处理异常的点)
使用不同的内存块作为临时堆栈并将esp指向其末尾也是同样适用的。
异常处理和堆栈展开的问题与编译后的C和C++代码包含一些与异常处理相关的数据结构有关,例如eip范围及其对应异常处理程序的链接(这告诉每个代码片段最近的异常处理程序在哪里),还有一些与调用函数的识别相关的信息(比如返回地址在堆栈上的位置等),以便可以向上传递异常。如果您只是将原始机器代码插入到此“框架”中,则无法正确扩展这些异常处理数据结构以涵盖它,如果出现问题,它们可能会非常严重(整个进程可能会崩溃或受损,尽管您在生成的代码周围有异常处理程序)。
所以,是的,只要小心,您就可以使用堆栈。

2
您可以使用任何区域作为处理器的堆栈(除了内存保护)。基本上,您只需将ESP寄存器(“MOV ESP,…”)加载到指向新区域的指针中,无论您如何分配它。
您必须有足够的空间来运行您的程序以及它可能调用的任何内容(例如Windows OS API)和操作系统的任何有趣行为。您可能能够确定代码需要多少空间;一个好的编译器可以轻松完成这项工作。确定Windows所需的空间量更加困难;您总是可以分配“过多”的空间,这就是Windows程序倾向于这样做的原因。
如果您决定严格管理此空间,则可能需要切换堆栈以调用Windows函数。但这并不足够;您可能会遭受各种Windows意外的损失。我在这里描述了其中之一:Windows:避免在堆栈上推送完整的x86上下文。我有一些一般性的解决方案,但没有很好的解决方案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接