如何检测到StackOverflowException?

Noe*_*mer 67 c# stack-overflow infinite-loop

TL; TR
当我问这个问题时,我假设a StackOverflowException是一种阻止应用程序无限运行的机制.这不是真的.
A StackOverflowException未被检测到.
当堆栈没有分配更多内存的容量时抛出它.

[原始问题:]

这是一个普遍的问题,每个编程语言可能有不同的答案.
我不确定C#以外的语言如何处理堆栈溢出.

我今天经历了例外,并一直在思考如何StackOverflowException检测到它.我相信不可能说fe是否深度为1000次调用,然后抛出异常.因为在某些情况下,正确的逻辑可能会那么深.

在我的程序中检测无限循环的逻辑是什么?

StackOverflowExceptionclass:
https://msdn.microsoft.com/de-de/library/system.stackoverflowexception%28v=vs.110%29.aspx类文档中

提到的交叉引用StackOverflowException:https:
//msdn.microsoft.com/de -de /库/ system.reflection.emit.opcodes.localloc(v = vs.110)的.aspx

我刚刚在stack-overflow这个问题上添加了标记,并且描述说当调用堆栈消耗太多内存时它会被抛出.这是否意味着调用堆栈是我的程序的当前执行位置的某种路径,如果它不能存储更多的路径信息,那么抛出异常?

atl*_*ste 42

堆栈溢出

我会让你轻松; 但这实际上非常复杂......请注意,我会在这里概括一下.

您可能知道,大多数语言都使用堆栈来存储呼叫信息.另请参阅:https://msdn.microsoft.com/en-us/library/zkwh89ks.aspx,了解cdecl的工作原理.如果你调用一个方法,你可以在堆栈上推送东西; 如果你回来,你从堆栈中弹出东西.

请注意,递归通常不会"内联".(注意:我在这里明确说'递归'而不是'尾递归';后者的工作方式类似于'goto'并且不会增加堆栈).

检测堆栈溢出的最简单方法是检查当前堆栈深度(例如使用的字节数) - 如果它遇到边界,则给出错误.澄清这种"边界检查":这些检查的方式通常是使用保护页面; 这意味着边界检查通常不会像if-then-else检查那样实现(尽管存在一些实现......).

在大多数语言中,每个线程都有自己的堆栈.

检测无限循环

那么现在,这是一个我暂时没有听到过的问题.:-)

基本上检测所有无限循环需要您解决停机问题.这是一个不可判定的问题.这绝对不是由编译器完成的.

这并不意味着你不能做任何分析; 事实上,你可以做很多分析.但是,请注意,有时您希望无限期地运行(例如Web服务器中的主循环).

其他语言

同样有趣...功能语言使用递归,因此它们基本上由堆栈绑定.(也就是说,函数式语言也倾向于使用尾递归,尾递归或多或少像'goto',并且不会增加堆栈.)

然后是逻辑语言..现在好了,我不知道如何永远循环 - 你可能最终得到一些根本无法评估的东西(没有找到解决方案).(虽然,这可能取决于语言...)

屈服,异步,延续

您可能会想到一个有趣的概念叫做延续.我从微软那里听说,yield第一次实施时,真正的延续被视为实施.Continuations基本上允许你"保存"堆栈,继续其他地方并在以后"恢复"堆栈......(同样,细节要比这复杂得多;这只是基本的想法).

不幸的是,微软没有采用这个想法(虽然我可以想象为什么),但是通过使用辅助类来实现它.C#中的yield和async通过添加临时类并实现类中的所有局部变量来实现.如果你调用一个执行'yield'或'async'的方法,你实际上创建了一个帮助类(来自你调用的方法并推送堆栈),它被推送到堆上.在堆上推送的类具有功能(例如,yield这是枚举实现).这样做的方法是使用一个状态变量,它存储程序在MoveNext被调用时应该继续的位置(例如某个状态id).使用此ID的分支(交换机)负责其余部分.请注意,此机制对堆栈的工作方式没有任何"特殊"; 您可以使用类和方法自己实现相同的(它只涉及更多的输入:-)).

使用手动堆栈解决堆栈溢出问题

我总是喜欢充满洪水.如果你做错了,一张图片会给你一个很多的递归调用......比如说:

