在.NET 4.5 beta 版本中出现这个 FatalExectionEngineError 的原因是什么?

下面的示例代码是自然产生的。突然,我的代码解冻了一个听起来非常讨厌的 FatalExecutionEngineError异常。我花了整整30分钟试图分离并最小化罪魁祸首的样本。使用 VisualStudio2012作为控制台应用程序编译此文件:

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 screenshot

这是一个已知的错误,什么是原因,我可以做什么来减轻它?我目前的工作是不使用 string.Empty,但是我是否找错了树?改变这段代码的任何内容都可以让它正常工作——例如,删除 A的空静态构造函数,或者将类型参数从 object改为 int

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

我的笔记本电脑在框架4.0的时候崩溃了,代码和上面一样,但是 main 在框架4.5的时候崩溃了。两个系统都在使用 VS’12的最新更新(7月?).

更多信息 :

  • IL 代码(已编译的 Debug/AnyCPU/4.0/VS2010(不是说 IDE 应该重要?)) : http://codepad.org/boZDd98E
  • 没有看到 VS 2010与4.0。没有崩溃与/没有优化,不同的目标 CPU,调试器附加/未附加,等等-Tim Medora
  • 如果我使用 AnyCPU,那么在 x86中崩溃是可以的。在 VisualStudio2010SP1中崩溃,使用 Platform Target = AnyCPU,但是可以使用 Platform Target = x86。这台机器已经安装了 VS2012RC 以及4.5可能做就地更换。使用 AnyCPU 和 TargetPlatform = 3.5,它就不会崩溃,所以看起来像框架中的回归。Colinsmith
  • 无法在4.0版本的 VS2010中的 x86、 x64或 AnyCPU 上重现
  • 只发生在 x64,(2012rc,Fx4.5)-亨克 · 霍尔特曼
  • VS2012 RC 对 Win8 RP。最初在瞄准时没有看到这个 MDA。NET 4.5.当我们转换目标时。NET 4.0的 MDA 出现了。然后切换回。NET 4.5 MDA 保留。韦恩
12501 次浏览

一个观察结果,但是 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);
}
}

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

我相信我已经找到了一个很好的解释。NET JIT 团队回答。

更新

我深入调查了一下,我相信我已经找到了问题的根源。它似乎是由于 JIT 类型初始化逻辑中的一个 bug 和 C # 编译器中的一个变化引起的,这个变化依赖于 JIT 按预期工作的假设。我认为 JIT 错误存在于。NET 4.0,但是被编译器中的。NET 4.5.

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

来自.NET 4.0的 mscolib.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版本的 mscolib.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 发出静态构造函数,因为它知道将从非托管端分配 String.Empty。这种改变似乎是为了。NET 4.5.

似乎 EE 没有很快就沿着某些优化路径分配了 String.Empty。对编译器所做的更改(或者为了使 String.cctor消失而做的任何更改)期望 EE 在执行任何用户代码之前进行这种赋值,但是看起来 EE 在 String.Empty用于引用类型被物化的泛型类的方法之前并没有进行这种赋值。

最后,我认为这个 bug 表明了 JIT 类型初始化逻辑中的一个更深层次的问题。编译器中的变化似乎是 System.String的特殊情况,但我怀疑 JIT 在这里对 System.String做了特殊处理。

原创的

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

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

不过我发现了同一种病毒的奇怪表现。

此代码在访问冲突时失败:

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

但是,如果取消 Main//new A<int>(out s);的注释,那么代码就可以正常工作了。实际上,如果用任何引用类型实现了 A,程序就会失败,但是如果用任何值类型实现了 A,那么代码就不会失败。另外,如果注释掉 A的静态构造函数,代码永远不会失败。在深入研究了 TrimFormat之后,很明显,问题在于 Length是内联的,而且在这些以上的样本中,String类型还没有被初始化。特别是,在 A的构造函数体内,Main0没有被正确分配,尽管在 Main的体内,Main0被正确分配。

令我惊讶的是,String的类型初始化在某种程度上取决于 A是否具有值类型。我唯一的理论是,存在一些针对泛型类型初始化的优化 JIT 代码路径,这些路径在所有类型之间共享,并且该路径对 BCL 引用类型(“特殊类型?”)进行了假设和他们的国家。快速浏览一下其他具有 public static字段的 BCL 类,就会发现基本上其中的 所有实现了一个静态构造函数(即使是那些具有空构造函数且没有数据的类,如 System.DBNullSystem.Empty)。带有 public static字段的 BCL 值类型似乎不实现静态构造函数(例如,System.IntPtr)。这似乎表明 JIT 对 BCL 引用类型初始化做了一些假设。

以下是两个版本的 JIT 代码:

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 是相同的,除了对 B.Main()中的 A.ctor的调用,其中第一个版本的 IL 包含:

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

VS

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

在第二个。

另一件需要注意的事情是,A<int>.ctor(out string): 的 JIT 代码与非泛型版本相同。

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

如果我没记错的话:

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

我猜:

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

我让 没有运行你的代码,所以这部分可能是错的——但是如果我必须做另一个猜测的话,我会说可能是 string.Format(或者类似的 Console.WriteLine)内部需要访问的东西导致了崩溃,比如一个需要显式静态构造的与 地点相关的类。

再说一次,我还没有测试过,但这是我对数据的最佳猜测。

随时检验我的假设,告诉我进展如何。