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


74

我试图弄清楚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();
    }

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

  • 关闭“优化构建”(调试或发布)
  • 目标:x86
  • 目标:AnyCPU +“更喜欢32位”(这是VS 2012中的新功能,也是我第一次看到它。更多内容请点击此处。)
  • 代码中一些看似无害的分支(请参阅注释为“ else”的分支)。

相反,使用“优化构建”为ON +(目标= x64或“首选32位”为OFF(在64位CPU上)为AnyCPU),TCE发生并且计数器一直持续旋转(好的,可以说每次值溢出时它都会旋转))。

但是我注意到在这种StackOverflowException情况下无法解释的行为:它永远不会在完全相同的堆栈深度下发生?以下是一些32位运行版本Release的输出:

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.

和调试版本:

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命中时,如何解释堆栈深度的变化(有时是不平凡的)呢?

更新

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(地址空间布局随机化)。

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

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

为了对此进行测试,我使用了专用于此任务的Microsoft工具:EMET或“增强缓解经验工具包”。它允许在系统级或进程级设置ASLR标志(以及更多)。
(还有我没有尝试过的系统范围的注册表黑客替代方法

EMET GUI

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

在此处输入图片说明

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

但是,我在整个系统上禁用了ASLR,并且(稍后重新启动)我最终可以确认确实如此,因此SO异常现在始终在相同的堆栈深度发生。

奖金

与ASLR相关的较旧的新闻:Chrome是如何伪装的


1
我已经编辑了你的标题。请参阅“问题是否应该在标题中包含“标签”? ”,如果共识为“不,则不应”。
约翰·桑德斯

仅供参考:尝试不带Random且仅打印sz。同样的事情发生。
朱利安Urbano的

我想知道什么技术可以找出JIT是否内联了方法调用。
Cristian Diaconescu

1
@CristiDiaconescu JIT将编译代码(通过下拉列表Debug->Attach to processDebugger.Attach()在代码中放入)后,在Visual Studio中连接调试器,然后转到下拉菜单Debug->Windows->Disassembly查看JIT创建的机器代码。请记住,无论是否连接调试器,JIT编译代码的方式都不同,因此请确保在不连接调试器的情况下启动它。
Scott Chamberlain

12
+1用于发布实际上是StackOverflow主题的问题。可笑的是,有多少人发布的问题根本与堆栈溢出无关!
李·李

Answers:


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个字节。


2
以前从未听说过ASLR。到目前为止+1-当我学习新事物时,我会喜欢它。明天会测试。
Cristian Diaconescu

1
@汉斯:赛门铁克的研究准确地说,堆栈被随机偏移量(最多一页),因此有效地减小了大小。
fejesjoco

好的,我更喜欢您的解释。
汉斯·帕桑

-3

更改r.Next()r.Next(10)StackOverflowExceptions应该出现在相同的深度。

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

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


不,它停在不同的深度。即使您完全删除了随机数。
朱利安Urbano的

它对我有用(在DEBUG和RELEASE模式下)。(XP SP3-VS
2K8-.NET

虽然它是有道理的,它不适合我(Win7的64 SP,VS 2010,.NET 4.5)
朱利安乌尔巴诺

4
在XP中它不是随机的,因为它没有ASLR。
fejesjoco

1
@AhmedKRAIEM,您确定使用随机种子也不适合您吗?所谓的字符串长度差异不应影响堆栈。毕竟,堆栈仅保存(数量和大小相同)指向堆上分配的字符串的指针。(此外,它在关闭ASLR后对我都起作用)
Cristian Diaconescu
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.