Почему поиск инициализатора типа вызывает исключение NullReferenceException?

Это поставило меня в тупик. Я пытался оптимизировать некоторые тесты для 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 в соответствии с оригиналом, обе версии выйдут из строя.


person Jon Skeet    schedule 21.07.2012    source источник
comment
Повторная последняя пуля; это была та же самая сборка на моно? Или тот же c #, скомпилированный и запускаемый в моно? Просто интересно, компилятор ли это или время выполнения. Вы, наверное, глянули на Ил, и ничего безумного не выглядит?   -  person Marc Gravell    schedule 21.07.2012
comment
Хех - несмотря на то, что в моем коде нет нулевых значений, это может быть первый раз в записанной истории SO, что ошибка не в моей кодовой карте была успешно воспроизведена.   -  person Marc Gravell    schedule 21.07.2012
comment
@RemusRusanu: О, это интересно. На самом деле, интереснее, чем .NET 3.5 или 2.0.   -  person Jon Skeet    schedule 21.07.2012
comment
Возвращает True, и отладка не выполняет первый тест из cmdline с .NET 4 Framework, компилятором Visual C # 4.0.30319.1   -  person Kerry    schedule 21.07.2012
comment
@MarcGravell: Да, хотя я обычно очень скептически отношусь к словам, что в этом случае в моем коде нет ошибки, когда на кону стоит одно выражение, и исключение составляет NullReferenceException (который должен всегда указывает на ошибку) это действительно выглядит хитроумно. Я сильно подозреваю, что если это ошибка .NET 4.5, я пропустил окно для ее исправления ...   -  person Jon Skeet    schedule 21.07.2012
comment
@JonSkeet: Все мы знаем, что MS SP1 - это настоящая RTM; p   -  person leppie    schedule 21.07.2012
comment
Интересно: csc /o+ test.cs работает. Должно быть какая-то ошибка оптимизатора (отсутствие) ... Однако все еще не знаю, каковы эффекты.   -  person leppie    schedule 21.07.2012
comment
@JonSkeet Только что подтвердил, что получил точные результаты, которые вы получили при использовании 4.0.30319.17626 для Microsoft (R) .NET Framework 4.5 на планшете сборки с VS 2012 RC.   -  person Kerry    schedule 21.07.2012
comment
@JonSkeet: Некоторые дальнейшие результаты. Вылетает только когда csc /o- /debug- test.cs. Любая другая комбинация работает нормально. Я подозреваю ошибку JIT.   -  person leppie    schedule 21.07.2012
comment
@minitech: какие флаги были переданы ilasm?   -  person leppie    schedule 21.07.2012
comment
@leppie: Нет, но добавление /DEBUG работает. Вздох. Что ж, это не ошибка компилятора!   -  person Ry-♦    schedule 21.07.2012
comment
@minitech: Если бы это была ошибка компилятора, PEVerify завершился бы ошибкой. Это не.   -  person leppie    schedule 21.07.2012
comment
@leppie: Нет, csc /o+ /debug- Test.cs и у меня не получается, что странно.   -  person Jon Skeet    schedule 21.07.2012
comment
Пробовал csc /o+ /debug- Test.cs, у меня тоже не получилось.   -  person Bruno Brant    schedule 21.07.2012
comment
Когда вы включаете отслеживание JIT, все работает нормально. [assembly: Debuggable(true, true)]   -  person Mehdi Golchin    schedule 21.07.2012
comment
Да, ошибка воспроизводится только в том случае, если в PE-файле нет отладочной информации. Я обнаружил ошибку против команды CLR, и посмотрим, что они ответят.   -  person Kirill Osenkov    schedule 21.07.2012
comment
Используя csc.exe для компиляции и запуска на .Net 4.5 RC / X64, я получаю то же исключение. Но при компиляции и запуске в Visual Studio 2012 он работает успешно.   -  person softveda    schedule 22.07.2012
comment
@Jon, что в итоге закончилось?   -  person Ian Ringrose    schedule 19.11.2013
comment
@IanRingrose: Я считаю, что это было признано ошибкой и исправлено перед выпуском - или, возможно, в патче. (Сейчас я не могу воспроизвести это.)   -  person Jon Skeet    schedule 19.11.2013


