151

The sample code below occurred naturally. Suddenly my code thew a very nasty-sounding FatalExecutionEngineError exception. I spent a good 30 minutes trying to isolate and minimize the culprit sample. Compile this using Visual Studio 2012 as a console app:

class A<T> { static A() { } public A() { string.Format("{0}", string.Empty); } } class B { static void Main() { new A<object>(); } } 

Should produce this error on .NET framework 4 and 4.5:

FatalExecutionException screenshot

Is this a known bug, what is the cause and what can I do to mitigate it? My current work around is to not use string.Empty, but am I barking up the wrong tree? Changing anything about that code makes it function as you would expect - for example removing the empty static constructor of A, or changing the type paramter from object to int.

I tried this code on my laptop and it didn't complain. However, I did try my main app and it crashed on the laptop as well. I must have mangled away something when reducing the problem, I'll see if I can figure out what that was.

My laptop crashed with the same code as above, with framework 4.0, but main crashes even with 4.5. Both systems are using VS'12 with latest updates (July?).

More information:

  • IL Code (compiled Debug/Any CPU/4.0/VS2010 (not that IDE should matter?)): http://codepad.org/boZDd98E
  • Not seen VS 2010 with 4.0. Not crashing with/without optimizations, different target CPU, debugger attached/not attached, etc. - Tim Medora
  • Crashes in 2010 if I use AnyCPU, is fine in x86. Crashes in Visual Studio 2010 SP1, using Platform Target = AnyCPU, but fine with Platform Target=x86. This machine has VS2012RC installed as well so 4.5 possibly doing an in-place replacement. Use AnyCPU and TargetPlatform = 3.5 then it doesn't crash so looks like a regression in the Framework.- colinsmith
  • Cannot reproduce on x86, x64 or AnyCPU in VS2010 with 4.0. – Fuji
  • Only happens for x64, (2012rc, Fx4.5) - Henk Holterman
  • VS2012 RC on Win8 RP. Initially Not seeing this MDA when targeting .NET 4.5. When switched to targeting .NET 4.0 the MDA appeared. Then after switching back to .NET 4.5 the MDA remains. - Wayne
4
  • @ChrisSinclair, I don't think so. I mean I tested this code on my laptop, and got same results. Commented Aug 8, 2012 at 22:24
  • @ColeJohnson Yes the IL matches in all but the one obvious place. There does not appear to be any bug here in the c# compiler. Commented Aug 10, 2012 at 23:58
  • 15
    Thanks both to the original poster for reporting it here, and to Michael for his excellent analysis. My counterparts on the CLR tried to reproduce the bug here and discovered that it reproduces on the "Release Candidate" version of the 64 bit CLR, but not on the final "Released To Manufacturing" version, which had a number of bug fixes post-RC. (The RTM version will be available to the public on August 15th, 2012.) They therefore believe this to be the same issue as the one that was reported here: connect.microsoft.com/VisualStudio/feedback/details/737108/… Commented Aug 13, 2012 at 20:44
  • @casperOne You probably should just moderator close this again, but I've queried this reopening on meta. Commented Feb 15, 2013 at 7:19

3 Answers 3

115

This is also not a full answer, but I have a few ideas.

I believe I have found as good an explanation as we will find without somebody from the .NET JIT team answering.

UPDATE

I looked a little deeper, and I believe I have found the source of the issue. It appears to be caused by a combination of a bug in the JIT type-initialization logic, and a change in the C# compiler that relies on the assumption that the JIT works as intended. I think the JIT bug existed in .NET 4.0, but was uncovered by the change in the compiler for .NET 4.5.

I do not think that beforefieldinit is the only issue here. I think it's simpler than that.

The type System.String in mscorlib.dll from .NET 4.0 contains a static constructor:

.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 

In the .NET 4.5 version of mscorlib.dll, String.cctor (the static constructor) is conspicuously absent:

..... No static constructor :( .....

In both versions the String type is adorned with beforefieldinit:

.class public auto ansi serializable sealed beforefieldinit System.String 

I tried to create a type that would compile to IL similarly (so that it has static fields but no static constructor .cctor), but I could not do it. All of these types have a .cctor method in 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(); } } 

My guess is that two things changed between .NET 4.0 and 4.5:

First: The EE was changed so that it would automatically initialize String.Empty from unmanaged code. This change was probably made for .NET 4.0.

Second: The compiler changed so that it did not emit a static constructor for string, knowing that String.Empty would be assigned from the unmanaged side. This change appears to have been made for .NET 4.5.

It appears that the EE does not assign String.Empty soon enough along some optimization paths. The change made to the compiler (or whatever changed to make String.cctor disappear) expected the EE make this assignment before any user code executes, but it appears that the EE does not make this assignment before String.Empty is used in methods of reference type reified generic classes.

Lastly, I believe that the bug is indicative of a deeper problem in the JIT type-initialization logic. It appears the change in the compiler is a special case for System.String, but I doubt that the JIT has made a special case here for System.String.

Original

First of all, WOW The BCL people have gotten very creative with some performance optimizations. Many of the String methods are now performed using a Thread static cached StringBuilder object.

