151

下面的示例代码自然而然地发生了。突然间,我的代码出现了一个听起来很讨厌的FatalExecutionEngineError异常。我花了整整 30 分钟试图隔离和最小化罪魁祸首样本。使用 Visual Studio 2012 作为控制台应用程序进行编译:

class A<T>
{
    static A() { }

    public A() { string.Format("{0}", string.Empty); }
}

class B
{
    static void Main() { new A<object>(); }
}

应该在 .NET 框架 4 和 4.5 上产生此错误:

FatalExecutionException 截图

这是一个已知的错误,原因是什么,我能做些什么来减轻它?我目前的工作是不使用string.Empty,但我是在叫错树吗?更改有关该代码的任何内容都会使其按预期运行 - 例如删除 的空静态构造函数A,或将类型参数从 更改objectint

我在我的笔记本电脑上尝试了这个代码,它没有抱怨。但是,我确实尝试了我的主应用程序,但它也在笔记本电脑上崩溃了。在减少问题时,我一定是弄坏了一些东西,我会看看我是否能弄清楚那是什么。

我的笔记本电脑在使用框架 4.0 时使用与上述相同的代码崩溃,但即使使用 4.5 也主要崩溃。两个系统都使用带有最新更新的 VS'12(7 月?)。

更多信息

  • IL 代码(编译后的 Debug/Any CPU/4.0/VS2010(IDE 不重要?)):http ://codepad.org/boZDd98E
  • 没有看到 VS 2010 与 4.0。在有/没有优化、不同的目标 CPU、附加/未附加调试器等情况下不会崩溃 - Tim Medora
  • 如果我使用 AnyCPU,2010 年崩溃,在 x86 中很好。在 Visual Studio 2010 SP1 中崩溃,使用 Platform Target = AnyCPU,但使用 Platform Target=x86 很好。这台机器也安装了 VS2012RC,所以 4.5 可能会进行就地更换。使用 AnyCPU 和 TargetPlatform = 3.5 然后它不会崩溃,所以看起来像框架中的回归。- colinsmith
  • 无法在 VS2010 4.0 中的 x86、x64 或 AnyCPU 上重现。–富士
  • 仅适用于 x64,(2012rc,Fx4.5) - Henk Holterman
  • Win8 RP 上的 VS2012 RC。最初针对 .NET 4.5 时未看到此 MDA。当切换到面向 .NET 4.0 时,MDA 出现了。然后在切换回 .NET 4.5 后,MDA 仍然存在。-韦恩
4

3 回答 3

115

这也不是一个完整的答案,但我有一些想法。

我相信在没有 .NET JIT 团队的任何人回答的情况下,我已经找到了一个很好的解释。

更新

我看的更深了,我相信我找到了问题的根源。它似乎是由 JIT 类型初始化逻辑中的错误和 C# 编译器中的更改引起的,该更改依赖于 JIT 按预期工作的假设。我认为 JIT 错误存在于 .NET 4.0 中,但被 .NET 4.5 的编译器更改发现。

我不认为这beforefieldinit是这里唯一的问题。我认为它比这更简单。

.NET 4.0 中的 mscorlib.dll 中的类型System.String包含一个静态构造函数:

.method private hidebysig specialname rtspecialname static 
    void  .cctor() cil managed
{
  // Code size       11 (0xb)
  .maxstack  8
  IL_0000:  ldstr      ""
  IL_0005:  stsfld     string System.String::Empty
  IL_000a:  ret
} // end of method String::.cctor

在 mscorlib.dll 的 .NET 4.5 版本中,String.cctor(静态构造函数)明显不存在:

