Это поставило меня в тупик. Я пытался оптимизировать некоторые тесты для Noda Time, где у нас есть проверка инициализатора типа. Я думал, что выясню, есть ли у типа инициализатор типа (статический конструктор или статические переменные с инициализаторами), прежде чем загружать все в новый AppDomain
. К моему удивлению, небольшая проверка этого бросила NullReferenceException
- несмотря на то, что в моем коде не было нулевых значений . Это только бросает исключение при компиляции без информации отладки.
Вот короткая, но полная программа для демонстрации проблемы:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
Console.WriteLine("Got initializer? {0}", cctor != null);
}
}
И стенограмма компиляции и вывода:
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
Теперь вы заметите, что я использую .NET 4.5 (релиз-кандидат), что может быть здесь актуально. Для меня довольно сложно протестировать его с различными другими исходными фреймворками (в частности, "vanilla" .NET 4), но если у кого-то еще есть легкий доступ к машинам с другими фреймворками, я был бы заинтересован в результатах.
Другие детали:
- Я нахожусь на машине x64, но эта проблема возникает как со сборками x86, так и x64
- Различие имеет "отлаженность" вызывающего кода - хотя в приведенном выше тестовом примере он тестирует его на собственной сборке, когда я пробовал это с Noda Time, мне не пришлось перекомпилировать,
NodaTime.dll
чтобы увидеть различия - толькоTest.cs
что ссылался на это. - Запуск "сломанной" сборки на Mono 2.10.8 не бросает
Любые идеи? Ошибка фреймворка?
РЕДАКТИРОВАТЬ: Все любопытнее и любопытнее. Если снять Console.WriteLine
звонок:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
}
}
Теперь он не работает только при компиляции с csc /o- /debug-
. Если включить оптимизацию, ( /o+
) работает. Но если вы включите Console.WriteLine
вызов согласно оригиналу, обе версии выйдут из строя.
NullReferenceException
(что всегда должно указывать на ошибку), это действительно так. выглядят хитроумно. Я сильно подозреваю , если это ошибка .NET , 4.5, я пропустил окно для получения ее исправить ...csc /o+ /debug- Test.cs
у меня тоже не получается, что странно.Ответы:
с
csc test.cs
:Попытка загрузить,
[rsi+8]
когда@rsi
NULL. Давайте проверим функцию:@rsi
загружается в начале с,[rsp+20h]
поэтому его должен передать вызывающий. Посмотрим на звонящего:(Мой дизассемблер показывает,
System.Console.get_In
потому что я добавилConsole.GetLine()
в test.cs, чтобы иметь возможность сломать отладчик. Я подтвердил, что это не меняет поведения).Мы находимся в этом вызове:
000007fe8d45010c 41ff5228 call qword ptr [r10+28h]
(наш адрес возврата AV-кадра - это инструкция сразу после этогоcall
).Давайте сравним это с тем, что происходит при компиляции
csc /debug test.cs
. Мы можем настроитьbp 000007fee5735360
, к счастью, модуль загружается по тому же адресу. По инструкции, которая загружает@rsi
:Обратите внимание, что
@rsi
это 00000000002debd8. Пошаговое выполнение функции показывает, что это адрес, который будет разыменован позже в том месте, где сбой плохой exe (т.е.@rsi
не изменится). Стек очень интересен тем, что показывает лишний кадр :Вызов тот же,
call qword ptr [r10+28h]
что мы видели раньше, поэтому в плохом случае эта функция, вероятно, была встроена вMain()
, так что наличие лишнего фрейма - отвлекающий маневр. Если мы посмотрим на подготовку этогоcall qword ptr [r10+28h]
мы замечаем эту инструкцию:mov qword ptr [rsp+20h],rcx
. Это то, что загружает адрес, который в конечном итоге разыменовывается как@rsi
. В хорошем случае так@rcx
загружается:В плохом случае это выглядит совсем иначе:
Это совсем другое. В отличие от хорошего случая, который вызывает CORINFO_HELP_GETSHARED_GCSTATIC_BASE и считывает то, что оказывается критическим указателем, вызывающим AV из некоторого члена со смещением
1F0
в структуре возврата, оптимизированный код загружает его со статического адреса. И, конечно же, 12721220h содержит NULL:К сожалению, сейчас уже поздно копать глубже, разборка
CORINFO_HELP_GETSHARED_GCSTATIC_BASE
далеко не тривиальна. Я публикую это в надежде, что кто-то более осведомленный во внутреннем устройстве CLR может иметь смысл (как вы можете видеть, я действительно рассматривал проблему только из собственных инструкций POV и полностью игнорировал IL).источник
Поскольку я считаю, что нашел несколько новых интересных выводов о проблеме, я решил добавить их в качестве ответа, в то же время признав, что они не рассматривают «почему это происходит» в исходном вопросе. Может быть, кто-то, кто знает больше о внутренней работе задействованных типов, может опубликовать назидательный ответ, основанный также на моих наблюдениях.
Мне также удалось воспроизвести проблему на моем компьютере, и я отследил соединение с интерфейсом System.Runtime.InteropServices._Type , который реализуется
System.Type
классом.Изначально я нашел как минимум 3 обходных подхода для решения проблемы:
Просто отливку ,
Type
чтобы_Type
внутриMain
метода:var cctor = ((_Type)typeof(Test)).TypeInitializer;
Или убедитесь, что подход 1 использовался ранее внутри метода:
var warmUp = ((_Type)typeof(Test)).TypeInitializer; var cctor = ((Type)typeof(Test)).TypeInitializer;
Или добавив статическое поле к
Test
классу и инициализировав его (с приведением к_Type
):static ConstructorInfo _dummy1 = (typeof(object) as _Type).TypeInitializer;
Позже я обнаружил, что если мы не хотим задействовать
System.Runtime.InteropServices._Type
интерфейс в обходных путях, проблема не возникает:Добавление статического поля в
Test
класс и его инициализация (без приведения к нему_Type
):static ConstructorInfo _dummy2 = typeof(object).TypeInitializer;
Или инициализируя
cctor
саму переменную как статическое поле класса:static ConstructorInfo cctor = typeof(Test).TypeInitializer;
Жду ваших отзывов.
источник