堆栈溢出是如何发生的,确保它不会发生的最佳方法是什么,或者防止堆栈溢出的方法,特别是在 Web 服务器上,但其他示例也很有趣?

有帮助吗?

解决方案

在这种情况下,堆栈是程序运行时放置数据的后进先出缓冲区。后进先出 (LIFO) 意味着您最后放入的内容始终是您首先取出的内容 - 如果您将 2 个项目压入堆栈,“A”然后是“B”,那么您弹出的第一个项目堆栈外将是“B”,下一个是“A”。

当您在代码中调用函数时,函数调用后的下一条指令将存储在堆栈以及可能被函数调用覆盖的任何存储空间中。您调用的函数可能会为其自己的局部变量使用更多堆栈。完成后,它释放它使用的局部变量堆栈空间,然后返回到前一个函数。

堆栈溢出

堆栈溢出是指您使用的堆栈内存多于程序应使用的内存。在嵌入式系统中,堆栈可能只有 256 字节,如果每个函数占用 32 字节,那么您只能进行 8 层函数调用 - 函数 1 调用函数 2,函数 2 调用函数 3,函数 3 调用函数 4 ....谁调用函数 8 谁调用函数 9,但函数 9 覆盖了堆栈外的内存。这可能会覆盖内存、代码等。

许多程序员都会犯这样的错误:调用函数 A,然后调用函数 B,然后调用函数 C,然后调用函数 A。它可能在大多数情况下都有效,但只要有一次错误的输入,就会导致它永远陷入这个循环,直到计算机认识到堆栈被过度膨胀。

递归函数也是造成这种情况的一个原因,但如果您以递归方式编写(即您的函数调用自身),那么您需要意识到这一点并使用静态/全局变量来防止无限递归。

一般来说,您使用的操作系统和编程语言管理堆栈,但它不在您的控制范围内。您应该查看调用图(一个树结构,显示主函数中每个函数调用的内容),以了解函数调用的深度,并检测非预期的循环和递归。有意循环和递归需要人为检查,以便在它们相互调用次数过多时出错。

除了良好的编程实践、静态和动态测试之外,您在这些高级系统上无能为力。

嵌入式系统

在嵌入式世界中,特别是在高可靠性代码(汽车、飞机、太空)中,您需要进行广泛的代码审查和检查,但您还需要执行以下操作:

  • 禁止递归和循环 - 由策略和测试强制执行
  • 保持代码和堆栈远离(代码在闪存中,堆栈在 RAM 中,两者永远不会相遇)
  • 在堆栈周围放置保护带 - 用幻数填充的内存空白区域(通常是软件中断指令,但这里有很多选项),每秒数百或数千次查看保护带以确保它们没有被覆盖。
  • 使用内存保护(即不在堆栈上执行,不在堆栈外读取或写入)
  • 中断不会调用辅助函数 - 它们设置标志,复制数据,并让应用程序负责处理它(否则你的函数调用树可能会达到 8 层深度,产生一个中断,然后在函数调用树中执行另外几个函数)中断,导致井喷)。您有几个调用树 - 一个用于主进程,一个用于每个中断。如果你的打扰可以互相打扰......嗯,有龙……

高级语言和系统

但在操作系统上运行的高级语言中:

  • 减少局部变量存储(局部变量存储在堆栈上 - 尽管编译器对此非常聪明,如果您的调用树很浅,有时会将大局部变量放在堆上)
  • 避免或严格限制递归
  • 不要将程序分解为越来越小的函数 - 即使不计算局部变量,每个函数调用也会在堆栈上消耗多达 64 个字节(32 位处理器,节省一半的 CPU 寄存器、标志等)
  • 保持你的调用树浅(类似于上面的语句)

网络服务器

您是否可以控制甚至查看堆栈取决于您拥有的“沙箱”。您很可能可以像对待任何其他高级语言和操作系统一样对待 Web 服务器 - 这很大程度上超出了您的控制范围,但请检查您正在使用的语言和服务器堆栈。它 例如,可能会破坏 SQL 服务器上的堆栈。

-亚当

其他提示

实际代码中堆栈溢出很少发生。大多数发生这种情况的情况都是忘记终止的递归。然而,它可能很少发生在高度嵌套的结构中,例如特别大的 XML 文档。这里唯一真正的帮助是重构代码以使用显式堆栈对象而不是调用堆栈。

大多数人会告诉您,没有退出路径的递归会发生堆栈溢出 - 虽然大多数情况都是如此,但如果您使用足够大的数据结构,即使正确的递归退出路径也无济于事。

在这种情况下的一些选项:

无限递归是导致堆栈溢出错误的常见方法。为了防止 - 始终确保有一个出口路径 将要 被击中。:-)

另一种导致堆栈溢出的方法(至少在 C/C++ 中)是在堆栈上声明一些巨大的变量。

char hugeArray[100000000];

这样就可以了。

当 Jeff 和 Joel 希望为世界提供一个更好的地方来获取技术问题的答案时,就会发生堆栈溢出。阻止这种堆栈溢出已经太晚了。那个“其他网站”本来可以通过不模糊来阻止它。;)

通常,堆栈溢出是无限递归调用的结果(考虑到当今标准计算机中通常的内存量)。

当您以“标准”方式调用方法、函数或过程或进行调用时:

  1. 将调用的返回方向压入堆栈(即调用后的下一句话)
  2. 通常返回值的空间被保留到堆栈中
  3. 将每个参数压入堆栈(顺序有所不同,取决于每个编译器,有时也将其中一些参数存储在 CPU 寄存器中以提高性能)
  4. 进行实际通话。

因此,通常这需要几个字节,具体取决于参数的数量和类型以及机器架构。

然后您将看到,如果开始进行递归调用,堆栈将开始增长。现在,堆栈通常以与堆相反的方向增长的方式保留在内存中,因此,如果没有“返回”大量调用,堆栈就会开始变满。

现在,在过去,堆栈溢出可能仅仅因为耗尽了所有可用内存而发生,就像那样。对于超出范围的虚拟内存模型(X86 系统上最多 4GB),如果出现堆栈溢出错误,通常会查找无限递归调用。

除了从直接递归中获得的堆栈溢出形式(例如 Fibonacci(1000000)),我多次经历过的一种更微妙的形式是间接递归,其中一个函数调用另一个函数,另一个函数调用另一个函数,然后其中一个函数再次调用第一个函数。

这通常发生在为响应事件而调用的函数中,但它们本身可能会生成新事件,例如:

void WindowSizeChanged(Size& newsize) {
  // override window size to constrain width
    newSize.width=200;
    ResizeWindow(newSize);
}

在这种情况下,调用 ResizeWindow 可能会导致 WindowSizeChanged() 再次触发回调,调用 ResizeWindow 再次,直到你用完堆栈。在这样的情况下,您通常需要推迟响应事件,直到堆栈帧返回,例如通过发布消息。

什么?没有人会爱那些陷入无限循环的人吗?

do
{
  JeffAtwood.WritesCode();
} while(StackOverflow.MakingMadBank.Equals(false));

考虑到这被标记为“黑客”,我怀疑他所指的“堆栈溢出”是调用堆栈溢出,而不是更高级别的堆栈溢出,例如此处大多数其他答案中引用的堆栈溢出。它实际上并不适用于通常编写 Web 应用程序的任何托管或解释环境,例如 .NET、Java、Python、Perl、PHP 等,因此您唯一的风险是 Web 服务器本身,它可能是用以下语言编写的C 或 C++。

查看此线程:

https://stackoverflow.com/questions/7308/what-is-a-good-starting-point-for-learning-buffer-overflow

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top