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:
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 :
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.
BeforeFieldInit
) dans .NET 4.0.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.
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.
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);
}
}