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

时间:2013-11-27 14:58:06

标签: c# .net stack-overflow jit tail-recursion

我试图弄清楚C#编译器如何处理尾调用。

(答案:They're not.但是 64位JIT(s)会做TCE(尾部呼叫消除)。Restrictions apply。)

所以我使用递归调用编写了一个小测试,该调用打印在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();
    }

在提示上,该程序以以下任何一项的SO Exception结束:

  • '优化构建'关闭(调试或发布)
  • 目标:x86
  • 目标:AnyCPU +“首选32位”(这是VS 2012中的新功能,也是我第一次看到它。More here。)
  • 代码中一些看似无害的分支(请参阅注释'else'分支)。

相反,使用'优化构建'ON +(目标= x64或AnyCPU,'偏好32位'关闭(在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.

和Debug build:

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.

堆栈大小是常量(defaults to 1 MB)。堆栈帧的大小是不变的。

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

更新

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

所以我将代码简化为:

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

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

它的仍然在几次运行中不一样。 (或者我的测试有缺陷。)

更新:关闭

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

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

理论听起来不错。让我们付诸实践吧!

为了对此进行测试,我使用了专门用于该任务的Microsoft工具:EMET or The Enhanced Mitigation Experience Toolkit。它允许在系统级或进程级设置ASLR标志(以及更多) (我还没有尝试system-wide, registry hacking alternative

EMET GUI

为了验证该工具的有效性,我还发现Process Explorer在流程的“属性”页面中适当地报告了ASLR标志的状态。直到今天才看到它:)

enter image description here

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

但是,我为整个系统禁用了ASLR并且(稍后重新启动)我终于可以验证确实,SO异常现在总是在相同的堆栈深度发生。

奖金

ASLR相关,旧版新闻:How Chrome got pwned

2 个答案:

答案 0 :(得分:51)

我认为可能ASLR正在工作。你可以关闭DEP来测试这个理论。

请参阅此处了解用于检查内存信息的C#实用程序类:https://stackoverflow.com/a/8716410/552139

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

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

引用:

  

放置堆栈后,初始堆栈指针会更远   随机递减量随机化。初始偏移是   选择最多半页(2,048字节)

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

答案 1 :(得分:-3)

r.Next()更改为r.Next(10)StackOverflowException应该出现在同一深度。

生成的字符串应使用相同的内存,因为它们具有相同的大小。 r.Next(10).ToString().Length == 1 始终r.Next().ToString().Length是可变的。

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

,则同样适用