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

Cri*_*scu 73 .net c# stack-overflow jit tail-recursion

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

(答案:他们不是.但是64位JIT会做TCE(尾部呼叫消除).限制适用.)

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

在提示上,程序以以下任意一个的SO Exception结束:

  • '优化构建'关闭(调试或发布)
  • 目标:x86
  • 目标:AnyCPU +"首选32位"(这是VS 2012中的新功能,也是我第一次看到它.更多信息来自此处.)
  • 代码中有些看似无害的分支(请参阅注释'else'分支).

相反,使用"优化构建" ON +(目标= 64或AnyCPU与"身高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.
Run Code Online (Sandbox Code Playgroud)

和Debug构建:

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.
Run Code Online (Sandbox Code Playgroud)

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

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

UPDATE

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

所以我将代码简化为:

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

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

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

更新:关闭

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

这是一种安全的技术,使得它很难对缓冲区溢出攻击找到的(例如)特定系统调用的精确位置,通过在进程的地址空间随机化的各种东西,包括堆栈位置,显然,它的大小.

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

为了测试这一点,我使用了专门用于该任务的Microsoft工具:EMET或增强型缓解体验工具包.它允许在系统级或进程级设置ASLR标志(以及更多).
(还有一个系统范围的注册表黑客替代方案,我没试过)

EMET GUI

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

在此输入图像描述

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

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

奖金

在旧版新闻中与ASLR相关:Chrome是如何实现的

fej*_*oco 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我不知道ASLR随机化堆栈的*size*! (9认同)
  • 以前从未听说过ASLR.到目前为止+1 - 当我学习新东西时,我喜欢它.明天会考试. (2认同)