web-dev-qa-db-fra.com

Quelle est la cause de cette erreur FatalExecutionEngineError dans .NET 4.5 beta?

L'exemple de code ci-dessous s'est produit naturellement. Soudain, mon code devint une exception très désagréable FatalExecutionEngineError. J'ai passé 30 bonnes minutes à essayer d'isoler et de minimiser l'échantillon coupable. Compilez cela à l'aide de Visual Studio 2012 comme application console:

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

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

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

Devrait produire cette erreur sur .NET Framework 4 et 4.5:

FatalExecutionException screenshot

Est-ce un bug connu, quelle en est la cause et que puis-je faire pour l'atténuer? Mon travail actuel consiste à ne pas utiliser string.Empty, mais suis-je en train d'aboyer le mauvais arbre? Changer quoi que ce soit à propos de ce code le fait fonctionner comme vous vous en doutez - par exemple, supprimer le constructeur statique vide de A, ou changer le paramètre de type de object en int.

J'ai essayé ce code sur mon ordinateur portable et il ne s'est pas plaint. Cependant, j'ai essayé mon application principale et elle s'est également écrasée sur l'ordinateur portable. Je dois avoir mutilé quelque chose en réduisant le problème, je vais voir si je peux comprendre ce que c'était.

Mon ordinateur portable s'est écrasé avec le même code que ci-dessus, avec le framework 4.0, mais le principal plante même avec 4.5. Les deux systèmes utilisent VS'12 avec les dernières mises à jour (juillet?).

Plus d'informations :

  • Code IL (Debug compilé/Any CPU/4.0/VS2010 (pas que IDE devrait avoir de l'importance?)): http://codepad.org/boZDd98E
  • Pas vu VS 2010 avec 4.0. Ne plante pas avec/sans optimisations, CPU cible différent, débogueur attaché/non attaché, etc. - Tim Medora
  • Crashes en 2010 si j'utilise AnyCPU, c'est bien en x86. Crashes dans Visual Studio 2010 SP1, en utilisant Platform Target = AnyCPU, mais très bien avec Platform Target = x86. Cette machine a également installé VS2012RC, donc 4.5 peut-être effectuer un remplacement sur place. Utilisez AnyCPU et TargetPlatform = 3.5, puis il ne se bloque pas, il ressemble donc à une régression dans le Framework.- colinsmith
  • Impossible de reproduire sur x86, x64 ou AnyCPU dans VS2010 avec 4.0. - Fuji
  • Ne se produit que pour x64, (2012rc, Fx4.5) - Henk Holterman
  • VS2012 RC sur Win8 RP. Initialement ne voyant pas ce MDA lors du ciblage de .NET 4.5. Lorsqu'il est passé au ciblage .NET 4.0, le MDA est apparu. Ensuite, après être revenu à .NET 4.5, le MDA reste. - Wayne
150
Gleno

Ce n'est pas non plus une réponse complète, mais j'ai quelques idées.

Je crois avoir trouvé une aussi bonne explication que nous trouverons sans que quelqu'un de l'équipe .NET JIT ne réponde.

MISE À JOUR

J'ai regardé un peu plus en profondeur et je pense avoir trouvé la source du problème. Il semble être provoqué par une combinaison d'un bogue dans la logique d'initialisation de type JIT et d'un changement dans le compilateur C # qui repose sur l'hypothèse que le JIT fonctionne comme prévu. Je pense que le bogue JIT existait dans .NET 4.0, mais a été découvert par le changement dans le compilateur pour .NET 4.5.

Je ne pense pas que beforefieldinit soit le seul problème ici. Je pense que c'est plus simple que ça.

Le type System.String Dans mscorlib.dll de .NET 4.0 contient un constructeur statique:

.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

Dans la version .NET 4.5 de mscorlib.dll, String.cctor (Le constructeur statique) est visiblement absent:

..... Pas de constructeur statique :( .....

Dans les deux versions, le type String est orné de beforefieldinit:

.class public auto ansi serializable sealed beforefieldinit System.String

J'ai essayé de créer un type qui se compilerait en IL de manière similaire (afin qu'il ait des champs statiques mais pas de constructeur statique .cctor), Mais je n'ai pas pu le faire. Tous ces types ont une méthode .cctor En 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(); } 
}

Je suppose que deux choses ont changé entre .NET 4.0 et 4.5:

Premièrement: l'EE a été modifiée afin qu'elle initialise automatiquement String.Empty À partir du code non géré. Cette modification a probablement été apportée à .NET 4.0.

Deuxièmement: le compilateur a changé pour ne pas émettre de constructeur statique pour la chaîne, sachant que String.Empty Serait attribué du côté non géré. Cette modification semble avoir été effectuée pour .NET 4.5.

Il semble que l'EE n'attribue pas String.Empty Assez tôt le long de certains chemins d'optimisation. La modification apportée au compilateur (ou tout ce qui a changé pour faire disparaître String.cctor) Prévoyait que l'EE effectue cette affectation avant l'exécution de tout code utilisateur, mais il semble que l'EE n'effectue pas cette affectation avant String.Empty est utilisé dans les méthodes des classes génériques réifiées de type référence.

Enfin, je pense que le bogue indique un problème plus profond dans la logique d'initialisation de type JIT. Il semble que le changement dans le compilateur soit un cas spécial pour System.String, Mais je doute que le JIT ait fait un cas spécial ici pour System.String.

Original

Tout d'abord, WOW Les gens de BCL sont devenus très créatifs avec quelques optimisations de performances. De nombreuses des méthodes String sont désormais exécutées à l'aide d'un objet Thread statique mis en cache StringBuilder.

J'ai suivi cet exemple pendant un certain temps, mais StringBuilder n'est pas utilisé sur le chemin de code Trim, j'ai donc décidé que cela ne pouvait pas être un problème statique de thread.

Je pense que j'ai trouvé une étrange manifestation du même bug.

Ce code échoue avec une violation d'accès:

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

Cependant, si vous décommentez //new A<int>(out s); dans Main alors le code fonctionne très bien. En fait, si A est réifié avec n'importe quel type de référence, le programme échoue, mais si A est réifié avec n'importe quel type de valeur, le code n'échoue pas. De plus, si vous commentez le constructeur statique de A, le code n'échoue jamais. Après avoir creusé dans Trim et Format, il est clair que le problème est que Length est en ligne, et que dans ces exemples ci-dessus, le type String a pas été initialisé. En particulier, à l'intérieur du corps du constructeur de A, string.Empty N'est pas correctement attribué, bien qu'à l'intérieur du corps de Main, string.Empty Soit attribué correctement.

Il est étonnant pour moi que l'initialisation du type de String dépend en quelque sorte si A est réifié ou non avec un type de valeur. Ma seule théorie est qu'il existe un chemin d'optimisation du code JIT pour l'initialisation de type générique qui est partagé entre tous les types, et que ce chemin fait des hypothèses sur les types de référence BCL ("types spéciaux?") Et leur état. Un coup d'œil rapide à travers les autres classes BCL avec les champs public static Montre que fondamentalement toutes implémentent un constructeur statique (même celles avec des constructeurs vides et aucune donnée, comme System.DBNull et System.Empty. Les types de valeur BCL avec des champs public static ne semblent pas implémenter un constructeur statique (System.IntPtr, par exemple). semble indiquer que le JIT fait quelques hypothèses sur l'initialisation du type de référence BCL.

FYI Voici le code JITed pour les deux 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 
    }

