Qual è la causa di questo FatalExecutionEngineError in .NET 4.5 beta? [chiuso]


150

Il seguente codice di esempio si è verificato in modo naturale. All'improvviso il mio codice rappresentò un'eccezione molto sgradevole FatalExecutionEngineError. Ho trascorso ben 30 minuti cercando di isolare e minimizzare il campione colpevole. Compilalo usando Visual Studio 2012 come app console:

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

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

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

Dovrebbe produrre questo errore su .NET framework 4 e 4.5:

Schermata FatalExecutionException

È un bug noto, qual è la causa e cosa posso fare per mitigarlo? Il mio lavoro attuale è di non usare string.Empty, ma sto abbaiando sull'albero sbagliato? Cambiare qualsiasi cosa su quel codice lo fa funzionare come ci si aspetterebbe, ad esempio rimuovendo il costruttore statico vuoto Ao cambiando il parametro di tipo da objecta int.

Ho provato questo codice sul mio laptop e non mi sono lamentato. Tuttavia, ho provato la mia app principale ed è andata in crash anche sul laptop. Devo aver distrutto qualcosa nel ridurre il problema, vedrò se riesco a capire cosa fosse.

Il mio laptop si è bloccato con lo stesso codice di cui sopra, con Framework 4.0, ma il crash principale anche con 4.5. Entrambi i sistemi utilizzano VS'12 con gli ultimi aggiornamenti (luglio?).