public void FloodFill(int x, int y, int color)
{
    // Wait for the crash to happen...
    if (Valid(x,y))
    {
        SetPixel(x, y, color);
        FloodFill(x - 1, y, color);
        FloodFill(x + 1, y, color);
        FloodFill(x, y - 1, color);
        FloodFill(x, y + 1, color);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是这个代码没有错.它完成了所有的工作,但我们的堆栈阻碍了它.有一个手动堆栈解决了这个问题,即使实现基本相同:

public void FloodFill(int x, int y, int color)
{
    Stack<Tuple<int, int>> stack = new Stack<Tuple<int, int>>();
    stack.Push(new Tuple<int, int>(x, y));
    while (stack.Count > 0)
    {
        var current = stack.Pop();

        int x2 = current.Item1;
        int y2 = current.Item2;

        // "Recurse"
        if (Valid(x2, y2))
        {
            SetPixel(x2, y2, color);
            stack.Push(new Tuple<int, int>(x2-1, y2));
            stack.Push(new Tuple<int, int>(x2+1, y2));
            stack.Push(new Tuple<int, int>(x2, y2-1));
            stack.Push(new Tuple<int, int>(x2, y2+1));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 暂停问题不是NP-Complete,而是_Undecidable_. (12认同)
  • 许多函数式编程语言都支持[tail recursion](http://stackoverflow.com/questions/33923/what-is-tail-recursion).许多递归函数(如此函数)可以转换为尾递归,因此只使用常量堆栈空间. (5认同)
  • @Chris Java(以及HotSpot)当然不会*"只是溢出".规范要求任何实现抛出stackoverflow异常并避免任何未定义的行为(假设没有JNI).这是在通常使用保护页面实现的现代系统上(没有明确比较指针,因为这只是不必要的昂贵,这当然不是Raja声称的"通常"方式.另一方面,你需要明确检查以确保你有阴影页面的空间). (3认同)
  • @Voo为了完整性:.NET也使用保护页面,就像我所知道的大多数C++实现一样.LLVM/Clang曾经使用带有检查的分段堆栈/堆栈,但是现在他们也使用防护页面.对于那些感兴趣的人,https://github.com/rust-lang/rust/issues/14742给出了一些不错的细节. (3认同)
  • @ BlueRaja-DannyPflughoeft我想不出很多托管语言实际上比较堆栈和堆指针来检测溢出(虽然我绝不是其中许多人的专家).例如,Java只设置了一个堆栈大小限制并超出了它.我认为声称这个答案不正确是不公平的,然后指出一堆不普遍的事情. (2认同)

Eri*_*ert 33

这里已经有很多答案,其中许多都得到了要点,其中许多都有微妙或大的错误.不要试图从头开始解释整个事情,而是让我达到一些高点.

我不确定C#以外的语言如何处理堆栈溢出.

你的问题是"如何检测到堆栈溢出?" 您有关于如何在C#或其他语言中检测到它的问题吗?如果您对其他语言有疑问,建议您创建一个新问题.

我认为不可能说(例如)堆栈是否为1000深度调用,然后抛出异常.因为在某些情况下,正确的逻辑可能会那么深.

完全可以实现这样的堆栈溢出检测.在实践中,这不是如何完成的,但是没有原则上的理由说明为什么系统不能以这种方式设计.

在我的程序中检测无限循环的逻辑是什么?

你的意思是一个无限的递归,而不是一个无限的循环.

我将在下面描述它.

我刚刚在这个问题中添加了堆栈溢出标记,并且描述说当调用堆栈消耗太多内存时它会被抛出.这是否意味着调用堆栈是我的程序的当前执行位置的某种路径,如果它不能存储更多的路径信息,那么抛出异常?

简短回答:是的.

更长的答案:调用堆栈用于两个目的.

首先,表示激活信息.也就是说,其生存期等于或短于方法的当前激活("调用")的局部变量和临时值的值.

第二,代表继续信息.也就是说,当我完成这个方法时,我接下来需要做什么?请注意,栈根本没有代表自己:"我是从哪里来的?".堆栈代表我下一步要去的地方,通常当一个方法返回时,你会回到你来自的地方.

堆栈还存储非本地延续的信息 - 即异常处理.当方法抛出时,调用堆栈包含的数据可帮助运行时确定哪些代码(如果有)包含相关的catch块.那个catch块然后变成了方法的延续 - "我接下来要做什么".

现在,在我继续之前,我注意到调用堆栈是一个用于两个目的的数据结构,违反了单一责任原则.没有要求有一个堆栈用于两个目的,实际上有一些奇特的架构,其中有两个堆栈,一个用于激活帧,一个用于返回地址(这是继续的具体化.)这样的架构是不太容易受到像C这样的语言中可能发生的"堆栈粉碎"攻击.

当你调用一个方法时,在堆栈上分配内存来存储返回地址 - 我接下来该做什么 - 以及激活帧 - 新方法的本地.Windows上的堆栈默认为固定大小,因此如果没有足够的空间,就会发生坏事.

更详细地说,Windows如何进行堆栈检测?

我在20世纪90年代为32位Windows版本的VBScript和JScript编写了栈外检测逻辑.CLR使用与我使用的类似技术,但如果您想了解CLR特定的详细信息,则必须咨询CLR专家.

我们只考虑32位Windows; 64位Windows的工作原理类似.

Windows当然使用虚拟内存 - 如果您不了解虚拟内存的工作原理,那么现在是您在阅读之前学习的好时机.每个进程都有一个32位的扁平地址空间,一半为操作系统保留,一半用于用户代码.默认情况下,每个线程都有一个1兆字节地址空间的保留连续块.(注意:这是线程重量级的一个原因.当你首先只有20亿个字节时,一百万字节的连续内存就很多了.)

这里有一些细微之处,关于这个连续的地址空间是仅仅是保留还是实际提交,但让我们对它们进行修饰.我将继续描述它在传统Windows程序中的工作原理,而不是进入CLR细节.

好吧,所以我们可以说一百万字节的内存,分为250页,每页4kb.但是程序首次开始运行时只需要几kb的堆栈.所以这是它的工作原理.当前的堆栈页面是一个非常好的提交页面; 这只是正常的记忆.超出该页面的页面被标记为保护页面.我们的百万字节堆栈中的最后一页被标记为一个非常特殊的保护页面.

假设我们尝试在堆栈页面之外编写一个堆栈内存字节.该页面受到保护,因此发生页面错误.操作系统通过使堆栈页面良好来处理故障,并且下一页成为新的保护页面.

但是,如果最后一个防护页面被击中 - 非常特殊 - 那么Windows会触发堆栈外异常,而Windows会将防护页面重置为"如果再次命中此页面,则终止进程".如果发生这种情况,Windows会立即终止该过程.没有例外.没有清理代码.没有对话框.如果你曾经看到一个Windows应用程序突然完全消失,可能发生的事情是有人第二次在堆栈末端点击了防护页面.

好了,现在我们理解了这些机制 - 再次,我在这里讨论了很多细节 - 你可能会看到如何编写产生堆栈外异常的代码.礼貌的方式 - 我在VBScript和JScript中所做的 - 是在堆栈上进行虚拟内存查询并询问最终防护页面的位置.然后定期查看当前的堆栈指针,如果它在几页内,只需创建一个VBScript错误或者抛出一个JavaScript异常,而不是让操作系统为你做.

如果你不想这样做试探自己,那么你可以处理的第一次机会异常操作系统给你当最终保护页被击中,把它转换成,C#语言理解堆栈溢出异常,并且非常小心,以没有第二次打防守页面.

  • @MarcGravell:我应该更清楚地说这个页面是错误的.是的,这样做的目的是避免每个线程必须提交一百万个字节,但由于它自己的原因我相信CLR确实提交了整个堆栈.我不记得为什么. (3认同)
  • 一如既往的精美细节.问题:在第一次触摸之前,中间页面作为防护页面的目的是什么?这是因为操作系统可以及时为虚拟空间分配物理内存吗?否则,我不清楚为什么它会想要自己处理异常只是为了标记好... (2认同)

Ser*_*rvy 14

堆栈只是一个固定大小的内存块,在创建线程时分配.还有一个"堆栈指针",一种跟踪当前正在使用多少堆栈的方法.作为创建新堆栈帧的一部分(在调用方法,属性,构造函数等时),它会将堆栈指针向上移动新帧所需的量.那时它将检查是否已经将堆栈指针移动到堆栈的末尾,如果是,则抛出SOE.

该程序无法检测无限递归.无限递归(当运行时被强制为每次调用创建一个新的堆栈帧时),它只会导致执行如此多的方法调用以填充此有限空间.您可以使用有限数量的嵌套方法调用轻松填充有限空间,这些方法调用恰好比堆栈消耗更多空间.(这往往很难做到;它通常是由递归的方法引起的,而不是无限的,但是有足够的深度,堆栈无法处理它.)


Dav*_*vid 6

警告:这与引擎盖机制有很大关系,包括CLR本身必须如何工作.如果你开始学习汇编级编程,这才有意义.

在引擎盖下,通过将控制传递给另一个方法的站点来执行方法调用.为了传递参数和返回,它们被加载到堆栈中.为了知道如何将控制返回给调用方法,CLR还必须实现一个调用堆栈,调用堆栈时调用方法并从方法返回时弹出.这个堆栈就返回方法,其中返回控制.

由于计算机只有一个有限的内存,因此有时候调用堆栈会变得太大.因此,StackOverflowException不是一个无限运行或无限递归程序的检测,它是检测计算机不能再处理跟踪您的方法需要返回,所需的参数所需的堆栈的大小,退货,变量或(更常见地)它们的组合.在无限递归期间发生此异常的事实是因为逻辑不可避免地压倒了堆栈.

要回答你的问题,如果一个程序故意有逻辑会使堆栈超载,那么你会看到一个StackOverflowException.然而,除非你创建了一个无限递归循环,否则这通常是数千到数百万次深度调用并且很少成为实际问题.

附录:我提到递归循环的原因是因为只有当你覆盖堆栈时才会发生异常 - 这通常意味着你正在调用最终回调到同一方法并增长调用堆栈的方法.如果你的某些东西在逻辑上是无限的,但不是递归的,你通常不会看到StackOverflowException


usr*_*usr 5

堆栈溢出的问题并不在于它们可能源于无限计算。问题是堆栈内存耗尽,而堆栈内存是当今操作系统和语言中的有限资源。

当程序尝试访问超出分配给堆栈的内存部分时,会检测到此情况。这会导致异常。