.....没有静态构造函数:( .....

在这两个版本中,String类型都装饰有beforefieldinit

.class public auto ansi serializable sealed beforefieldinit System.String

我尝试创建一个可以类似地编译为 IL 的类型(因此它具有静态字段但没有静态构造函数.cctor),但我做不到。所有这些类型.cctor在 IL 中都有一个方法:

public class MyString1 {
    public static MyString1 Empty = new MyString1();        
}

public class MyString2 {
    public static MyString2 Empty = new MyString2();

    static MyString2() {}   
}

public class MyString3 {
    public static MyString3 Empty;

    static MyString3() { Empty = new MyString3(); } 
}

我的猜测是 .NET 4.0 和 4.5 之间发生了两件事:

首先:EE 已更改,以便它可以自动String.Empty从非托管代码初始化。此更改可能是针对 .NET 4.0 进行的。

其次:编译器​​发生了变化,因此它不会为字符串发出静态构造函数,因为它知道String.Empty将从非托管端分配。此更改似乎是针对 .NET 4.5 进行的。

似乎 EE在某些优化路径上分配得不够快。String.Empty对编译器所做的更改(或任何使String.cctor消失的更改)期望 EE 在执行任何用户代码之前进行此分配,但似乎 EE 在String.Empty用于引用类型具体化泛型类的方法之前并未进行此分配。

最后,我认为该错误表明 JIT 类型初始化逻辑中存在更深层次的问题。看来编译器中的更改是 的特例System.String,但我怀疑 JIT 是否在这里为System.String.

原来的

首先,WOW BCL 人在一些性能优化方面非常有创意。 许多方法String现在使用 Thread 静态缓存StringBuilder对象执行。

我跟随了一段时间,但StringBuilder没有在Trim代码路径上使用,所以我认为它不可能是线程静态问题。

我想我发现了同一个错误的奇怪表现。

此代码因访问冲突而失败:

class A<T>
{
    static A() { }

    public A(out string s) {
        s = string.Empty;
    }
}

class B
{
    static void Main() { 
        string s;
        new A<object>(out s);
        //new A<int>(out s);
        System.Console.WriteLine(s.Length);
    }
}

但是,如果您取消注释//new A<int>(out s);Main那么代码就可以正常工作。事实上,如果A用任何引用类型具体化,程序就会失败,但如果A用任何值类型具体化,那么代码不会失败。此外,如果您注释掉A的静态构造函数,则代码永远不会失败。在深入研究Trimand之后Format,很明显问题在于Length被内联,并且在上面的这些示例中,String类型尚未初始化。特别是,在A的构造函数体内,string.Empty没有正确分配,尽管在 , 的体内Mainstring.Empty正确分配。

令我惊讶的是,类型初始化以String某种方式取决于是否A用值类型具体化。我唯一的理论是,所有类型共享的通用类型初始化有一些优化的 JIT 代码路径,并且该路径对 BCL 引用类型(“特殊类型?”)及其状态做出了假设。快速浏览其他具有字段的 BCL 类public static表明,它们基本上实现了静态构造函数(即使是那些具有空构造函数且没有数据的类,例如System.DBNullSystem.Empty。具有字段的 BCL 值类型public static似乎没有实现静态构造函数(System.IntPtr例如) . 这似乎表明 JIT 对 BCL 引用类型初始化做了一些假设。

仅供参考,这是两个版本的 JITed 代码:

A<object>.ctor(out string)

    public A(out string s) {
00000000  push        rbx 
00000001  sub         rsp,20h 
00000005  mov         rbx,rdx 
00000008  lea         rdx,[FFEE38D0h] 
0000000f  mov         rcx,qword ptr [rcx] 
00000012  call        000000005F7AB4A0 
            s = string.Empty;
00000017  mov         rdx,qword ptr [FFEE38D0h] 
0000001e  mov         rcx,rbx 
00000021  call        000000005F661180 
00000026  nop 
00000027  add         rsp,20h 
0000002b  pop         rbx 
0000002c  ret 
    }

A<int32>.ctor(out string)

    public A(out string s) {
00000000  sub         rsp,28h 
00000004  mov         rax,rdx 
            s = string.Empty;
00000007  mov         rdx,12353250h 
00000011  mov         rdx,qword ptr [rdx] 
00000014  mov         rcx,rax 
00000017  call        000000005F691160 
0000001c  nop 
0000001d  add         rsp,28h 
00000021  ret 
    }

其余代码 ( Main) 在两个版本之间是相同的。

编辑

此外,两个版本的 IL 是相同的,除了对A.ctorin的调用B.Main(),其中第一个版本的 IL 包含:

newobj     instance void class A`1<object>::.ctor(string&)

相对

... A`1<int32>...

在第二。

需要注意的另一件事是 : 的 JITed 代码A<int>.ctor(out string)与非通用版本中的相同。

于 2012-08-09T02:48:12.370 回答
3

我强烈怀疑这是由.NET 4.0 中的这种优化(与 .NET 相关BeforeFieldInit)引起的。

如果我没记错的话:

当您显式声明静态构造函数时,beforefieldinit会发出,告诉运行时静态构造函数必须在任何静态成员访问之前运行

我猜:

我猜他们以某种方式在 x64 JITer 上搞砸了这个事实,所以当从一个自己的静态构造函数已经运行的类访问不同类型的静态成员时,它会以某种方式跳过运行(或以错误的顺序执行)静态构造函数——因此会导致崩溃。(你没有得到空指针异常,可能是因为它不是空初始化的。)

没有运行你的代码,所以这部分可能是错误的——但如果我不得不再猜测一下,我会说它可能是某些东西string.Format(或Console.WriteLine类似的)需要在内部访问导致崩溃,例如也许是一个需要显式静态构造的与语言环境相关的类。

同样,我还没有测试它,但这是我对数据的最佳猜测。

随意测试我的假设,让我知道它是怎么回事。

于 2012-08-11T01:20:54.693 回答
1

一个观察结果,但 DotPeek 显示了反编译的 string.Empty :

/// <summary>
/// Represents the empty string. This field is read-only.
/// </summary>
/// <filterpriority>1</filterpriority>
[__DynamicallyInvokable]
public static readonly string Empty;

internal sealed class __DynamicallyInvokableAttribute : Attribute
{
  [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
  public __DynamicallyInvokableAttribute()
  {
  }
}

如果我以相同的方式声明自己Empty的属性,但没有属性,我将不再获得 MDA:

class A<T>
{
    static readonly string Empty;

    static A() { }

    public A()
    {
        string.Format("{0}", Empty);
    }
}
于 2012-08-09T00:08:58.230 回答