เหตุใดการค้นหาตัวเริ่มต้นของประเภทจึงทำให้ NullReferenceException


194

นี่ทำให้ฉันนิ่งงัน ฉันพยายามเพิ่มประสิทธิภาพการทดสอบบางอย่างสำหรับ Noda Time ซึ่งเรามีการตรวจสอบประเภทเริ่มต้น ฉันคิดว่าฉันพบว่าเป็นชนิดที่มี initializer ชนิด (คอนสตรัคคงที่หรือตัวแปรคงที่กับ initializers) AppDomainก่อนที่จะโหลดทุกอย่างเข้าใหม่ สำหรับความประหลาดใจของฉันการทดสอบเล็ก ๆ น้อย ๆ ของการขว้างปานี้NullReferenceException- แม้ว่าจะไม่มีค่า null ในรหัสของฉัน มันจะโยนข้อยกเว้นเมื่อรวบรวมโดยไม่มีข้อมูลการแก้ปัญหา

นี่เป็นโปรแกรมสั้น ๆ แต่สมบูรณ์เพื่อแสดงให้เห็นถึงปัญหา:

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 (ตัวเลือกการเปิดตัว) - ซึ่งอาจเกี่ยวข้องกับที่นี่ มันค่อนข้างยุ่งยากสำหรับฉันที่จะทดสอบกับเฟรมเวิร์กดั้งเดิมอื่น ๆ (โดยเฉพาะ "วานิลลา". NET 4) แต่ถ้าใครอื่นสามารถเข้าถึงเครื่องจักรกับเฟรมเวิร์กอื่น ๆ ได้ฉันจะสนใจผลลัพธ์

รายละเอียดอื่น ๆ:

  • ฉันใช้เครื่อง x64 แต่ปัญหานี้เกิดขึ้นกับชุดประกอบ x86 และ x64
  • มันเป็น "debug-ness" ของรหัสการโทรที่สร้างความแตกต่าง - แม้ว่าในกรณีทดสอบข้างต้นเป็นการทดสอบในชุดประกอบของตัวเองเมื่อฉันลองกับ Noda Time ฉันไม่ต้องคอมไพล์NodaTime.dllเพื่อดูความแตกต่าง - เพียงTest.csซึ่งเรียกมัน
  • การรัน "เสีย" การชุมนุมในโมโน 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โทรตามต้นฉบับทั้งสองเวอร์ชันจะล้มเหลว


92
Heh - "แม้จะไม่มีค่า null ในรหัสของฉัน" นี่อาจเป็นครั้งแรกในประวัติศาสตร์ SO ที่บันทึกไว้ว่าการ์ด "ข้อผิดพลาดไม่อยู่ในรหัสของฉัน" ได้รับการเล่นสำเร็จแล้ว
Marc Gravell

1
ส่งคืน True ได้ดีโดยไม่มี Debug ทำการทดสอบครั้งแรกจาก cmdline ด้วย. NET 4 Framework, คอมไพเลอร์ Visual C # 4.0.30319.1
Kerry

2
@ MarcGravell: ใช่ในขณะที่ฉันมักสงสัยมากว่า "ไม่มีข้อบกพร่องในรหัสของฉัน " ในกรณีนี้เมื่อมีการแสดงออกเดียวที่เดิมพันและข้อยกเว้นคือNullReferenceException(ซึ่งควรระบุข้อผิดพลาดเสมอ ) มันจริง ๆ ดูหลบ ผมขอผู้ต้องสงสัยว่านี้เป็นข้อผิดพลาด .NET 4.5, ฉันพลาดหน้าต่างสำหรับการมันคง ...
จอนสกีต

15
@ JonSkeet: เราทุกคนรู้ว่า SP1 ของ MS คือ RTM จริง p
leppie

1
@ leppie: csc /o+ /debug- Test.csไม่ก็ล้มเหลวสำหรับฉันเช่นกันซึ่งแปลก
Jon Skeet

คำตอบ:


284

ด้วย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](ที่อยู่ ret เฟรม AV ของเราคือคำแนะนำหลังจากนี้call)

csc /debug test.csให้เปรียบเทียบกับสิ่งที่เกิดขึ้นเมื่อเรารวบรวม เราสามารถตั้งค่า a 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

โปรดทราบว่า@rsi00000000002debd8 การก้าวผ่านฟังก์ชั่นแสดงให้เห็นว่านี่คือที่อยู่ที่จะถูกยกเลิกการลงทะเบียนภายหลังเมื่อเกิดเหตุระเบิด 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. นี่คือสิ่งที่โหลดอยู่ที่ได้รับ dereferenced @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 อย่างสมบูรณ์)


46
คุณสมควรได้รับ reps มากกว่านี้สำหรับทักษะการดีบั๊ก
JSB ձոգչ

23
นี่เป็นข้อบกพร่องของเครื่องมือเพิ่มประสิทธิภาพ CORINFO * เป็นตัวชี้ฟังก์ชันเรียกใช้ JIT_GetSharedGCStaticBase ฉันเดาว่ามันเพิ่มขึ้นตามคุณลักษณะ jit พื้นหลัง 4.5 ใหม่และเข้าถึงเขตข้อมูลก่อนที่จะถูกเตรียมใช้งานโดยลืมใส่ jit ในชั้นเรียน รายงานสิ่งนี้ได้ที่ connect.microsoft.com
Hans Passant

28
ไม่จำเป็น. เรากำลังมองหาอยู่ คุณพูดถูกสิ่งที่เกิดขึ้นก็เพราะเราจัดสรรอินสแตนซ์ของ RuntimeType โดยตรงที่ cctor ของ Type ที่ไม่เคยถูกเรียกดังนั้น Type.EmptyTypes ยังคงเป็นโมฆะและนั่นคือสิ่งที่ผ่านไปยัง GetConstructor
Kirill Osenkov

3
มีหนังสือที่ฉันสามารถอ่านเพื่อรับทักษะการแก้ไขข้อบกพร่องเหล่านี้ได้หรือไม่ (ควรเริ่มต้นด้วย "Idiots Guide to" หรือลงท้ายด้วย "for Dummies")
Igby Largeman

1
@IgbyLargeman: การดีบัก Windows ขั้นสูงค่อนข้างดี
Remus Rusanu

10

เนื่องจากฉันเชื่อว่าฉันพบสิ่งที่ค้นพบใหม่ที่น่าสนใจเกี่ยวกับปัญหานี้ฉันจึงตัดสินใจที่จะเพิ่มคำตอบลงในขณะเดียวกันพวกเขาก็ไม่ได้กล่าวถึง"สาเหตุของปัญหา"ในคำถามเดิม บางทีคนที่รู้เพิ่มเติมเกี่ยวกับการทำงานภายในของประเภทที่เกี่ยวข้องอาจโพสต์คำตอบที่แก้ไขตามการสังเกตที่ฉันโพสต์

ฉันจัดการเพื่อสร้างปัญหาบนเครื่องของฉันและฉันได้ติดตามการเชื่อมต่อกับ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;

ฉันรอคอยที่จะแสดงความคิดเห็นของคุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.