为啥递归调用会导致不同堆栈深度的 ***?

Posted

技术标签:

【中文标题】为啥递归调用会导致不同堆栈深度的 ***?【英文标题】:Why does a recursive call cause *** at different stack depths?为什么递归调用会导致不同堆栈深度的 ***? 【发布时间】:2013-12-13 06:54:16 【问题描述】:

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

(答案:They're not. 但 64 位 JIT(s) 将执行 TCE(尾调用消除)。Restrictions apply.)

所以我使用递归调用编写了一个小测试,它打印了在***Exception 终止进程之前它被调用了多少次。

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”分支)。

相反,使用 'Optimize build' ON +(Target = x64 或 AnyCPU with 'Prefer 32bit' OFF(在 64 位 CPU 上)),TCE 发生并且计数器永远旋转(好吧,它可以说是旋转 down 每次它的值溢出)。

但我注意到***Exception 案例中的一种我无法解释的行为:它从来没有(?)发生在完全相同相同的堆栈深度。以下是一些 32 位运行的输出,发布版本:

51600 Random: 1778264579
Process is terminated due to ***Exception.

51599 Random: 1515673450
Process is terminated due to ***Exception.

51602 Random: 1567871768
Process is terminated due to ***Exception.

51535 Random: 2760045665
Process is terminated due to ***Exception.

并调试构建:

28641 Random: 4435795885
Process is terminated due to ***Exception.

28641 Random: 4873901326  //never say never
Process is terminated due to ***Exception.

28623 Random: 7255802746
Process is terminated due to ***Exception.

28669 Random: 1613806023
Process is terminated due to ***Exception.

堆栈大小是恒定的 (defaults to 1 MB)。堆栈帧的大小是恒定的。

那么,当***Exception 命中时,什么可以解释堆栈深度的(有时不是微不足道的)变化?

更新

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 or The Enhanced Mitigation Experience Toolkit。它允许在系统或进程级别设置 ASLR 标志(以及更多)。 (还有一个system-wide, registry hacking alternative没试过)

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

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

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

奖金

与 ASLR 相关,在较早的新闻中:How Chrome got pwned

【问题讨论】:

我已经编辑了你的标题。请参阅“Should questions include “tags” in their titles?”,其中的共识是“不,他们不应该”。 仅供参考:刚刚尝试不使用Random,仅打印sz。也会发生同样的情况。 我想知道有什么技术可以判断 JIT 是否内联了方法调用。 @CristiDiaconescu 在 JIT 编译代码之后在 Visual Studio 中附加一个调试器(通过下拉菜单 Debug->Attach to process 或在代码中放置 Debugger.Attach())然后转到下拉菜单 Debug->Windows->Disassembly查看 JIT 创建的机器代码。请记住,无论您是否附加了调试器,JIT 编译代码的方式都会有所不同,因此请务必在未附加调试器的情况下启动它。 +1 用于发布实际上与 *** 主题相关的问题。可笑的是,有多少人发布根本与堆栈溢出无关的问题! 【参考方案1】:

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

查看这里的 C# 实用程序类来检查内存信息:https://***.com/a/8716410/552139

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

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

引用:

一旦堆栈被放置,初始堆栈指针将进一步 通过随机递减量随机化。初始偏移量为 选择为最多半页(2,048 字节)

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

【讨论】:

以前从未听说过 ASLR。到目前为止 +1 - 我喜欢学习新事物。明天测试。 @Hans:赛门铁克的研究准确地说,堆栈被随机偏移,最多半页,因此有效地减小了大小。 好的,我更喜欢你的解释。【参考方案2】:

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

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

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

【讨论】:

不,它停在不同的深度。即使您完全删除随机数。 它适用于我(在 DEBUG 和 RELEASE 模式下)。 (XP SP3 - VS 2K8 - .NET 3.5) 虽然说得通,但对我来说不是(Win7 64 SP1,VS 2010,.net 4.5) 在 XP 中不是随机的,因为它没有 ASLR。 @AhmedKRAIEM,你确定它对随机种子也不起作用吗?所谓的字符串长度差异不应影响堆栈。毕竟,堆栈只保存(相同数量和大小)指针,指向在堆上分配的字符串。 (此外,在关闭 ASLR 后,它对我的​​两种方式都有效)

以上是关于为啥递归调用会导致不同堆栈深度的 ***?的主要内容,如果未能解决你的问题,请参考以下文章

为啥无限递归会导致段错误

为啥导致 ***Error 的递归方法的调用次数在程序运行之间会有所不同? [复制]

如果不允许 LP 递归,那么可能会出现堆栈溢出的情况?

堆栈溢出一般是由啥原因导致的?

为啥这个递归函数超过调用堆栈大小?

由于递归方法调用导致 Java 堆栈溢出