Ulteriori informazioni :

  • Codice IL (debug compilato / Qualsiasi CPU / 4.0 / VS2010 (non dovrebbe interessare l'IDE?)): Http://codepad.org/boZDd98E
  • Non visto VS 2010 con 4.0. Non crash con / senza ottimizzazioni, CPU di destinazione diversa, debugger collegato / non collegato, ecc. - Tim Medora
  • Si blocca nel 2010 se uso AnyCPU, va bene in x86. Si arresta in modo anomalo in Visual Studio 2010 SP1, utilizzando Platform Target = AnyCPU, ma va bene con Platform Target = x86. Su questa macchina è installato VS2012RC, quindi è possibile che 4.5 esegua una sostituzione sul posto. Usa AnyCPU e TargetPlatform = 3.5 quindi non si arresta in modo anomalo, quindi sembra una regressione nel Framework.- colinsmith
  • Impossibile riprodurre su x86, x64 o AnyCPU in VS2010 con 4.0. - Fuji
  • Succede solo per x64, (2012rc, Fx4.5) - Henk Holterman
  • VS2012 RC su Win8 RP. Inizialmente non viene visualizzato questo MDA quando si utilizza .NET 4.5. Quando si passò al targeting .NET 4.0 apparve la MDA. Quindi, dopo essere tornato a .NET 4.5, rimane il MDA. - Wayne

Non ho mai saputo che potresti creare un costruttore statico insieme a uno pubblico. Diamine, non ho mai saputo che esistessero costruttori statici.
Cole Johnson,

Ho un'idea: perché stai cambiando B da essere in qualche modo una classe statica a solo una classe con un Main statico?
Cole Johnson,

@ChrisSinclair, non credo. Voglio dire, ho testato questo codice sul mio laptop e ho ottenuto gli stessi risultati.
Gleno,

@ColeJohnson Sì, l'IL corrisponde a tutti tranne che all'unico posto ovvio. Non sembra esserci alcun bug qui nel compilatore c #.
Michael Graczyk,

14
Grazie sia al poster originale per averlo segnalato qui, sia a Michael per la sua eccellente analisi. Le mie controparti sul CLR hanno provato a riprodurre il bug qui e hanno scoperto che si riproduce sulla versione "Release Candidate" del CLR a 64 bit, ma non sulla versione finale "Release To Manufacturing", che aveva una serie di correzioni di bug post- RC. (La versione RTM sarà disponibile al pubblico il 15 agosto 2012.) Ritengono quindi che si tratti dello stesso problema di quello riportato qui: connect.microsoft.com/VisualStudio/feedback/details/737108/…
Eric Lippert,

Risposte:


114

Anche questa non è una risposta completa, ma ho alcune idee.

Credo di aver trovato una spiegazione altrettanto valida che troveremo senza che qualcuno del team .NET JIT risponda.

AGGIORNARE

Ho guardato un po 'più a fondo e credo di aver trovato la fonte del problema. Sembra essere causato da una combinazione di un bug nella logica di inizializzazione del tipo JIT e da una modifica nel compilatore C # che si basa sul presupposto che JIT funzioni come previsto. Penso che il bug JIT esistesse in .NET 4.0, ma è stato scoperto dalla modifica nel compilatore per .NET 4.5.

Non penso che beforefieldinitsia l'unico problema qui. Penso che sia più semplice di così.

Il tipo System.Stringin mscorlib.dll da .NET 4.0 contiene un costruttore statico:

.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

Nella versione .NET 4.5 di mscorlib.dll, String.cctor(il costruttore statico) è evidentemente assente:

..... Nessun costruttore statico :( .....

In entrambe le versioni il Stringtipo è decorato con beforefieldinit:

.class public auto ansi serializable sealed beforefieldinit System.String

Ho provato a creare un tipo che si sarebbe compilato in modo simile a IL (in modo che avesse campi statici ma nessun costruttore statico .cctor), ma non sono riuscito a farlo. Tutti questi tipi hanno un .cctormetodo 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(); } 
}

La mia ipotesi è che due cose sono cambiate tra .NET 4.0 e 4.5:

Primo: l'EE è stato modificato in modo String.Emptyda inizializzarsi automaticamente dal codice non gestito. Questa modifica è stata probabilmente apportata per .NET 4.0.

Secondo: il compilatore è cambiato in modo da non emettere un costruttore statico per stringa, sapendo che String.Emptysarebbe stato assegnato dal lato non gestito. Questa modifica sembra essere stata apportata per .NET 4.5.

Sembra che EE non si assegni String.Emptyabbastanza presto lungo alcuni percorsi di ottimizzazione. La modifica apportata al compilatore (o qualsiasi altra modifica per far String.cctorscomparire) prevedeva che EE eseguisse questa assegnazione prima dell'esecuzione di qualsiasi codice utente, ma sembra che EE non esegua tale assegnazione prima che String.Emptyvenga utilizzata nei metodi di classi generiche reificate di tipo di riferimento.

Infine, credo che il bug sia indicativo di un problema più profondo nella logica di inizializzazione del tipo JIT. Sembra che il cambiamento nel compilatore sia un caso speciale per System.String, ma dubito che la JIT abbia fatto un caso speciale qui System.String.

Originale

Prima di tutto, WOW Le persone BCL sono diventate molto creative con alcune ottimizzazioni delle prestazioni. Molti dei Stringmetodi vengono ora eseguiti utilizzando un StringBuilderoggetto memorizzato nella cache statica .

Ho seguito quel comando per un po ', ma StringBuildernon è usato sul Trimpercorso del codice, quindi ho deciso che non poteva essere un problema statico di Thread.

Penso di aver trovato una strana manifestazione dello stesso bug.

Questo codice ha esito negativo con una violazione di accesso:

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

Tuttavia, se si rimuovere il commento //new A<int>(out s);in Mainpoi il codice funziona bene. In effetti, se Aviene reificato con qualsiasi tipo di riferimento, il programma ha esito negativo, ma se Aviene reificato con qualsiasi tipo di valore, il codice non fallisce. Inoltre, se commentate il Acostruttore statico, il codice non fallisce mai. Dopo aver scavato in Trime Format, è chiaro che il problema è che Lengthè stato sottolineato e che in questi esempi sopra il Stringtipo non è stato inizializzato. In particolare, all'interno del corpo del Acostruttore, string.Emptynon è assegnato correttamente, sebbene all'interno del corpo di Main, string.Emptysia assegnato correttamente.

È sorprendente per me che l'inizializzazione del tipo in Stringqualche modo dipenda dal fatto che sia o meno Areificato con un tipo di valore. La mia unica teoria è che esiste un percorso di codice JIT ottimizzante per l'inizializzazione di tipo generico che è condiviso tra tutti i tipi e che quel percorso fa ipotesi sui tipi di riferimento BCL ("tipi speciali?") E sul loro stato. Una rapida occhiata anche se altre classi BCL con public staticcampi mostrano che praticamente tutti implementano un costruttore statico (anche quelli con costruttori vuoti e senza dati, come System.DBNulle System.Empty. I tipi di valore BCL con public staticcampi non sembrano implementare un costruttore statico ( System.IntPtr, ad esempio) Questo sembra indicare che JIT fa alcune ipotesi sull'inizializzazione del tipo di riferimento BCL.

Cordiali saluti Ecco il codice JITed per le due versioni:

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 
    }

Il resto del codice ( Main) è identico tra le due versioni.

MODIFICARE

Inoltre, l'IL delle due versioni è identico ad eccezione della chiamata a A.ctorin B.Main(), dove l'IL per la prima versione contiene:

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

contro

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

nel secondo.

Un'altra cosa da notare è che il codice JITed per A<int>.ctor(out string): è lo stesso della versione non generica.


3
Ho cercato risposte lungo un percorso molto simile, ma non sembra condurre da nessuna parte. Questo sembra essere un problema di classe stringa e si spera non sia un problema più generale. Quindi adesso sto aspettando che qualcuno (Eric) con il codice sorgente arrivi e spieghi cosa è andato storto, e se qualcos'altro viene effettuato. Come piccolo vantaggio, questa discussione ha già risolto il dibattito sull'opportunità di utilizzare string.Emptyo ""... :)
Gleno,

