如何检测到StackOverflowException?
TL; TR
当我问这个问题时,我认为StackOverflowException
是一种防止应用程序无限运行的机制。 这不是真的。
StackOverflowException
未被检测到。
当堆栈没有分配更多内存的能力时抛出它。
[原文:]
这是一个普遍的问题,每种编程语言可能有不同的答案。
我不确定C#以外的语言如何处理堆栈溢出。
我今天遇到异常,并不断思考如何检测到StackOverflowException
。 我相信如果堆栈的调用深度为1000,则不可能说fe,然后抛出异常。 因为在某些情况下,正确的逻辑可能会很深。
在我的程序中检测无限循环的逻辑是什么?
StackOverflowException
类:
https://msdn.microsoft.com/de-de/library/system.stackoverflowexception%28v=vs.110%29.aspx
在StackOverflowException
类文档中提到的交叉引用:
https://msdn.microsoft.com/de-de/library/system.reflection.emit.opcodes.localloc(v=vs.110).aspx
我只是将stack-overflow
标签添加到这个问题中,并且描述表明当调用堆栈消耗太多内存时它将被抛出。 这是否意味着调用堆栈是我的程序当前执行位置的某种路径,如果它不能存储更多的路径信息,那么异常会被抛出?
堆栈溢出
我会让你轻松; 但这实际上相当复杂......请注意,我会在这里概括一下。
您可能知道,大多数语言都使用堆栈来存储呼叫信息。 另请参阅:https://msdn.microsoft.com/en-us/library/zkwh89ks.aspx关于cdecl如何工作。 如果你调用一个方法,你可以将东西放在堆栈上; 如果你回来,你从堆栈中弹出东西。
请注意,递归通常不是“内联”的。 (注意:我在这里明确地说'递归'而不是'尾递归';后者像'goto'一样工作并且不增长堆栈)。
检测堆栈溢出的最简单方法是检查当前的堆栈深度(例如使用的字节数) - 如果遇到边界,则给出错误。 为了澄清这种“边界检查”:这些检查的方式通常是使用警卫页面; 这意味着边界检查通常不会像if-then-else检查那样执行(尽管存在一些实现...)。
在大多数语言中,每个线程都有自己的堆栈。
检测无限循环
那么现在呢,这是我一段时间没有听到的问题。 :-)
基本上检测所有无限循环需要你解决停机问题。 顺便说一句,这是一个不可解决的问题。 这绝对不是由编译器完成的。
这并不意味着你不能做任何分析; 事实上,你可以做很多分析。 但是,还要注意,有时候你希望事情无限期地运行(比如Web服务器中的主循环)。
其他语言
也有趣...功能语言使用递归,所以它们基本上被堆栈绑定。 (也就是说,函数式语言也倾向于使用尾递归,它或多或少地像'goto'一样工作,不会增长堆栈。)
然后是逻辑语言..现在,我不确定如何永远循环 - 你可能会得到一些根本无法评估的东西(无法找到解决方案)。 (虽然这可能取决于语言......)
屈服,异步,延续
一个有趣的概念是你可能会想到的叫做continuations。 我从微软那里听说,当yield
首次实现时,真正的延续被认为是实现。 继续基本上允许你'保存'堆栈,在其他地方继续并在稍后的时间'恢复'堆栈......(再一次,细节比这更复杂;这只是基本的想法)。
不幸的是,微软并没有为这个想法去做(尽管我能想象为什么),但是通过使用助手类来实现它。 C#中的yield和async通过添加一个临时类并实现类中的所有局部变量来工作。 如果你调用了一个'yield'或者'async'的方法,你实际上创建了一个帮助器类(从你调用的方法中推入堆栈),并将它推入堆中。 推入堆的类具有功能(例如,对于yield
这是枚举实现)。 这样做的方式是使用状态变量,该变量存储位置(例如某些状态ID),在MoveNext
时程序应继续MoveNext
。 使用此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);
}
}
尽管这段代码没有任何问题。 它完成所有的工作,但是我们的堆栈阻碍了它。 有一个手动堆栈解决了这个问题,即使实现基本相同:
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));
}
}
}
这里已经有了很多答案,其中许多答案都是要点,其中许多答案都有微妙或大的错误。 我没有试图从头开始解释整个事情,而是让我打出几个高点。
我不确定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。 但是程序刚开始运行时只需要几个字节的堆栈。 所以这是它的工作原理。 当前的堆栈页面是一个完美的提交页面; 这只是正常的记忆。 超出该页面的页面被标记为警戒页面。 我们的百万字节堆栈中的最后一页被标记为非常特殊的警戒页面。
假设我们试图在我们的好堆栈页面之外写入一个堆栈内存字节。 该页面被保护,所以发生页面错误。 操作系统通过使堆栈页良好来处理故障,并且下一页成为新的防护页。
但是,如果最后一个守护页被击中 - 非常特殊 - 那么Windows会触发堆栈外的异常,并且Windows会将防护页重置为“如果该页再次被击中,终止该过程”。 如果发生这种情况,Windows会立即终止该进程。 没有例外。 没有清理代码。 没有对话框。 如果你曾经见过一个Windows应用程序突然完全消失,那么可能发生了什么事是第二次有人在堆栈的末尾点击了警卫页面。
好的,现在我们已经理解了这些机制 - 再次,我在这里详述了很多细节 - 您可能会看到如何编写出现堆栈外例外的代码。 礼貌的方式 - 就是我在VBScript和JScript中所做的 - 是在堆栈上进行虚拟内存查询并询问最终的守护页在哪里。 然后定期查看当前的堆栈指针,如果它在几页之内,只需创建一个VBScript错误或抛出一个JavaScript异常,而不是让操作系统为您做。
如果你不想这样做,那么你可以处理操作系统给你的第一个机会异常,当最终守卫页面被击中时,把它变成C#可以理解的堆栈溢出异常,并且非常小心没有第二次打到警卫页面。
堆栈只是创建线程时分配的固定大小的内存块。 还有一个“堆栈指针”,一种跟踪堆栈当前正在使用多少的方法。 作为创建新堆栈框架的一部分(调用方法,属性,构造函数等时),它会将堆栈指针向上移动新框架需要的量。 那时它会检查是否已经将堆栈指针移到了堆栈的末尾,如果是,则抛出一个SOE。
该程序不会检测到无限递归。 无限递归(当运行时被迫为每个调用创建一个新的栈帧时),它只会导致执行如此多的方法调用以填充这个有限空间。 您可以使用有限数量的嵌套方法调用来轻松填充有限空间,这些调用恰恰会消耗比堆栈更多的空间。 (尽管这通常很难实现;它通常是由递归的方法引起的,而不是无限的,但是具有足够的深度以致堆栈无法处理它。)
链接地址: http://www.djcxy.com/p/14175.html