Cela m'a dérouté. J'essayais d'optimiser certains tests pour Noda Time, où nous avons une vérification d'initialisation de type. J'ai pensé découvrir si un type avait un initialiseur de type (constructeur statique ou variables statiques avec initialiseurs) avant de tout charger dans un nouveau AppDomain
. À ma grande surprise, un petit test de cela a jeté NullReferenceException
- malgré l'absence de valeurs nulles dans mon code. Il lève uniquement l'exception lorsqu'il est compilé sans informations de débogage.
Voici un programme court mais complet pour illustrer le problème:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
Console.WriteLine("Got initializer? {0}", cctor != null);
}
}
Et une transcription de la compilation et de la sortie:
c:\Users\Jon\Test>csc Test.cs
Microsoft (R) Visual C# Compiler version 4.0.30319.17626
for Microsoft (R) .NET Framework 4.5
Copyright (C) Microsoft Corporation. All rights reserved.
c:\Users\Jon\Test>test
Unhandled Exception: System.NullReferenceException: Object reference not set to
an instance of an object.
at System.RuntimeType.GetConstructorImpl(BindingFlags bindingAttr, Binder bin
der, CallingConventions callConvention, Type[] types, ParameterModifier[] modifi
ers)
at Test.Main()
c:\Users\Jon\Test>csc /debug+ Test.cs
Microsoft (R) Visual C# Compiler version 4.0.30319.17626
for Microsoft (R) .NET Framework 4.5
Copyright (C) Microsoft Corporation. All rights reserved.
c:\Users\Jon\Test>test
Got initializer? True
Vous remarquerez maintenant que j'utilise .NET 4.5 (la version candidate) - ce qui peut être pertinent ici. C'est un peu difficile pour moi de le tester avec les différents autres frameworks originaux (en particulier "vanilla" .NET 4) mais si quelqu'un d'autre a un accès facile aux machines avec d'autres frameworks, je serais intéressé par les résultats.
Autres détails:
- Je suis sur une machine x64, mais ce problème se produit avec les assemblys x86 et x64
- C'est le «débogage» du code appelant qui fait la différence - même si dans le cas de test ci-dessus, il le teste sur son propre assemblage, lorsque j'ai essayé cela contre Noda Time, je n'ai pas eu à recompiler
NodaTime.dll
pour voir les différences - justeTest.cs
qui s'y référait. - L'exécution de l'assemblage "cassé" sur Mono 2.10.8 ne lance pas
Des idées? Bogue du framework?
EDIT: Curieux et curieux. Si vous retirez l' Console.WriteLine
appel:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
}
}
Il échoue désormais uniquement lorsqu'il est compilé avec csc /o- /debug-
. Si vous activez les optimisations, ( /o+
) cela fonctionne. Mais si vous incluez l' Console.WriteLine
appel conformément à l'original, les deux versions échoueront.
NullReferenceException
(qui devrait toujours indiquer un bug), il le fait vraiment regardez douteux. Je soupçonne fortement qu'il s'agit d' un bogue .NET 4.5, j'ai raté la fenêtre pour le corriger ...csc /o+ /debug- Test.cs
échoue aussi pour moi, ce qui est étrange.Réponses:
avec
csc test.cs
:Essayer de charger à partir de
[rsi+8]
quand@rsi
est NULL. Permet d'inspecter la fonction:@rsi
est chargé au début de[rsp+20h]
donc il doit être passé par l'appelant. Regardons l'appelant:(Mon démontage s'affiche
System.Console.get_In
parce que j'ai ajouté unConsole.GetLine()
fichier dans test.cs pour avoir la possibilité de casser le débogueur. J'ai validé que cela ne change pas le comportement).Nous sommes dans cet appel:
000007fe8d45010c 41ff5228 call qword ptr [r10+28h]
(notre adresse ret de trame AV est l'instruction juste après celacall
).Permet de comparer cela avec ce qui se passe lors de la compilation
csc /debug test.cs
. Nous pouvons mettre en place unbp 000007fee5735360
, heureusement le module se charge à la même adresse. Sur l'instruction qui charge@rsi
:Notez que
@rsi
c'est 00000000002debd8. Parcourir la fonction montre que c'est l'adresse qui sera déréférencée plus tard à l'endroit où les mauvaises bombes exe (c'est-à-dire@rsi
ne changent pas). La pile est très intéressante car elle montre un cadre supplémentaire :L'appel est le même
call qword ptr [r10+28h]
que celui que nous avons vu auparavant, donc dans le mauvais cas, cette fonction était probablement intégrée dans leMain()
, donc le fait qu'il y ait un cadre supplémentaire est un hareng rouge. Si nous regardons la préparation de ce quecall qword ptr [r10+28h]
nous remarquons cette instruction:mov qword ptr [rsp+20h],rcx
. C'est ce qui charge l'adresse qui est finalement déréférencée en tant que@rsi
. Dans le bon cas, voici comment@rcx
est chargé:Dans le mauvais cas, cela semble très différent:
C'est très différent. Contrairement au bon cas qui appelle CORINFO_HELP_GETSHARED_GCSTATIC_BASE et lit ce qui finit comme le pointeur critique qui provoque l'AV d'un membre à l'offset
1F0
dans une structure de retour, le code optimisé le charge à partir d'une adresse statique. Et bien sûr, 12721220h contient NULL:Malheureusement, il est trop tard pour moi pour approfondir en ce moment, le démontage de
CORINFO_HELP_GETSHARED_GCSTATIC_BASE
est loin d'être anodin. Je poste ceci dans l'espoir que quelqu'un plus compétent en interne CLR puisse avoir du sens (comme vous pouvez le voir, j'ai vraiment considéré le problème uniquement à partir des instructions natives POV et complètement ignoré IL).la source
Comme je crois avoir trouvé de nouvelles découvertes intéressantes sur le problème, j'ai décidé de les ajouter comme réponse, reconnaissant en même temps qu'ils ne traitent pas du "pourquoi cela se produit" dans la question initiale. Peut-être que quelqu'un qui en sait plus sur le fonctionnement interne des types impliqués peut publier une réponse édifiante basée également sur les observations que je poste.
J'ai également réussi à reproduire le problème sur ma machine et j'ai suivi une connexion avec l' interface System.Runtime.InteropServices._Type , qui est implémentée par la
System.Type
classe.Au départ, j'ai trouvé au moins 3 approches de contournement pour résoudre le problème:
Il suffit de lancer le
Type
à l'_Type
intérieur de laMain
méthode:Ou en vous assurant que l'approche 1 a été utilisée précédemment dans la méthode:
Ou en ajoutant un champ statique à la
Test
classe et en l'initialisant (en le convertissant en_Type
):Plus tard, j'ai découvert que si nous ne voulons pas impliquer l'
System.Runtime.InteropServices._Type
interface dans les solutions de contournement, le problème ne se produit pas non plus par:Ajouter un champ statique à la
Test
classe et l'initialiser (sans le transtyper_Type
):Ou en initialisant la
cctor
variable elle-même en tant que champ statique de la classe:J'attends vos commentaires avec impatience.
la source