L'IL tra loro è lo stesso?
Cole Johnson,

49
Buona analisi! Lo passerò al team BCL. Grazie!
Eric Lippert,

2
@EricLippert e altri: ho scoperto che codice come typeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty);fornisce risultati diversi su .NET 4.0 rispetto a .NET 4.5. Questa modifica è correlata alla modifica sopra descritta? Come può .NET 4.5 tecnicamente ignorarmi cambiando un valore di campo? Forse dovrei fare una nuova domanda a riguardo?
Jeppe Stig Nielsen

4
@JeppeStigNielsen: Le risposte alle tue domande sono: "forse", "abbastanza facilmente, apparentemente" e "questo è un sito di domande e risposte, quindi sì, è una buona idea se vuoi una risposta migliore alla tua domanda di "forse" ".
Eric Lippert,

3

Sospetto fortemente che ciò sia causato da questa ottimizzazione (relativa a BeforeFieldInit) in .NET 4.0.

Se ricordo bene:

Quando si dichiara esplicitamente un costruttore statico, beforefieldinitviene emesso, indicando al runtime che il costruttore statico deve essere eseguito prima di qualsiasi accesso ai membri statici .

La mia ipotesi:

Direi che in qualche modo incasinato questo fatto sul JITer x64, in modo che quando un altro di tipo membro statico si accede da una classe la cui propria statica costruttore ha già eseguito, in qualche modo salta in esecuzione (o esegue nell'ordine sbagliato) il costruttore statico - e quindi provoca un arresto anomalo. (Non si ottiene un'eccezione puntatore null, probabilmente perché non è inizializzato null.)

Ho Non eseguire il codice, quindi questa parte può essere sbagliato - ma se dovessi fare un altro indovinare, direi che potrebbe essere qualcosa string.Format(o Console.WriteLine, che è simile) ha bisogno di accedere internamente che sta causando l'incidente, come ad esempio forse una classe relativa alle impostazioni locali che necessita di una costruzione statica esplicita.

Ancora una volta, non l'ho testato, ma è la mia ipotesi migliore sui dati.

Sentiti libero di testare la mia ipotesi e fammi sapere come va.


Il bug si verifica ancora quando Bnon ha un costruttore statico e non si verifica quando Aviene reificato con un tipo di valore. Penso che sia un po 'più complicato.
Michael Graczyk,

@MichaelGraczyk: penso di poterlo spiegare (di nuovo, con ipotesi). Bavere un costruttore statico non ha molta importanza. Poiché Aha un ctor statico, il runtime incasina l'ordine in cui viene eseguito rispetto ad alcune classi relative alle impostazioni locali in altri spazi dei nomi. Quindi quel campo non è ancora inizializzato. Tuttavia, se si crea un'istanza Acon un tipo di valore, allora potrebbe essere il secondo passaggio del runtime tramite l'istanza A(probabilmente il CLR lo ha già pre-istanziato con un tipo di riferimento, come ottimizzazione), quindi l'ordine funziona quando viene eseguito una seconda volta .
user541686

@MichaelGraczyk: Anche se questa non è proprio la spiegazione, però - penso di essere abbastanza convinto che l' beforefieldinitottimizzazione data sia la causa principale. È possibile che alcune delle spiegazioni effettive siano diverse da quelle che ho menzionato, ma la causa principale è probabilmente la stessa cosa.
user541686

Ho esaminato di più l'IL, e penso che ti piaccia qualcosa. Non penso che l'idea del secondo passaggio sarà rilevante qui, perché il codice fallisce ancora se faccio arbitrariamente molte chiamate a A<object>.ctor().
Michael Graczyk, l'

@MichaelGraczyk: Buono a sapersi, e grazie per quel test. Purtroppo non riesco a riprodurlo sul mio laptop. (2010 4.0 x64) È possibile verificare se è effettivamente correlato alla formattazione delle stringhe (ovvero alle impostazioni locali)? Cosa succede se rimuovi quella parte?
user541686

1

Un'osservazione, ma DotPeek mostra la stringa decompilata, quindi vuota:

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

Se dichiaro il mio Emptyallo stesso modo, tranne senza l'attributo, non ottengo più l'MDA:

class A<T>
{
    static readonly string Empty;

    static A() { }

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

E con quell'attributo? Lo abbiamo già stabilito ""risolve.
Henk Holterman,

Quell'attributo "Prestazioni critiche ..." influenza il costruttore dell'attributo stesso, non i metodi che l'attributo adorna.
Michael Graczyk,

È interno. Quando definisco il mio stesso identico attributo, non causa ancora la MDA. Non che me lo aspettassi, se JITter sta cercando quel particolare attributo non troverà il mio.
lesscode
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.