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

(回答:They're not.,但64bit 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"的分支).

相反,在+(Target=x64或任何关闭了'prefere 32bit'的CPU(在64位CPU上))上使用'Optimize build'(优化构建),TCE就会发生,计数器会一直旋转(好吧,每次值溢出时,它都会旋转down).

But I noticed a behaviour I can't explainStackOverflowException的情况下:它永远不会(?)发生在相同的堆栈深度.以下是一些32位运行的输出,发布版本:

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.

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

那么,当StackOverflowException次命中时,堆栈深度的变化(有时是不寻常的)是什么原因呢?

使现代化

Hans Passant提出了P/Invoke、互操作和可能的非确定性锁定的问题.

所以我将代码简化为:

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

我在没有调试器的情况下,在Release/32bit/Optimization上运行了它.当程序崩溃时,我连接调试器并判断计数器的值.

而且它在几次运行中都不是相同的.(或者我的测试有缺陷.)

使现代化: Closure

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

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

The theory sounds good. Let's put it into practice!

为了测试这一点,我使用了特定于该任务的Microsoft工具:EMET or The Enhanced Mitigation Experience Toolkit.它允许在系统或进程级别设置ASLR标志(以及更多).
(还有一款system-wide, registry hacking alternative我没试过)

EMET GUI

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

在此处输入图像描述

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

然而,我为整个系统禁用了ASLR,并且(一次重启之后)我最终可以验证,确实,SO异常现在总是在相同的堆栈深度发生.

prize

与ASLR相关的旧新闻:How Chrome got pwned

推荐答案

我想可能有ASLR人在工作.您可以关闭DEP来测试此理论.

有关判断内存信息的C#实用程序类,请参见此处:https://stackoverflow.com/a/8716410/552139

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

更新:好吧,现在我知道我是对的.我继续研究了半页理论,找到了这篇研究Windows:http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf中ASLR实现的文档

引述:

一旦堆栈被放置,初始堆栈指针将进一步 按随机递减量随机化的.初始偏移量为 Select 最多半页(2,048字节)

这就是你问题的答案.ASLR随机取走初始堆栈中的0到2048字节.

.net相关问答推荐

使用EFCore.BulkExtensions库方法BulkInertOrUpdate时区分插入和更新的记录

在 F# 中处理 Option - Some(null) 的好策略是什么

Dotnet 反射:使用 F# 中的out参数调用 MethodInfo 上的调用

cmd 冻结中的 dotnet 命令.怎么了?

如何正确使用await using语法?

为什么这两个比较有不同的结果?

我应该创建一个 DateRange 对象吗?

调整小数精度,.net

是否可以模拟 .NET HttpWebResponse?

发布版本中的 Debug.WriteLine

.NET 的 `Array.Sort()` 方法使用的排序算法是稳定的算法吗?

如何将自定义 UserControl 显示为对话框?

多个等待与 Task.WaitAll - 等效?

Int 到字节数组

MemoryStream.Close() 或 MemoryStream.Dispose()

如何保护我的 .NET 程序集免受反编译?

DataGridView 在我的两个屏幕之一上的可怕重绘性能

如何从文件中删除单个属性(例如只读)?

从字节数组中读取 C# 中的 C/C++ 数据 struct

在 WPF 中设置 Tab 键顺序