Quelle est la cause de cette FatalExecutionEngineError dans .NET 4.5 beta? [fermé]

150

L'exemple de code ci-dessous s'est produit naturellement. Soudain, mon code a fait une très mauvaise FatalExecutionEngineErrorexception. J'ai passé 30 bonnes minutes à essayer d'isoler et de minimiser l'échantillon coupable. Compilez ceci à l'aide de Visual Studio 2012 en tant qu'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:

Capture d'écran de FatalExecutionException

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 est-ce que j'aboie le mauvais arbre? Changer quoi que ce soit à propos de ce code le fait fonctionner comme vous vous en doutez - par exemple en supprimant le constructeur statique vide de Aou en changeant le paramètre de type de objectà 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 plantée sur l'ordinateur portable. J'ai dû démanteler 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 (Débogage compilé / Tout CPU / 4.0 / VS2010 (pas que l'IDE ne devrait avoir d'importance?)): Http://codepad.org/boZDd98E
  • Pas vu VS 2010 avec 4.0. Pas écraser avec / sans optimisations, différentes CPU cible, débogueur attaché / non attaché, etc. - Tim Medora
  • Crashes en 2010 si j'utilise AnyCPU, ça va dans x86. Crashes dans Visual Studio 2010 SP1, en utilisant Platform Target = AnyCPU, mais bien avec Platform Target = x86. VS2012RC est également installé sur cette machine, donc la version 4.5 peut éventuellement effectuer un remplacement sur place. Utilisez AnyCPU et TargetPlatform = 3.5, cela ne plante pas et 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 pas voir ce MDA lors du ciblage de .NET 4.5. Lors du passage au ciblage .NET 4.0, le MDA est apparu. Ensuite, après le retour à .NET 4.5, le MDA reste. - Wayne
Gleno
la source
Je n'ai jamais su que vous pouviez créer un constructeur statique avec un constructeur public. Heck, je n'ai jamais su que les constructeurs statiques existaient.
Cole Johnson
J'ai une idée: parce que vous changez B d'une classe quelque peu statique à juste une classe avec un Main statique?
Cole Johnson
@ChrisSinclair, je ne pense pas. Je veux dire que j'ai testé ce code sur mon ordinateur portable et j'ai obtenu les mêmes résultats.
Gleno
@ColeJohnson Oui, l'IL correspond à tous sauf à un endroit évident. Il ne semble y avoir aucun bogue ici dans le compilateur c #.
Michael Graczyk
14
Merci à la fois à l'affiche originale pour l'avoir signalé ici, et à Michael pour son excellente analyse. Mes homologues du CLR ont essayé de reproduire le bogue ici et ont découvert qu'il se reproduisait sur la version "Release Candidate" du CLR 64 bits, mais pas sur la version finale "Released To Manufacturing", qui avait un certain nombre de corrections de bogues post- RC. (La version RTM sera disponible au public le 15 août 2012.) Ils pensent donc qu'il s'agit du même problème que celui qui a été signalé ici: connect.microsoft.com/VisualStudio/feedback/details/737108/…
Eric Lippert

Réponses:

114

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

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

METTRE À JOUR

J'ai regardé un peu plus profondément et je pense avoir trouvé la source du problème. Cela semble être causé 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 la modification du compilateur pour .NET 4.5.

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

Le type System.Stringdans 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 manifestement absent:

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

Dans les deux versions, le Stringtype 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 la même manière (afin qu'il ait des champs statiques mais pas de constructeur statique .cctor), mais je ne pouvais pas le faire. Tous ces types ont une .cctorméthode 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é pour qu'il s'initialise automatiquement à String.Emptypartir du code non géré. Cette modification a probablement été effectuée pour .NET 4.0.

Deuxièmement: le compilateur a changé de sorte qu'il n'émette pas de constructeur statique pour string, sachant que String.Empty serait affecté du côté non managé. Cette modification semble avoir été effectuée pour .NET 4.5.

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

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

Original

Tout d'abord, WOW Les gens de la BCL sont devenus très créatifs avec quelques optimisations de performances. Un grand nombre des Stringméthodes sont maintenant effectuées à l' aide d' un cache statique de discussion StringBuilderobjet.

J'ai suivi cette piste pendant un certain temps, mais je ne suis StringBuilderpas utilisé sur le Trimchemin du code, j'ai donc décidé qu'il ne pouvait pas s'agir d'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);
    }
}

Toutefois, si vous décommentez //new A<int>(out s);dans Mainalors le code fonctionne très bien. En fait, si Aest réifié avec n'importe quel type de référence, le programme échoue, mais s'il Aest réifié avec n'importe quel type de valeur, le code n'échoue pas. De plus, si vous mettez en commentaire Ale constructeur statique de, le code n'échoue jamais. Après avoir fouillé dans Trimet Format, il est clair que le problème est qu'il Lengthest en ligne et que dans ces échantillons ci-dessus, le Stringtype n'a pas été initialisé. En particulier, à l'intérieur du corps du Aconstructeur de, string.Emptyn'est pas correctement assigné, bien qu'à l'intérieur du corps de Main,string.Empty est attribué correctement.