Le reste du code (Main) est identique entre les deux versions.

MODIFIER

De plus, l'IL des deux versions est identique à l'exception de l'appel à A.ctor Dans B.Main(), où l'IL pour la première version contient:

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

versus

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

dans la seconde.

Une autre chose à noter est que le code JITed pour A<int>.ctor(out string): est le même que dans la version non générique.

113
Michael Graczyk

Je soupçonne fortement que cela est causé par cette optimisation (liée à BeforeFieldInit) dans .NET 4.0.

Si je me souviens bien:

Lorsque vous déclarez explicitement un constructeur statique, beforefieldinit est émis, indiquant au runtime que le constructeur statique doit être exécuté avant tout accès de membre statique.

Ma conjecture:

Je suppose qu'ils ont en quelque sorte foiré ce fait sur le JITer x64, de sorte que lorsque un type différent statique Le membre est accessible à partir d'une classe dont le constructeur statique propre a déjà été exécuté, il fonctionne en quelque sorte saute en cours d'exécution (ou exécute dans le mauvais ordre) le constructeur statique - et provoque donc un crash. (Vous n'obtenez pas d'exception de pointeur nul, probablement car il n'est pas initialisé par null.)

Je n'ai pas exécuté votre code, donc cette partie peut être erronée - mais si je devais faire une autre supposition, je dirais que cela pourrait être quelque chose string.Format (ou Console.WriteLine, qui est similaire) doit accéder en interne à l'origine du crash, comme peut-être une classe associée à locale qui nécessite une construction statique explicite.

Encore une fois, je ne l'ai pas testé, mais c'est ma meilleure estimation des données.

N'hésitez pas à tester mon hypothèse et faites-moi savoir comment ça se passe.

3
Mehrdad

Une observation, mais DotPeek montre la chaîne décompilée. Videz ainsi:

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

Si je déclare mon propre Empty de la même manière, sauf sans l'attribut, je n'obtiens plus le MDA:

class A<T>
{
    static readonly string Empty;

    static A() { }

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