【问题标题】:Why would finding a type's initializer throw a NullReferenceException?为什么查找类型的初始化程序会引发 NullReferenceException?
【发布时间】:2012-07-20 13:44:04
【问题描述】:

这让我很困惑。我试图优化 Noda Time 的一些测试,我们有一些类型初始化程序检查。我想在将所有内容加载到新的AppDomain 之前,我会找出类型 是否有 类型初始化程序(静态构造函数或带有初始化程序的静态变量)。令我惊讶的是,对此的一个小测试抛出了NullReferenceException - 尽管 my 代码中没有空值。它只有在没有调试信息的情况下编译时抛出异常。

这里有一个简短但完整的程序来演示这个问题:

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 调用,则两个版本都会失败。

【问题讨论】:

  • 嘿——“尽管我的代码中没有空值”这实际上可能是有记录的 SO 历史上第一次成功播放“错误不在我的代码中”卡.
  • 返回 True 就好了,没有 Debug 从 cmdline 使用 .NET 4 Framework、Visual C# 编译器 4.0.30319.1 进行第一次测试
  • @MarcGravell:是的,虽然我通常很怀疑在这种情况下说“my 代码中没有错误”,当有一个表达式处于危险之中时,并且例外是NullReferenceException(它应该总是表示一个错误)它确实看起来很狡猾。我强烈怀疑如果这个 一个 .NET 4.5 错误,我错过了修复它的窗口...
  • @JonSkeet:我们都知道 MS 的 SP1 是真正的 RTM ;p
  • @leppie:不,csc /o+ /debug- Test.cs 我也失败了,这很奇怪。

标签: .net reflection nullreferenceexception .net-4.5 typeinitializer


【解决方案1】:

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 为NULL 时,尝试从[rsi+8] 加载。让我们检查一下函数:

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,因为我在 test.cs 中添加了一个 Console.GetLine() 以便有机会中断调试器。我验证它不会改变行为)。

我们在这个调用中:000007fe8d45010c 41ff5228 call qword ptr [r10+28h](我们的 AV 帧 ret 地址是紧跟在这个 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)。

【讨论】:

  • 你的调试技能应该得到比这更多的代表。
  • 这是一个优化器错误。 CORINFO* 是一个函数指针,它调用 JIT_GetSharedGCStaticBase。我的猜测是,它被新的 4.5 后台 jit 功能绊倒了,并在初始化之前访问了一个字段,忘记了 jit 类。在 connect.microsoft.com 上报告此问题
  • 不需要。我们已经在找了。你是绝对正确的,发生的事情是因为我们直接分配了一个 RuntimeType 的实例,所以 Type 的 cctor 永远不会被调用,所以 Type.EmptyTypes 保持为空,这就是传递给 GetConstructor 的内容。
  • 有没有一本书可以让我了解这些调试技巧? (最好以“白痴指南”开头或以“傻瓜”结尾)
  • @IgbyLargeman: Advanced Windows Debugging 不错。
【解决方案2】:

我相信我已经找到了一些关于这个问题的有趣的新发现,我决定将它们添加为答案,同时承认它们没有解决“为什么会发生”在原始问题中。也许对所涉及类型的内部运作有更多了解的人可能会根据我发布的观察结果发布一个有启发性的答案。

我还设法在我的机器上重现了该问题,并且我跟踪了与 System.Runtime.InteropServices._Type Interface 的连接,该连接由 System.Type 类实现。

最初,我找到了至少 3 种解决问题的解决方法:

  1. 只需在Main 方法中将Type 转换为_Type

    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;
    

期待您的反馈。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-26
    • 2016-03-27
    • 1970-01-01
    • 1970-01-01
    • 2017-05-18
    • 2013-05-15
    • 2013-11-08
    相关资源
    最近更新 更多