为什么recursion调用会导致StackOverflow处于不同的堆栈深度?

我试图找出C#编译器如何处理tail调用。

(答案: 他们不是,但是64位的JIT将会执行TCE(tail call elimination)。

所以我使用recursion调用编写了一个小testing,打印了多less次在StackOverflowException进程之前被调用。

 class Program { static void Main(string[] args) { Rec(); } static int sz = 0; static Random r = new Random(); static void Rec() { sz++; //uncomment for faster, more imprecise runs //if (sz % 100 == 0) { //some code to keep this method from being inlined var zz = r.Next(); Console.Write("{0} Random: {1}\r", sz, zz); } //uncommenting this stops TCE from happening //else //{ // Console.Write("{0}\r", sz); //} Rec(); } 

正确的提示,该程序结束以下任何exception:

  • “优化构build”OFF(debugging或发布)
  • 目标:x86
  • 目标:AnyCPU +“更喜欢32位”(这是VS 2012中的新function,也是我第一次看到它。
  • 代码中一些看起来无害的分支(请参见注释“else”分支)。

相反,使用'优化构build'ON +(目标= 64位或任何CPU与'首选32位'OFF(在一个64位CPU)),TCE发生,计数器永远旋转起来(好吧,它可以说是每次它的数值溢出)。

但是我注意到了一个我StackOverflowException情况下无法解释的行为 :它永远不会(?)发生在完全相同的堆栈深度。 这里是几个32位运行的输出,Release build:

 51600 Random: 1778264579 Process is terminated due to StackOverflowException. 51599 Random: 1515673450 Process is terminated due to StackOverflowException. 51602 Random: 1567871768 Process is terminated due to StackOverflowException. 51535 Random: 2760045665 Process is terminated due to StackOverflowException. 

和debugging版本:

 28641 Random: 4435795885 Process is terminated due to StackOverflowException. 28641 Random: 4873901326 //never say never Process is terminated due to StackOverflowException. 28623 Random: 7255802746 Process is terminated due to StackOverflowException. 28669 Random: 1613806023 Process is terminated due to StackOverflowException. 

堆栈大小不变( 默认为1 MB )。 堆栈帧的大小是不变的。

那么,当StackOverflowException命中时,什么可以解释栈深度的变化(有时是非平凡的)呢?

UPDATE

Hans Passant提出了Console.WriteLine触及P / Invoke,Interop和可能的非确定性锁的问题。

所以我简化了代码:

 class Program { static void Main(string[] args) { Rec(); } static int sz = 0; static void Rec() { sz++; Rec(); } } 

我在没有debugging器的情况下在Release / 32bit / Optimization ON中运行它。 当程序崩溃时,我附加debugging器并检查计数器的值。

而且在几次运行中仍然不一样。 (或者我的testing是有缺陷的。)

更新:closures

正如fejesjoco所build议的,我研究了ASLR(地址空间布局随机化)。

这是一种安全技术,通过随机化进程地址空间中的各种东西(包括堆栈位置以及显然其大小),缓冲区溢出攻击很难find(例如)特定系统调用的精确位置。

这个理论听起来不错。 让我们把它付诸实践!

为了testing这个,我使用了一个专门用于该任务的Microsoft工具: EMET或增强型缓解体验工具包 。 它允许在系统级或进程级设置ASLR标志(以及更多)。
(也有一个系统范围的registry黑客替代scheme ,我没有尝试)

EMET GUI

为了validation该工具的有效性,我还发现Process Explorer会在进程的“属性”页面中正确报告ASLR标志的状态。 从来没有见过,直到今天:)

在这里输入图像说明

理论上,EMET可以(重新)为单个进程设置ASLR标志。 实际上,它似乎没有改变任何东西(见上图)。

但是,我禁用整个系统的ASLR和(稍后重启一次),我终于可以确认,SOexception现在总是发生在相同的堆栈深度。

奖金

ASLR相关的,在旧的消息: Chrome如何得到

我认为这可能是ASLR的工作。 你可以closuresDEP来testing这个理论。

看到这里的C#工具类来检查内存信息: https : //stackoverflow.com/a/8716410/552139

顺便说一下,使用这个工具,我发现最大和最小堆栈大小之间的差异大约是2 KiB,这是半页。 这很奇怪。

更新:好的,现在我知道我是对的。 我跟进了半页理论,发现这个文档检查Windows中的ASLR实现: http : //www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf

引用:

一旦堆栈已经被放置,初始堆栈指针被进一步随机化为一个随机的递减量。 初始偏移量select为半页(2,048字节)

这是你的问题的答案。 ASLR随机抽取0到2048字节的初始堆栈。

r.Next()更改为r.Next(10)StackOverflowException应该发生在相同的深度。

生成的string应该消耗相同的内存,因为它们具有相同的大小。 r.Next(10).ToString().Length == 1 alwaysr.Next().ToString().Length是可变的。

如果使用r.Next(100, 1000)