【问题标题】:What's the cause of this FatalExecutionEngineError in .NET 4.5 beta? [closed].NET 4.5 beta 中这个 FatalExecutionEngineError 的原因是什么? [关闭]
【发布时间】:2012-08-06 02:39:38
【问题描述】:

下面的示例代码自然而然地发生了。突然间,我的代码出现了一个听起来很讨厌的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 上产生此错误:

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

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

我的笔记本电脑使用与上述相同的代码崩溃,使用框架 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 上重现。 – Fuji
  • 仅适用于 x64,(2012rc, Fx4.5) - Henk Holterman
  • Win8 RP 上的 VS2012 RC。最初针对 .NET 4.5 时未看到此 MDA。当切换到面向 .NET 4.0 时,MDA 出现了。然后在切换回 .NET 4.5 后,MDA 仍然存在。 - Wayne

【问题讨论】:

  • 我从来不知道你可以创建一个静态构造函数和一个公共构造函数。哎呀,我从来不知道存在静态构造函数。
  • 我有一个想法:因为您正在将 B 从某种静态类更改为仅具有静态 Main 的类?
  • @ChrisSinclair,我不这么认为。我的意思是我在笔记本电脑上测试了这段代码,得到了相同的结果。
  • @ColeJohnson 是的,除了一个明显的地方,IL 在所有地方都匹配。 c# 编译器中似乎没有任何错误。
  • 感谢原始发帖人在此报道,感谢 Michael 出色的分析。我在 CLR 上的同行试图在这里重现这个错误,发现它在 64 位 CLR 的“候选发布”版本上重现,但在最终的“发布到制造”版本上没有重现,该版本在发布后修复了许多错误钢筋混凝土。 (RTM 版本将于 2012 年 8 月 15 日向公众提供。)因此,他们认为这与此处报告的问题相同:connect.microsoft.com/VisualStudio/feedback/details/737108/…

标签: c# clr


【解决方案1】:

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

我相信我找到了一个很好的解释,因为没有 .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。对编译器所做的更改(或使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);
    }
}

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

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

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

A&lt;object&gt;.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&lt;int32&gt;.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&)

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

在第二个。

另外需要注意的是 A&lt;int&gt;.ctor(out string): 的 JIT 代码与非通用版本中的相同。

【讨论】:

  • 我已经沿着非常相似的路径搜索了答案,但它似乎并没有导致任何地方。这似乎是一个字符串类问题,希望不是更普遍的问题。所以现在我正在等待某人(Eric)提供源代码并解释出现了什么问题,以及是否有其他影响。作为一个小好处,这个讨论已经解决了应该使用string.Empty 还是""... :)
  • 它们之间的IL是一样的吗?
  • 好分析!我会将它传递给 BCL 团队。谢谢!
  • @EricLippert 和其他人:我发现像 typeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty); 这样的代码在 .NET 4.0 和 .NET 4.5 上给出了不同的结果。此更改是否与上述更改有关? .NET 4.5 如何在技术上忽略我更改字段值?也许我应该就此提出一个新问题?
  • @JeppeStigNielsen:您的问题的答案是:“也许”、“显然很容易”和“这是一个问答网站,所以是的,如果你想要一个比“也许”更好的答案。
【解决方案2】:

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

如果我没记错的话:

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

我的猜测:

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

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

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

请随意测试我的假设并告诉我结果如何。

【讨论】:

  • 这个bug在B没有静态构造函数时依然会出现,当A用值类型具体化时不会出现。我认为它有点复杂。
  • @MichaelGraczyk:我想我可以解释一下(再次,有猜测)。 B 拥有静态构造函数并不重要。由于A 有一个静态ctor,与其他命名空间中的某些与语言环境相关的类相比,运行时会弄乱它的运行顺序。因此该字段尚未初始化。但是,如果您使用值类型实例化 A,那么它可能是运行时的 second 传递通过实例化 A(CLR 可能已经使用引用类型预先实例化它,作为优化),所以当它第二次运行时,订单就会生效。
  • @MichaelGraczyk:尽管这不是完全的解释——我想我非常相信给定的beforefieldinit 优化是根本原因。可能是一些实际的解释与我提到的不同,但根本原因可能是同一件事。
  • 我对 IL 进行了更多研究,我认为您有所了解。我不认为第二遍的想法在这里是相关的,因为如果我对A&lt;object&gt;.ctor() 进行任意多次调用,代码仍然会失败。
  • @MichaelGraczyk:很高兴听到这个消息,感谢您的测试。不幸的是,我无法在自己的笔记本电脑上复制它。 (2010 4.0 x64)你能检查一下它是否确实与字符串格式相关(即与语言环境相关)?如果删除该部分会发生什么?
【解决方案3】:

观察,但 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);
    }
}

【讨论】:

  • 那个属性?我们已经建立"" 解决它。
  • “性能关键......”属性影响 Attribute 构造函数本身,而不是属性修饰的方法。
  • 它是内部的。当我定义自己的相同属性时,它仍然不会导致 MDA。并不是我期望它——如果 JITter 正在寻找那个特定的属性,它不会找到我的。
猜你喜欢
  • 2016-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-08
  • 2021-08-09
  • 1970-01-01
相关资源
最近更新 更多