I followed that lead for a while, but StringBuilder isn't used on the Trim code path, so I decided it couldn't be a Thread static problem.

I think I found a strange manifestation of the same bug though.

This code fails with an access violation:

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

However, if you uncomment //new A<int>(out s); in Main then the code works just fine. In fact, if A is reified with any reference type, the program fails, but if A is reified with any value type then the code does not fail. Also if you comment out A's static constructor, the code never fails. After digging into Trim and Format, it is clear that the problem is that Length is being inlined, and that in these samples above the String type has not been initialized. In particular, inside the body of A's constructor, string.Empty is not correctly assigned, although inside the body of Main, string.Empty is assigned correctly.

It is amazing to me that the type initialization of String somehow depends on whether or not A is reified with a value type. My only theory is that there is some optimizing JIT code path for generic type-initialization that is shared among all types, and that that path makes assumptions about BCL reference types ("special types?") and their state. A quick look though other BCL classes with public static fields shows that basically all of them implement a static constructor (even those with empty constructors and no data, like System.DBNull and System.Empty. BCL value types with public static fields do not seem to implement a static constructor (System.IntPtr, for instance). This seems to indicate that the JIT makes some assumptions about BCL reference type initialization.

FYI Here is the JITed code for the two versions:

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 } 

The rest of the code (Main) is identical between the two versions.

EDIT

In addition, the IL from the two versions is identical except for the call to A.ctor in B.Main(), where the IL for the first version contains:

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

versus

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

in the second.

Another thing to note is that the JITed code for A<int>.ctor(out string): is the same as in the non-generic version.

Sign up to request clarification or add additional context in comments.

6 Comments

I've searched for answers along a very similar path, but it doesn't seem to lead anywhere. This appears to be a string class problem and hopefully not a more general issue. So right now I'm waiting for someone (Eric) with the source code to come along and explain what went wrong, and if anything else is effected. As a small benefit this discussion already settled the debate whether one should use string.Empty or ""... :)
Is the IL between them the same?
Good analysis! I will pass it along to the BCL team. Thanks!
@EricLippert and others: I discovered that code like typeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty); gives different results on .NET 4.0 versus .NET 4.5. Is this change related to the change described above? How can .NET 4.5 technically ignore me changing a field value? Maybe I should ask a new question about this?
@JeppeStigNielsen: The answers to your questions are: "maybe,", "quite easily, apparently," and "this is a question-and-answer site, so yes, that's a good idea if you want an answer to your question better than 'maybe'".
|
3

I strongly suspect this is caused by this optimization (related to BeforeFieldInit) in .NET 4.0.

If I remember correctly:

When you declare a static constructor explicitly, beforefieldinit is emitted, telling the runtime that the static constructor must be run before any static member access.

My guess:

I'd guess that they somehow screwed up this fact on the x64 JITer, so that when a different type's static member is accessed from a class whose own static constructor has already run, it somehow skips running (or executes in the wrong order) the static constructor -- and therefore causes a crash. (You don't get a null pointer exception, probably because it's not null-initialized.)

I have not run your code, so this part may be wrong -- but if I had to make another guess, I'd say it might be something string.Format (or Console.WriteLine, which is similar) needs to access internally that's causing the crash, such as perhaps a locale-related class which needs explicit static construction.

Again, I haven't tested it, but it's my best guess at the data.

Feel free to test my hypothesis and let me know how it goes.

6 Comments

The bug still occurs when B does not have a static constructor, and it doesn't occur when A is reified with a value type. I think it is a little more complicated.
@MichaelGraczyk: I think I can explain that (again, with guesses). B having a static constructor doesn't matter much. Since A has a static ctor, the runtime messes up the order in which it is run when compared with some locale-related class in some other namespace. So that field is not yet initialized. However, if you instantiate A with a value type, then it might be the runtime's second pass through instantiating A (the CLR has likely already pre-instantiated it with a reference type, as an optimization) so the order works out when it's run a second time.
@MichaelGraczyk: Even if this isn't quite the explanation, though -- I think I'm quite convinced that the given beforefieldinit optimization is the root cause. It might be that some of the actual explanation is different from what I mentioned, but the root cause is likely the same thing.
I looked into the IL more, and I think you are onto something. I do not think that the second pass idea is going to be relevant here, because the code still fails if I do arbitrarily many calls to A<object>.ctor().
@MichaelGraczyk: Good to hear, and thanks for that test. I can't reproduce it on my own laptop, unfortunately. (2010 4.0 x64) Can you check to see if it's indeed related to the string-formatting (i.e. locale-related)? What happens if you remove that part?
|
1

An observation, but DotPeek shows the decompiled string.Empty thus:

/// <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() { } } 

If I declare my own Empty the same way except without the attribute, I no longer get the MDA:

class A<T> { static readonly string Empty; static A() { } public A() { string.Format("{0}", Empty); } } 

3 Comments

And with that attribute? We already established "" solves it.
That "Performance critical ... " Attribute affects the Attribute constructor itself, not the methods that the attribute adorns.
It's internal. When I define my own identical attribute it still doesn't cause the MDA. Not that I would expect it to -- if the JITter is looking for that specific attribute it won't find mine.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.