7

(我认为这个问题很有可能是重复的,或者已经在这里得到了回答,但是由于“堆栈分配”和相关术语的干扰,很难找到答案。)

我有一个我一直在为脚本语言工作的玩具编译器。为了能够在脚本执行过程中暂停脚本的执行并返回到宿主程序,它有自己的堆栈:一个简单的内存块,带有一个“堆栈指针”变量,该变量使用正常的 C 代码操作递增诸如此类的事情等等。到目前为止不感兴趣。

目前我编译为 C。但我也有兴趣研究编译为机器代码 - 同时保持辅助堆栈和在预定义控制点返回主机程序的能力。

所以......我认为在我自己的代码中使用传统的堆栈寄存器不太可能成为问题,我假设寄存器发生的事情是我自己的事,只要一切都在完成后恢复(如果我在这一点上是错误的)。但是......如果我希望脚本代码调用其他一些库代码,那么使用这个“虚拟堆栈”离开程序是否安全,或者为此目的是否必须将原始堆栈归还?

像thisthis这样的答案表明堆栈不是传统的内存块,而是依赖于特殊的系统特定行为来处理页面错误等等。

所以:

  • 将堆栈指针移动到其他内存区域是否安全?堆栈内存不是“特殊”的吗?我认为线程库必须做这样的事情,因为它们会创建更多的堆栈......
  • 假设内存的任何区域都可以安全地使用堆栈寄存器和指令进行操作,我认为没有理由调用具有已知调用深度(即没有递归,没有函数指针)的任何函数会成为问题,只要那个数量在虚拟堆栈上可用。对?
  • 无论如何,堆栈溢出显然是普通代码中的一个问题,但是在这样的系统中溢出会产生任何额外的灾难性后果吗?

这显然不是必需的,因为简单地将指针返回到真正的堆栈将是完全可用的,或者就此而言,首先不要滥用它们而只是忍受更少的寄存器,我可能不应该尝试这样做完全没有(尤其是因为显然超出了我的理解范围)。但无论哪种方式,我仍然很好奇。想知道这些事情是如何工作的。

编辑:对不起,当然应该说。我正在研究 x86(我自己的机器为 32 位)、Windows 和 Ubuntu。没有什么异国情调。

4

3 回答 3

4

所有这些答案都基于“通用处理器架构”,并且由于它涉及生成汇编代码,因此它必须是“特定于目标的” - 如果您决定在处理器 X 上执行此操作,它对堆栈有一些奇怪的处理,下面是显然不值得它写在[替代纸]上的屏幕表面。对于一般的 x86,除非另有说明,否则以下内容成立。

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

堆栈内存不是“特殊”的吗?我认为线程库必须做这样的事情,因为它们会创建更多的堆栈......

这样的记忆并不特别。然而,这确实假设它不在 x86 架构上,其中堆栈段用于限制堆栈使用。虽然这是可能的,但在实现中很少见。我知道几年前诺基亚有一个使用 32 位模式段的特殊操作系统。就我现在所能想到的,这是我接触过的唯一一个使用栈段的人,正如 x86 分段模式所描述的那样。

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

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

无论如何,堆栈溢出显然是普通代码中的一个问题,但是在这样的系统中溢出会产生任何额外的灾难性后果吗?

确实,如果你有点不走运,这将是一个难以破解的错误。

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

添加一些偶尔检查堆栈深度的代码(您知道堆栈的开始和结束位置,因此检查特定堆栈值是否“超出范围”应该不难[如果您给自己一些“额外的缓冲区”堆栈顶部和您保护的“我们死了”区域之间的空间” - 如果它是一辆撞车的汽车,他们会称之为“崩溃区域”。您还可以用一个填充整个堆栈可识别的模式,并检查其中有多少是“未触及”的。

于 2013-03-03T23:24:28.187 回答
2

通常,在 x86 上,您可以毫无问题地使用现有堆栈,只要:

  • 你不会溢出它
  • 您不会将堆栈指针寄存器(使用popadd esp, positive_value/ sub esp, negative_value)增加超出代码开头的内容(如果这样做,中断或异步回调(信号)或使用堆栈的任何其他活动将丢弃其内容)
  • 您不会导致任何 CPU 异常(如果这样做,异常处理代码可能无法将堆栈展开到可以处理异常的最近点)

这同样适用于将不同的内存块用于临时堆栈并指向esp其末尾。

异常处理和堆栈展开的问题与您编译的 C 和 C++ 代码包含一些与异常处理相关的数据结构(如范围)有关,eip其中包含指向它们各自异常处理程序的链接(这告诉最近的异常处理程序在哪里是针对每一段代码的),还有一些与调用函数的标识相关的信息(即返回地址在堆栈上的位置等),因此您可以冒泡异常。如果你只是将原始机器代码插入到这个“框架”中,你将无法正确扩展这些异常处理数据结构来覆盖它,如果出现问题,它们很可能会出错(整个过程可能会崩溃或损坏,尽管您在生成的代码周围有异常处理程序)。

所以,是的,如果你小心,你可以玩筹码。

于 2013-03-03T23:52:30.713 回答
2

您可以将任何您喜欢的区域用于处理器的堆栈(以内存保护为模)。

本质上,您只需使用指向新区域的指针加载 ESP 寄存器(“MOV ESP,...”),但是您设法分配它。

您必须为您的程序提供足够的资源,以及它可能调用的任何东西(例如,Windows 操作系统 API),以及操作系统具有的任何有趣行为。您可能能够计算出您的代码需要多少空间;一个好的编译器可以很容易地做到这一点。弄清楚 Windows 需要多少更难;您总是可以分配“太多”,这是 Windows 程序倾向于做的事情。

如果您决定严格管理此空间,则可能必须切换堆栈以调用 Windows 函数。这还不够;您可能会被各种 Windows 惊喜所困扰。我在这里描述了其中之一Windows: Avoid push full x86 context on stack。我有平庸的解决方案,但没有很好的解决方案。

于 2013-03-04T00:14:26.133 回答