To mnie zaskoczyło. Próbowałem zoptymalizować niektóre testy dla Noda Time, gdzie mamy pewne sprawdzanie inicjatora typu. Pomyślałem, że dowiem się, czy typ ma inicjator typu (konstruktor statyczny lub zmienne statyczne z inicjalizatorami) przed załadowaniem wszystkiego do nowego AppDomain
. Ku mojemu zdziwieniu rzucił to niewielki test NullReferenceException
- pomimo tego, że w moim kodzie nie ma żadnych wartości zerowych . To tylko zgłasza wyjątek, gdy skompilowany bez informacji debugowania.
Oto krótki, ale kompletny program pokazujący problem:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
Console.WriteLine("Got initializer? {0}", cctor != null);
}
}
I zapis kompilacji i wyjścia:
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
Teraz zauważysz, że używam .NET 4.5 (kandydata do wydania) - co może mieć znaczenie tutaj. Testowanie go z różnymi innymi oryginalnymi frameworkami (w szczególności „waniliowym” .NET 4) jest dla mnie nieco trudne, ale jeśli ktokolwiek inny ma łatwy dostęp do maszyn z innymi frameworkami, jestem zainteresowany wynikami.
Inne szczegóły:
- Korzystam z komputera x64, ale ten problem występuje zarówno w zestawach x86, jak i x64
- To „debugowanie” kodu wywołującego robi różnicę - chociaż w powyższym przypadku testowym testuje się go na swoim własnym zestawie, kiedy wypróbowałem to w stosunku do Noda Time, nie musiałem rekompilować,
NodaTime.dll
aby zobaczyć różnice - właśnieTest.cs
o tym mowa. - Uruchomienie „zepsutego” zestawu na Mono 2.10.8 nie rzuca
Jakieś pomysły? Błąd ramowy?
EDYCJA: Curiouser i curiouser. Jeśli wyjmiesz Console.WriteLine
połączenie:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
}
}
Teraz kończy się niepowodzeniem tylko po kompilacji csc /o- /debug-
. Jeśli włączysz optymalizacje, ( /o+
) to zadziała. Ale jeśli dołączysz Console.WriteLine
połączenie zgodnie z oryginałem, obie wersje zakończą się niepowodzeniem.
NullReferenceException
(co zawsze powinno wskazywać na błąd), tak naprawdę wyglądać podejrzanie. Podejrzewam, że jest to błąd .NET 4.5, przegapiłem okno, w którym można to naprawić ...csc /o+ /debug- Test.cs
też mi się nie udaje, co jest dziwne.Odpowiedzi:
z
csc test.cs
:Próbuję załadować,
[rsi+8]
kiedy@rsi
jest NULL. Pozwala sprawdzić funkcję:@rsi
jest ładowany na początku od,[rsp+20h]
więc musi zostać przekazany przez dzwoniącego. Spójrzmy na rozmówcę:(Mój demontaż pokazuje,
System.Console.get_In
ponieważ dodałem plikConsole.GetLine()
in test.cs, aby mieć możliwość włamania się do debuggera. Zweryfikowałem, że to nie zmienia zachowania).Jesteśmy w tej rozmowie:
000007fe8d45010c 41ff5228 call qword ptr [r10+28h]
(nasz adres retencji ramki AV jest instrukcją zaraz po tymcall
).Porównajmy to z tym, co dzieje się podczas kompilacji
csc /debug test.cs
. Możemy skonfigurowaćbp 000007fee5735360
, na szczęście moduł ładuje się pod tym samym adresem. Zgodnie z instrukcją, która się ładuje@rsi
:Pamiętaj, że
@rsi
jest to 00000000002debd8. Przejście przez funkcję pokazuje, że jest to adres, który zostanie odwołany później w miejscu, w którym zły eks bombarduje (tj.@rsi
Nie zmienia się). Stos jest bardzo interesujący, ponieważ pokazuje dodatkową ramkę :Wywołanie jest takie samo
call qword ptr [r10+28h]
, jak widzieliśmy wcześniej, więc w złym przypadku funkcja ta została prawdopodobnie wbudowana wMain()
, więc fakt, że istnieje dodatkowa ramka, jest czerwonym śledziem. Jeśli spojrzymy na przygotowania tocall qword ptr [r10+28h]
zauważamy tej instrukcji:mov qword ptr [rsp+20h],rcx
. To właśnie ładuje adres, który ostatecznie zostaje wyrejestrowany jako@rsi
. W dobrym przypadku jest@rcx
to ładowane w następujący sposób:W złym przypadku wygląda to zupełnie inaczej:
To jest bardzo różne. W przeciwieństwie do dobrego przypadku, który wywołuje CORINFO_HELP_GETSHARED_GCSTATIC_BASE i czyta to, co kończy się jako krytyczny wskaźnik, który powoduje, że AV z jakiegoś elementu jest przesunięty
1F0
w strukturze zwrotnej, zoptymalizowany kod ładuje go ze statycznego adresu. I oczywiście 12721220h zawiera NULL:Niestety, jest już za późno, by kopać głębiej, dezasemblacja
CORINFO_HELP_GETSHARED_GCSTATIC_BASE
wcale nie jest trywialna. Zamieszczam to w nadziei, że ktoś bardziej zaznajomiony z wewnętrznymi elementami CLR może mieć sens (jak widać, naprawdę wziąłem ten problem tylko z natywnych instrukcji POV i całkowicie zignorowałem IL).źródło
Ponieważ uważam, że znalazłem kilka nowych interesujących ustaleń dotyczących problemu, postanowiłem dodać je jako odpowiedź, jednocześnie uznając, że nie zajmują się one „dlaczego tak się dzieje” w pierwotnym pytaniu. Być może ktoś, kto wie więcej na temat wewnętrznych działań zaangażowanych typów, może opublikować budującą odpowiedź na podstawie również moich obserwacji.
Udało mi się również odtworzyć problem na moim komputerze i śledziłem połączenie z interfejsem System.Runtime.InteropServices._Type , który jest implementowany przez
System.Type
klasę.Początkowo znalazłem co najmniej 3 sposoby obejścia problemu w celu rozwiązania problemu:
Po prostu przez odlewanie
Type
do_Type
wewnątrzMain
metody:Lub upewnienie się, że wcześniej zastosowano metodę 1 w metodzie:
Lub dodając pole statyczne do
Test
klasy i inicjując je (z rzutowaniem na_Type
):Później odkryłem, że jeśli nie chcemy angażować
System.Runtime.InteropServices._Type
interfejsu w obejścia, problem nie występuje również przez:Dodanie pola statycznego do
Test
klasy i zainicjowanie go (bez rzutowania go_Type
):Lub poprzez zainicjowanie
cctor
samej zmiennej jako pola statycznego klasy:Czekam na Wasze opinie.
źródło