.NET 4.5 beta中FatalExecutionEngineError的原因是什么?

时间:2012-08-08 21:56:20

标签: c# clr

以下示例代码自然发生。突然间,我的代码出现了一个非常令人讨厌的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 framework 4和4.5上产生此错误:

FatalExecutionException screenshot

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

我在笔记本电脑上尝试了这个代码并没有抱怨。但是,我确实尝试了我的主应用程序,它也在笔记本电脑上崩溃了。我必须在减少问题的时候把一些东西弄错了,我会看看能不知道那是什么。

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

更多信息

  • IL Code(已编译的Debug / Any CPU / 4.0 / VS2010(不是IDE应该重要吗?)):http://codepad.org/boZDd98E
  • 没有看到VS 2010 with 4.0。没有优化,没有目标CPU,附加/未连接的调试器等等都没有崩溃 - Tim Medora
  • 如果我使用AnyCPU,2010年崩溃,在x86中很好。使用Platform Target = AnyCPU在Visual Studio 2010 SP1中崩溃,但在Platform Target = x86时很好。这台机器也安装了VS2012RC,因此4.5可以进行就地更换。使用AnyCPU和TargetPlatform = 3.5然后它不会崩溃所以看起来像Framework中的回归.- colinsmith
  • 无法在带有4.0的VS2010中的x86,x64或AnyCPU上重现。 - Fuji
  • 仅适用于x64,(2012rc,Fx4.5) - Henk Holterman
  • Win8 RP上的VS2012 RC。最初在针对.NET 4.5时没有看到此MDA。当切换到针对.NET 4.0时,MDA出现了。然后在切换回.NET 4.5后,MDA仍然存在。 - Wayne

3 个答案:

答案 0 :(得分:113)

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

我相信我已经找到了一个很好的解释,我们会发现没有.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

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

  

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

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

.class public auto ansi serializable sealed beforefieldinit System.String

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

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。对编译器所做的更改(或更改为make String.cctor的更改消失)预计EE在执行任何用户代码之前进行此分配,但看起来EE在使用String.Empty之前不进行此分配在引用类型的方法中具体化的泛型类。

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

<强>原始

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

我跟着那个引导了一段时间,但StringBuilder代码路径上没有使用Trim,所以我认为它不是Thread静态问题。

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

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

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的静态构造函数,代码永远不会失败。在深入研究TrimFormat之后,问题显然是Length被内联,而String类型以上的这些示例中的问题尚未初始化。特别是,在A的构造函数体内,string.Empty未正确分配,尽管在Main的正文中,string.Empty已正确分配。

令我惊讶的是,String的类型初始化在某种程度上取决于A是否用值类型来实现。我唯一的理论是,在所有类型之间共享通用类型初始化的优化JIT代码路径,并且该路径对BCL引用类型(“特殊类型?”)及其状态进行假设。快速查看其他带有public static字段的BCL类,可以看出基本上所有都实现了一个静态构造函数(即使是那些带有空构造函数且没有数据的类,如System.DBNull和{{ 1}}。带有System.Empty字段的BCL值类型似乎没有实现静态构造函数(例如public static)。这似乎表明JIT对BCL引用类型初始化做了一些假设。 / p>

FYI以下是两个版本的JITed代码:

<强> System.IntPtr

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 } )在两个版本之间是相同的。

修改

此外,除MainA.ctor的调用外,两个版本的IL相同,其中第一个版本的IL包含:

B.Main()

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

在第二个。

需要注意的另一件事是 ... A`1<int32>... :的JITed代码与非通用版本中的相同。

答案 1 :(得分:3)

我强烈怀疑这是由.NET 4.0中的this optimization (related to BeforeFieldInit)引起的。

如果我没记错的话:

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

我猜:

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

我有运行你的代码,所以这部分可能是错误的 - 但如果我不得不做出另一个猜测,我会说它可能是string.Format(或{ {1}},类似的)需要在内部访问导致崩溃的内容,例如可能是需要显式静态构造的 locale 相关类。

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

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

答案 2 :(得分:1)

观察,但DotPeek显示反编译的字符串。因此:

/// <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);
    }
}