Tại sao việc tìm kiếm trình khởi tạo của một loại sẽ ném NullReferenceException?


194

Điều này đã làm tôi bối rối. Tôi đã cố gắng tối ưu hóa một số thử nghiệm cho Noda Time, nơi chúng tôi có một số loại kiểm tra khởi tạo. Tôi nghĩ rằng tôi sẽ tìm hiểu xem một loại bộ khởi tạo kiểu (hàm tạo tĩnh hay biến tĩnh với bộ khởi tạo) trước khi tải mọi thứ vào một kiểu mới AppDomain. Trước sự ngạc nhiên của tôi, một thử nghiệm nhỏ về điều này đã ném NullReferenceException- mặc dù không có giá trị null trong mã của tôi . Nó chỉ ném ngoại lệ khi được biên dịch mà không có thông tin gỡ lỗi.

Đây là một chương trình ngắn nhưng đầy đủ để chứng minh vấn đề:

using System;

class Test
{
    static Test() {}

    static void Main()
    {
        var cctor = typeof(Test).TypeInitializer;
        Console.WriteLine("Got initializer? {0}", cctor != null);
    }    
}

Và một bảng điểm tổng hợp và đầu ra:

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

Bây giờ bạn sẽ nhận thấy tôi đang sử dụng .NET 4.5 (ứng cử viên phát hành) - có thể có liên quan ở đây. Việc thử nghiệm nó với các khung ban đầu khác nhau (cụ thể là "vanilla" .NET 4) hơi khó khăn nhưng nếu bất kỳ ai khác có quyền truy cập dễ dàng vào các máy có các khung khác, tôi sẽ quan tâm đến kết quả.

Những chi tiết khác:

  • Tôi đang sử dụng máy x64, nhưng sự cố này xảy ra với cả hai cụm x86 và x64
  • Đó là "gỡ lỗi" của mã cuộc gọi tạo ra sự khác biệt - mặc dù trong trường hợp thử nghiệm ở trên nó đang thử nghiệm nó trên chính nó, khi tôi thử nó với Noda Time, tôi không phải biên dịch lại NodaTime.dllđể thấy sự khác biệt - chỉ cần Test.csđề cập đến nó.
  • Chạy lắp ráp "bị hỏng" trên Mono 2.10.8 không ném

Có ý kiến ​​gì không? Lỗi khung?

EDIT: Tò mò và tò mò hơn. Nếu bạn nhận Console.WriteLinecuộc gọi:

using System;

class Test
{
    static Test() {}

    static void Main()
    {
        var cctor = typeof(Test).TypeInitializer;
    }    
}

Nó bây giờ chỉ thất bại khi biên dịch với csc /o- /debug-. Nếu bạn bật tối ưu hóa, ( /o+) nó hoạt động. Nhưng nếu bạn bao gồm Console.WriteLinecuộc gọi theo bản gốc, cả hai phiên bản sẽ thất bại.


92
Heh - "mặc dù không có giá trị null trong mã của tôi" nhưng đây thực sự có thể là lần đầu tiên trong lịch sử SO được ghi lại rằng thẻ "lỗi không có trong mã của tôi" đã được phát thành công.
Marc Gravell

1
Trả về Đúng chỉ tốt khi không có Debug thực hiện thử nghiệm đầu tiên từ cmdline với .NET 4 Framework, trình biên dịch Visual C # 4.0.30319.1
Kerry

2
@MarcGravell: Vâng, trong khi tôi thường rất nghi ngờ khi nói rằng "Không có lỗi trong mã của tôi " trong trường hợp này, khi có một biểu thức duy nhất bị đe dọa, và ngoại lệ là một NullReferenceException( luôn luôn chỉ ra một lỗi) nó thực sự xảy ra nhìn tinh ranh. Tôi cực kỳ nghi ngờ nếu đây lỗi .NET 4.5, tôi đã bỏ lỡ cửa sổ để sửa nó ...
Jon Skeet

15
@JonSkeet: Chúng ta đều biết SP1 của MS là RTM thực sự; p
leppie

1
@leppie: Không, csc /o+ /debug- Test.cstôi cũng thất bại, điều này thật kỳ quặc.
Jon Skeet

Câu trả lời:


284

với 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=????????????????

Cố gắng tải từ [rsi+8]khi nào @rsilà NULL. Cho phép kiểm tra chức năng:

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được tải ngay từ đầu [rsp+20h]để nó phải được người gọi thông qua. Hãy nhìn vào người gọi:

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

(Việc phân tách của tôi hiển thị System.Console.get_Invì tôi đã thêm một Console.GetLine()test.cs để có cơ hội phá vỡ trình gỡ lỗi. Tôi xác nhận nó không thay đổi hành vi).

Chúng tôi đang thực hiện cuộc gọi này: 000007fe8d45010c 41ff5228 call qword ptr [r10+28h](địa chỉ giữ lại khung AV của chúng tôi là hướng dẫn ngay sau này call).

Hãy so sánh điều này với những gì xảy ra khi chúng tôi biên dịch csc /debug test.cs. Chúng ta có thể thiết lập một bp 000007fee5735360, may mắn là các mô-đun tải tại cùng một địa chỉ. Trên hướng dẫn tải @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