Ответы (2)


с csc test.cs:

(196c.1874): Access violation - code c0000005 (first chance)
mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0xa3:
000007fe`e5735403 488b4608        mov     rax,qword ptr [rsi+8] ds:00000000`00000008=????????????????

Попытка загрузить с [rsi+8], когда @rsi NULL. Давайте проверим функцию:

0:000> ln 000007fe`e5735403
(000007fe`e5735360)   mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0xa3
0:000> uf 000007fe`e5735360
Flow analysis was incomplete, some code may be missing
mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[]):
000007fe`e5735360 53              push    rbx
000007fe`e5735361 55              push    rbp
000007fe`e5735362 56              push    rsi
000007fe`e5735363 57              push    rdi
000007fe`e5735364 4154            push    r12
000007fe`e5735366 4883ec30        sub     rsp,30h
000007fe`e573536a 498bf8          mov     rdi,r8
000007fe`e573536d 8bea            mov     ebp,edx
000007fe`e573536f 48c744242800000000 mov   qword ptr [rsp+28h],0
000007fe`e5735378 488bb42480000000 mov     rsi,qword ptr [rsp+80h]
000007fe`e5735380 4889742420      mov     qword ptr [rsp+20h],rsi
000007fe`e5735385 41b903000000    mov     r9d,3
...    
mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0x97:
000007fe`e57353f7 488b4b08        mov     rcx,qword ptr [rbx+8]
000007fe`e57353fb 85c9            test    ecx,ecx
000007fe`e57353fd 0f848e000000    je      mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0x131 (000007fe`e5735491)

mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0xa3:
000007fe`e5735403 488b4608        mov     rax,qword ptr [rsi+8]
000007fe`e5735407 85c0            test    eax,eax
000007fe`e5735409 7545            jne     mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0xf0 (000007fe`e5735450)
...

@rsi загружается в начале с [rsp+20h], поэтому его должен передать вызывающий. Посмотрим на звонящего:

0:000> k3
Child-SP          RetAddr           Call Site
00000000`001fec70 000007fe`8d450110 mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0xa3
00000000`001fecd0 000007fe`ecb6e073 image00000000_01120000!Test.Main()+0x60
00000000`001fed20 000007fe`ecb6dcb2 clr!CoUninitializeEE+0x7ae1f
0:000> ln 000007fe`8d450110
(000007fe`8d4500b0)   image00000000_01120000!Test.Main()+0x60
0:000> uf 000007fe`8d4500b0
image00000000_01120000!Test.Main():
000007fe`8d4500b0 53              push    rbx
000007fe`8d4500b1 4883ec40        sub     rsp,40h
000007fe`8d4500b5 e8a69ba658      call    mscorlib_ni!System.Console.get_In() (000007fe`e5eb9c60)
000007fe`8d4500ba 4c8bd8          mov     r11,rax
000007fe`8d4500bd 498b03          mov     rax,qword ptr [r11]
000007fe`8d4500c0 488b5048        mov     rdx,qword ptr [rax+48h]
000007fe`8d4500c4 498bcb          mov     rcx,r11
000007fe`8d4500c7 ff5238          call    qword ptr [rdx+38h]
000007fe`8d4500ca 488d0d7737eeff  lea     rcx,[000007fe`8d333848]
000007fe`8d4500d1 e88acb715f      call    clr!CoUninitializeEE+0x79a0c (000007fe`ecb6cc60)
000007fe`8d4500d6 4c8bd8          mov     r11,rax
000007fe`8d4500d9 48b92012531200000000 mov rcx,12531220h
000007fe`8d4500e3 488b09          mov     rcx,qword ptr [rcx]
000007fe`8d4500e6 498b03          mov     rax,qword ptr [r11]
000007fe`8d4500e9 4c8b5068        mov     r10,qword ptr [rax+68h]
000007fe`8d4500ed 48c744242800000000 mov   qword ptr [rsp+28h],0
000007fe`8d4500f6 48894c2420      mov     qword ptr [rsp+20h],rcx
000007fe`8d4500fb 41b903000000    mov     r9d,3
000007fe`8d450101 4533c0          xor     r8d,r8d
000007fe`8d450104 ba38000000      mov     edx,38h
000007fe`8d450109 498bcb          mov     rcx,r11
000007fe`8d45010c 41ff5228        call    qword ptr [r10+28h]
000007fe`8d450110 48bb1032531200000000 mov rbx,12533210h
000007fe`8d45011a 488b1b          mov     rbx,qword ptr [rbx]
000007fe`8d45011d 33d2            xor     edx,edx
000007fe`8d45011f 488bc8          mov     rcx,rax
000007fe`8d450122 e829452e58      call    mscorlib_ni!System.Reflection.ConstructorInfo.op_Equality(System.Reflection.ConstructorInfo, System.Reflection.ConstructorInfo) (000007fe`e5734650)
000007fe`8d450127 0fb6c8          movzx   ecx,al
000007fe`8d45012a 33c0            xor     eax,eax
000007fe`8d45012c 85c9            test    ecx,ecx
000007fe`8d45012e 0f94c0          sete    al
000007fe`8d450131 0fb6c8          movzx   ecx,al
000007fe`8d450134 894c2430        mov     dword ptr [rsp+30h],ecx
000007fe`8d450138 488d542430      lea     rdx,[rsp+30h]
000007fe`8d45013d 488d0d24224958  lea     rcx,[mscorlib_ni+0x682368 (000007fe`e58e2368)]
000007fe`8d450144 e807246a5f      call    clr+0x2550 (000007fe`ecaf2550)
000007fe`8d450149 488bd0          mov     rdx,rax
000007fe`8d45014c 488bcb          mov     rcx,rbx
000007fe`8d45014f e81cab2758      call    mscorlib_ni!System.Console.WriteLine(System.String, System.Object) (000007fe`e56cac70)
000007fe`8d450154 90              nop
000007fe`8d450155 4883c440        add     rsp,40h
000007fe`8d450159 5b              pop     rbx
000007fe`8d45015a c3              ret

(Мой дизассемблер показывает System.Console.get_In, потому что я добавил Console.GetLine() в test.cs, чтобы иметь возможность сломать отладчик. Я подтвердил, что это не меняет поведения).

Мы находимся в этом вызове: 000007fe8d45010c 41ff5228 call qword ptr [r10+28h] (наш адрес возврата AV-кадра - это инструкция сразу после этого call).

Давайте сравним это с тем, что происходит, когда мы компилируем csc /debug test.cs. Мы можем установить bp 000007fee5735360, к счастью, модуль загружается по тому же адресу. По инструкции, которая загружает @rsi:

0:000> r
rax=000007fee58e2f30 rbx=00000000027c6258 rcx=00000000027c6258
rdx=0000000000000038 rsi=00000000002debd8 rdi=0000000000000000
rip=000007fee5735378 rsp=00000000002de990 rbp=0000000000000038
 r8=0000000000000000  r9=0000000000000003 r10=000007fee58831c8
r11=00000000002de9c0 r12=0000000000000000 r13=00000000002dedc0
r14=00000000002dec58 r15=0000000000000004
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0x18:
000007fe`e5735378 488bb42480000000 mov     rsi,qword ptr [rsp+80h] ss:00000000`002dea10=a0627c0200000000

Обратите внимание, что @rsi - это 00000000002debd8. Пошаговое выполнение функции показывает, что это адрес, который будет разыменован позже в том месте, где сбой плохой exe (т.е. @rsi не изменится). Стек очень интересен тем, что показывает лишний фрейм:

0:000> k3
Child-SP          RetAddr           Call Site
00000000`002de990 000007fe`e5eddf68 mscorlib_ni!System.RuntimeType.GetConstructorImpl(System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[])+0x18
00000000`002de9f0 000007fe`8d460119 mscorlib_ni!System.Type.get_TypeInitializer()+0x48
00000000`002dea30 000007fe`ecb6e073 good!Test.Main()+0x49*** WARNING: Unable to verify checksum for good.exe

0:000> ln 000007fe`e5eddf68
(000007fe`e5eddf20)   mscorlib_ni!System.Type.get_TypeInitializer()+0x48
0:000> uf 000007fe`e5eddf20
mscorlib_ni!System.Type.get_TypeInitializer():
000007fe`e5eddf20 53              push    rbx
000007fe`e5eddf21 4883ec30        sub     rsp,30h
000007fe`e5eddf25 488bd9          mov     rbx,rcx
000007fe`e5eddf28 ba22010000      mov     edx,122h
000007fe`e5eddf2d b901000000      mov     ecx,1
000007fe`e5eddf32 e8d1a075ff      call    CORINFO_HELP_GETSHARED_GCSTATIC_BASE (000007fe`e5638008)
000007fe`e5eddf37 488b88f0010000  mov     rcx,qword ptr [rax+1F0h]
000007fe`e5eddf3e 488b03          mov     rax,qword ptr [rbx]
000007fe`e5eddf41 4c8b5068        mov     r10,qword ptr [rax+68h]
000007fe`e5eddf45 48c744242800000000 mov   qword ptr [rsp+28h],0
000007fe`e5eddf4e 48894c2420      mov     qword ptr [rsp+20h],rcx
000007fe`e5eddf53 41b903000000    mov     r9d,3
000007fe`e5eddf59 4533c0          xor     r8d,r8d
000007fe`e5eddf5c ba38000000      mov     edx,38h
000007fe`e5eddf61 488bcb          mov     rcx,rbx
000007fe`e5eddf64 41ff5228        call    qword ptr [r10+28h]
000007fe`e5eddf68 90              nop
000007fe`e5eddf69 4883c430        add     rsp,30h
000007fe`e5eddf6d 5b              pop     rbx
000007fe`e5eddf6e c3              ret
0:000> ln 000007fe`8d460119

Вызов тот же call qword ptr [r10+28h], который мы видели раньше, поэтому в плохом случае эта функция, вероятно, была встроена в Main(), так что наличие лишнего кадра - отвлекающий маневр. Если мы посмотрим на подготовку этого call qword ptr [r10+28h], мы заметим эту инструкцию: mov qword ptr [rsp+20h],rcx. Это то, что загружает адрес, который в конечном итоге разыменовывается как @rsi. В хорошем случае так загружается @rcx:

000007fe`e5eddf32 e8d1a075ff      call    CORINFO_HELP_GETSHARED_GCSTATIC_BASE (000007fe`e5638008)
000007fe`e5eddf37 488b88f0010000  mov     rcx,qword ptr [rax+1F0h]

В плохом случае это выглядит совсем иначе:

000007fe`8d4600d9 48b92012721200000000 mov rcx,12721220h
000007fe`8d4600e3 488b09          mov     rcx,qword ptr [rcx]

Это совсем другое. В отличие от хорошего случая, который вызывает CORINFO_HELP_GETSHARED_GCSTATIC_BASE и считывает то, что заканчивается как критический указатель, который вызывает AV от некоторого члена со смещением 1F0 в структуре возврата, оптимизированный код загружает его со статического адреса. И, конечно же, 12721220h содержит NULL:

0:000> dp 12721220h L8
00000000`12721220  00000000`00000000 00000000`00000000
00000000`12721230  00000000`00000000 00000000`02722198
00000000`12721240  00000000`027221c8 00000000`027221f8
00000000`12721250  00000000`02722228 00000000`02722258

К сожалению, сейчас уже слишком поздно, чтобы копать глубже, разборка CORINFO_HELP_GETSHARED_GCSTATIC_BASE далеко не тривиальна. Я публикую это в надежде, что кто-то более осведомленный во внутреннем устройстве CLR может иметь смысл (как вы можете видеть, я действительно рассматривал проблему только из собственных инструкций POV и полностью игнорировал IL).

person Remus Rusanu    schedule 21.07.2012
comment
Вы заслуживаете гораздо большего количества представителей, чем это, за свои навыки отладки. - person JSBձոգչ; 22.07.2012
comment
Это ошибка оптимизатора. CORINFO * - указатель на функцию, он вызывает JIT_GetSharedGCStaticBase. Я предполагаю, что он отключился из-за новой функции фонового jit 4.5 и обращается к полю до его инициализации, забывая jit класса. Сообщите об этом на connect.microsoft.com - person Hans Passant; 22.07.2012
comment
Нет надобности. Мы уже ищем. Вы абсолютно правы, происходит то, что, поскольку мы напрямую выделяем экземпляр RuntimeType, cctor Type никогда не вызывается, поэтому Type.EmptyTypes остается нулевым, и это то, что передается GetConstructor. - person Kirill Osenkov; 25.07.2012
comment
Хех, я думаю, что я тоже могу столкнуться с этой ошибкой, за исключением того, что моя программа полностью контролирует нарушение прав доступа. Мои грубые попытки проанализировать собственный JIT-код ни к чему не привели, а проект большой, и попытка зафиксировать ошибку в меньшем тестовом примере оказалась невозможной :( - person Earlz; 08.09.2012
comment
Фактически, эта программа, похоже, также вызывает нарушение прав доступа. И я использую официальную версию .Net 4.5. - person Earlz; 08.09.2012
comment
@Earlz: задайте отдельный вопрос и опишите проблему там, а не в комментариях. К вашему сведению, управляемое исключение NullReferenceException - это как раз то, как собственный AV преобразуется в управляемое исключение. Оборудование работает родным образом. - person Remus Rusanu; 08.09.2012
comment
Есть ли книга, которую я могу прочитать, чтобы получить эти навыки отладки? (Желательно начинать с «Руководства по идиотам» или заканчивать «Для чайников») - person Igby Largeman; 19.04.2013
comment
@IgbyLargeman: Расширенная отладка Windows довольно хороша. - person Remus Rusanu; 19.04.2013
comment
@RemusRusanu: спасибо. Есть ли у вас опыт работы с его аналогом, Расширенная отладка .Net < / а>? - person Igby Largeman; 19.04.2013

Поскольку я считаю, что нашел несколько новых интересных выводов о проблеме, я решил добавить их в качестве ответа, в то же время признав, что они не рассматривают «почему это происходит» в исходном вопрос. Может быть, кто-то, кто знает больше о внутренней работе задействованных типов, может опубликовать назидательный ответ, основанный также на моих наблюдениях.

Мне также удалось воспроизвести проблему на моем компьютере, и я отследил соединение с System.Runtime.InteropServices._Type Interface, который реализуется классом System.Type.

Изначально я нашел как минимум 3 обходных пути для решения проблемы:

  1. Просто преобразовав Type в _Type внутри метода Main:

    var cctor = ((_Type)typeof(Test)).TypeInitializer;
    
  2. Или убедитесь, что подход 1 использовался ранее внутри метода:

    var warmUp = ((_Type)typeof(Test)).TypeInitializer; 
    var cctor = ((Type)typeof(Test)).TypeInitializer;
    
  3. Или добавив статическое поле к классу Test и инициализировав его (с приведением к _Type):

    static ConstructorInfo _dummy1 = (typeof(object) as _Type).TypeInitializer;
    

Позже я обнаружил, что если мы не хотим задействовать интерфейс System.Runtime.InteropServices._Type в обходных решениях, проблема не возникает ни из-за того, что:

  1. Добавление статического поля в класс Test и его инициализация (без преобразования в _Type):

    static ConstructorInfo _dummy2 = typeof(object).TypeInitializer;
    
  2. Или инициализируя саму переменную cctor как статическое поле класса:

    static ConstructorInfo cctor = typeof(Test).TypeInitializer;
    

Жду ваших отзывов.

person Alex Filipovici    schedule 16.05.2013