Il est étonnant pour moi que l'initialisation de type Stringdépend en quelque sorte de la réification ou non Aavec un type valeur. Ma seule théorie est qu'il existe un chemin de code JIT optimisant 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 rapide coup d'œil sur les autres classes BCL avec des public staticchamps montre que pratiquement toutes implémentent un constructeur statique (même celles avec des constructeurs vides et aucune donnée, comme System.DBNullet System.Empty. Les types de valeur BCL avec des public staticchamps ne semblent pas implémenter un constructeur statique (System.IntPtr par exemple) Cela semble indiquer que le JIT émet des 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.

ÉDITER

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

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

contre

... 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.

Michael Graczyk
la source
3
J'ai cherché des réponses sur un chemin très similaire, mais cela ne semble mener nulle part. Cela semble être un problème de classe de chaînes et, espérons-le, pas un problème plus général. Donc, en ce moment, j'attends que quelqu'un (Eric) avec le code source vienne expliquer ce qui n'a pas fonctionné, et si quelque chose d'autre est effectué. Comme petit avantage, cette discussion a déjà réglé le débat sur l'opportunité d'utiliser string.Emptyou ""... :)
Gleno
L'IL entre eux est-il le même?
Cole Johnson
49
Bonne analyse! Je vais le transmettre à l'équipe de la BCL. Merci!
Eric Lippert
2
@EricLippert et autres: j'ai découvert que le code comme typeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty);donne des résultats différents sur .NET 4.0 par rapport à .NET 4.5. Ce changement est-il lié au changement décrit ci-dessus? Comment .NET 4.5 peut-il techniquement ignorer la modification d'une valeur de champ? Peut-être que je devrais poser une nouvelle question à ce sujet?
Jeppe Stig Nielsen
4
@JeppeStigNielsen: Les réponses à vos questions sont: "peut-être", "assez facilement, apparemment" et "c'est un site de questions-réponses, donc oui, c'est une bonne idée si vous voulez une meilleure réponse à votre question que «peut-être» ».
Eric Lippert
3

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

Si je me souviens bien:

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

Ma conjecture:

Je suppose qu'ils ont en quelque sorte foiré ce fait sur le JITer x64, de sorte que lorsqu'un membre statique d' un type différent est accédé à partir d'une classe dont le propre constructeur statique a déjà exécuté, il saute d'une manière ou d'une autre l' 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 parce qu'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 estimation, je dirais que c'est peut-être quelque chose string.Format(ou Console.WriteLine, ce qui est similaire), qui doit accéder en interne à l'origine du crash, comme peut-être une classe liée à la locale qui a besoin d'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 à me dire comment ça se passe.

user541686
la source
Le bogue se produit toujours lorsque Bn'a pas de constructeur statique, et il ne se produit pas lorsque Aest réifié avec un type valeur. Je pense que c'est un peu plus compliqué.
Michael Graczyk
@MichaelGraczyk: Je pense que je peux l'expliquer (encore une fois, avec des suppositions). Bavoir un constructeur statique n'a pas beaucoup d'importance. Puisqu'il Aa un ctor statique, le runtime perturbe l'ordre dans lequel il est exécuté par rapport à une classe liée aux paramètres régionaux dans un autre espace de noms. Donc, ce champ n'est pas encore initialisé. Cependant, si vous instanciez Aavec un type valeur, il peut s'agir du deuxième passage du runtime via l'instanciation A(le CLR l'a probablement déjà pré-instancié avec un type de référence, en tant qu'optimisation) afin que l'ordre fonctionne lorsqu'il est exécuté une deuxième fois .
user541686
@MichaelGraczyk: Même si ce n'est pas tout à fait l'explication, je pense que je suis assez convaincu que l' beforefieldinitoptimisation donnée est la cause première. Il se peut qu'une partie de l'explication réelle soit différente de ce que j'ai mentionné, mais la cause profonde est probablement la même chose.
user541686
Je me suis penché davantage sur l'IL, et je pense que vous êtes sur quelque chose. Je ne pense pas que l'idée de la deuxième passe sera pertinente ici, car le code échoue toujours si je fais arbitrairement de nombreux appels à A<object>.ctor().
Michael Graczyk
@MichaelGraczyk: C'est bon à entendre, et merci pour ce test. Je ne peux malheureusement pas le reproduire sur mon propre ordinateur portable. (2010 4.0 x64) Pouvez-vous vérifier si cela est effectivement lié au formatage de la chaîne (c'est-à-dire lié aux paramètres régionaux)? Que se passe-t-il si vous supprimez cette partie?
user541686
1

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 le mien de Emptyla 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);
    }
}
lesscode
la source
Et avec cet attribut? Nous avons déjà établi le ""résout.
Henk Holterman
Cet attribut "Performance critique ..." affecte le constructeur d'attribut lui-même, pas les méthodes que l'attribut orne.
Michael Graczyk
C'est interne. Lorsque je définis mon propre attribut identique, cela ne cause toujours pas la MDA. Pas que je m'y attendais - si le JITter recherche cet attribut spécifique, il ne trouvera pas le mien.
lesscode