Lưu ý rằng đó @rsilà 00000000002debd8. Bước qua chức năng cho thấy đây là địa chỉ sẽ được hủy đăng ký sau đó tại địa điểm khi bom exe xấu (tức là @rsikhông thay đổi). Ngăn xếp rất thú vị vì nó hiển thị thêm khung :

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

Cuộc gọi này giống như cuộc gọi call qword ptr [r10+28h]mà chúng ta đã thấy trước đây, vì vậy trong trường hợp xấu, chức năng này có thể được đặt trong Main(), vì vậy thực tế là có thêm một khung là cá trích đỏ. Nếu chúng ta nhìn vào sự chuẩn bị này, call qword ptr [r10+28h]chúng ta sẽ thấy hướng dẫn này : mov qword ptr [rsp+20h],rcx. Đây là những gì tải địa chỉ mà cuối cùng được quy định là @rsi. Trong trường hợp tốt, đây là cách @rcxtải:

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

Trong trường hợp xấu, nó trông rất khác:

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

Điều này rất khác nhau. Không giống như trường hợp tốt gọi CORINFO_HELP_GETSHARED_GCSTATIC_BASE và đọc những gì kết thúc là con trỏ quan trọng khiến AV từ một thành viên nào đó bị lệch 1F0trong cấu trúc trả về, mã được tối ưu hóa sẽ tải nó từ một địa chỉ tĩnh. Và tất nhiên 12721220h chứa 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

Thật không may là quá muộn để tôi đào sâu hơn ngay bây giờ, sự phân tán của CORINFO_HELP_GETSHARED_GCSTATIC_BASEnó là xa tầm thường. Tôi đang đăng bài này với hy vọng ai đó hiểu biết hơn về nội bộ CLR có thể có ý nghĩa (như bạn có thể thấy, tôi thực sự đã xem xét vấn đề chỉ từ hướng dẫn bản địa POV và hoàn toàn bỏ qua IL).


46
Bạn xứng đáng có nhiều đại diện hơn thế này cho kỹ năng sửa lỗi của bạn.
JSB

23
Đây là một lỗi tối ưu hóa. CORINFO * là một con trỏ hàm, nó gọi JIT_GetSharedGCStaticBase. Tôi đoán là nó đã tăng gấp ba tính năng jit nền 4,5 mới và truy cập vào một trường trước khi nó được khởi tạo, quên đi jit lớp. Báo cáo điều này tại connect.microsoft.com
Hans Passant

28
Không cần. Chúng tôi đang tìm kiếm. Bạn hoàn toàn đúng, điều xảy ra là bởi vì chúng tôi phân bổ trực tiếp một thể hiện của RuntimeType, cctor của Type không bao giờ được gọi, vì vậy Type.EmptyTypes vẫn là null và đó là những gì được truyền cho GetConstructor.
Kirill Osenkov

3
Có cuốn sách nào tôi có thể đọc để có được những kỹ năng sửa lỗi này không? (Tốt nhất là bắt đầu bằng "Hướng dẫn ngu ngốc" hoặc kết thúc bằng "cho người giả")
Igby Largeeman

1
@IgbyLargeman: Gỡ lỗi Windows nâng cao khá tốt.
Remus Rusanu

10

Vì tôi tin rằng tôi đã tìm thấy một số phát hiện thú vị mới về vấn đề này, tôi quyết định thêm chúng dưới dạng câu trả lời, đồng thời thừa nhận rằng họ không giải quyết "tại sao nó xảy ra" trong câu hỏi ban đầu. Có lẽ ai đó biết nhiều hơn về hoạt động nội bộ của các loại liên quan có thể đăng câu trả lời phù hợp dựa trên những quan sát tôi đang đăng.

Tôi cũng đã quản lý để tái tạo sự cố trên máy của mình và tôi đã theo dõi một kết nối với System.R.78.InteropService._Type Interface , được System.Typelớp triển khai .

Ban đầu, tôi đã tìm thấy ít nhất 3 cách tiếp cận khắc phục để khắc phục sự cố:

  1. Đơn giản bằng cách truyền Typevào _Typebên trong Mainphương thức:

    var cctor = ((_Type)typeof(Test)).TypeInitializer;
  2. Hoặc đảm bảo rằng cách tiếp cận 1 đã được sử dụng trước đó trong phương thức:

    var warmUp = ((_Type)typeof(Test)).TypeInitializer; 
    var cctor = ((Type)typeof(Test)).TypeInitializer;
  3. Hoặc bằng cách thêm một trường tĩnh vào Testlớp và khởi tạo nó (với việc chuyển nó thành _Type):

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

Sau đó, tôi phát hiện ra rằng nếu chúng ta không muốn liên quan đến System.Runtime.InteropServices._Typegiao diện trong các cách giải quyết, thì vấn đề không xảy ra bởi:

  1. Thêm một trường tĩnh vào Testlớp và khởi tạo nó (mà không truyền nó tới _Type):

    static ConstructorInfo _dummy2 = typeof(object).TypeInitializer;
  2. Hoặc bằng cách tự khởi tạo cctorbiến như một trường tĩnh của lớp:

    static ConstructorInfo cctor = typeof(Test).TypeInitializer;

Tôi đang mong chờ phản hồi của bạn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.