디버거를 통해서 본 C# Generics

[제목] 디버거를 통해서 본 C# Generics

C# Generic Type은 컴파일시 Type Parameter 정보를 갖는 IL과 메타데이타를 생성한다. 런타임시 이러한 Generic Type이 처음 호출될 때, IL을 JIT 컴파일하는 단계에서 Type Parameter를 실제 타입으로 대체하게 된다. C#의 Generics는 런타임시에 Type Instantiation을 한다는 점에서 컴파일시에 Type을 매크로처럼 새로 만드는 C++와 다르며, 또한 C# Generics는 런타임시에 구체적인 타입을 생성해 낸다는 점에서 구체적 타입을 제거하는 Java와 다르다.

이 아티클에서는 디버깅을 통해 C# Generics가 런타임시 타임파라미터를 대체해여 새로운 타입을 생성해 내는 과정을 설명한다. 테스트용 프로그램으로써 아래 C# 프로그램은 C# Generics를 사용하여 4개의 서로 다른 타입파라미터를 사용, 서로 다른 Element 타입을 갖는 List 를 생성한 코드이다.

namespace genApp
{
	using System;
	using System.Collections.Generic;
	
    class Class1 { }
    class Class2 { }

    class Program
    {
        static void Main(string[] args)
        {
            new Program().Run();            
        }

        public void Run()
        {
            // Breakpoint1
            Console.WriteLine("Breakpoint1");
            Console.ReadLine();

            List<int> list1 = new List<int>();
            List<double> list2 = new List<double>();
            List<class1> list3 = new List<class1>();
            List<class2> list4 = new List<class2>();            

            Type t1 = list1.GetType();
            Type t2 = list2.GetType();
            Type t3 = list3.GetType();
            Type t4 = list4.GetType();
            
            Console.WriteLine(t1.ToString());
            Console.WriteLine(t2.ToString());
            Console.WriteLine(t3.ToString());
            Console.WriteLine(t4.ToString());
            
            // Breakpoint2
			Console.WriteLine("Breakpoint2");
            Console.ReadLine();
        }
    }
}

이 프로그램을 실행하면 다음과 같은 콘솔에 출력이 나온다. C# Generics 의 타입명은 특별한 Syntax를 갖는데 타입명 뒤에 ` 표시를 한 후 몇개의 타입파라미터를 갖는지를 숫자로 표시한다. 예를 들어, List<T> 의 경우 System.Collections.Generic.List 뒤에 `1 을 표시하고 해당 타입파라미터 타입을 뒤에 추가한다.

자 이제 디버거를 통해 내부를 살펴보기 위하여 위의 프로그램을 WinDbg에서 실행시킨 후 SOS를 로드한다. 위의 프로그램에서 Run() 메서드의 첫번째 Breakpoint1 부분까지 이동한 후, Program 클래스를 살펴보기 위해 !name2ee 를 써서 genApp.Program 클래스를 검색한다.

0:004> .loadby sos clr
0:004> !name2ee genApp!genApp.Program
Module:      00212eac
Assembly:    genApp.exe
Token:       02000004
MethodTable: 002137cc  <<===
EEClass:     002112ac
Name:        genApp.Program

여기서 MethodTable의 값 002137cc을 !dumpmt를 실행하여 genApp.Program.Run() 메서드를 살펴본다. 아래 결과 중에 JIT 컬럼을 보면 Run() 메서드가 이미 JIT 컴파일되었음을 알 수 있다. 만약 해당 메서드가 아직 JIT되지 않았다면 이곳에 None으로 표시된다.

0:004> !dumpmt -md 002137cc
EEClass:         002112ac
Module:          00212eac
Name:            genApp.Program
mdToken:         02000004
File:            D:\PROJECTS\genApp\bin\Debug\genApp.exe
BaseSize:        0xc
ComponentSize:   0x0
Slots in VTable: 7
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
   Entry MethodDe    JIT Name
615dcd88 612f60bc PreJIT System.Object.ToString()
615d6a90 612f60c4 PreJIT System.Object.Equals(System.Object)
615d6660 612f60e4 PreJIT System.Object.GetHashCode()
616567c0 612f60f8 PreJIT System.Object.Finalize()
004b00b0 002137c4    JIT genApp.Program..ctor()
004b0050 002137ac    JIT genApp.Program.Main(System.String[])
004b00e8 002137b8    JIT genApp.Program.Run()   <<===

이제 !U 명령을 사용하여 Run() 메서드를 Disassembly 해보자. 여기서 우리의 관심은 C# List Generics가 어떻게 표현되었는가에 대한 것이다. CLR은 런타임시에 C# Generics 으로부터 (만약 이미 만들지 않았다면) 구체적 타입파라미터와 함께 구체적인 타입을 새로 생성한다 (이를 Type Instantiation 이라 부른다). 아래 Disassembly를 보면, List<int>가 (MT: System.Collections.Generic.List`1[[System.Int32, mscorlib]]) 으로 표현되고 이 MethodTable 주소가 616d2828 임을 알 수 있다. 그리고 List<double>, List<Class1>, List<Class2> 들도 각각 616b31bc, 213878, 213C40 등의 MT 주소를 가지고 있다. 이 MT를 !dumpmt로 조사해 보면 실제 어떤 클래스인지 흥미로운 사실 하나를 알 수 있다(아래 설명).

0:004> !U 002137b8    
Normal JIT generated code
genApp.Program.Run()
Begin 004b00e8, size 188
...
D:\PROJECTS\genApp\Program.cs @ 22:
004b014f b928286d61      mov     ecx,offset mscorlib_ni+0x3e2828 (616d2828) (MT: System.Collections.Generic.List`1[[System.Int32, mscorlib]])
004b0154 e8a71fd5ff      call    00202100 (JitHelp: CORINFO_HELP_NEWSFAST)
004b0159 8945cc          mov     dword ptr [ebp-34h],eax
004b015c 8b4dcc          mov     ecx,dword ptr [ebp-34h]
004b015f e8ec2c1c61      call    mscorlib_ni+0x382e50 (61672e50) (System.Collections.Generic.List`1[[System.Int32, mscorlib]]..ctor(), mdToken: 060021e6)
004b0164 8b45cc          mov     eax,dword ptr [ebp-34h]
004b0167 8945f0          mov     dword ptr [ebp-10h],eax

D:\PROJECTS\genApp\Program.cs @ 23:
004b016a b9bc316b61      mov     ecx,offset mscorlib_ni+0x3c31bc (616b31bc) (MT: System.Collections.Generic.List`1[[System.Double, mscorlib]])
004b016f e88c1fd5ff      call    00202100 (JitHelp: CORINFO_HELP_NEWSFAST)
004b0174 8945c8          mov     dword ptr [ebp-38h],eax
004b0177 8b4dc8          mov     ecx,dword ptr [ebp-38h]
004b017a e82127a861      call    mscorlib_ni+0xc428a0 (61f328a0) (System.Collections.Generic.List`1[[System.Double, mscorlib]]..ctor(), mdToken: 060021e6)
004b017f 8b45c8          mov     eax,dword ptr [ebp-38h]
004b0182 8945ec          mov     dword ptr [ebp-14h],eax

D:\PROJECTS\genApp\Program.cs @ 24:
004b0185 b978382100      mov     ecx,213878h (MT: System.Collections.Generic.List`1[[genApp.Class1, genApp]])
004b018a e8711fd5ff      call    00202100 (JitHelp: CORINFO_HELP_NEWSFAST)
004b018f 8945c4          mov     dword ptr [ebp-3Ch],eax
004b0192 8b4dc4          mov     ecx,dword ptr [ebp-3Ch]
004b0195 e826f71261      call    mscorlib_ni+0x2ef8c0 (615df8c0) (System.Collections.Generic.List`1[[System.__Canon, mscorlib]]..ctor(), mdToken: 060021e6)
004b019a 8b45c4          mov     eax,dword ptr [ebp-3Ch]
004b019d 8945e8          mov     dword ptr [ebp-18h],eax

D:\PROJECTS\genApp\Program.cs @ 25:
004b01a0 b9403c2100      mov     ecx,213C40h (MT: System.Collections.Generic.List`1[[genApp.Class2, genApp]])
004b01a5 e8561fd5ff      call    00202100 (JitHelp: CORINFO_HELP_NEWSFAST)
004b01aa 8945c0          mov     dword ptr [ebp-40h],eax
004b01ad 8b4dc0          mov     ecx,dword ptr [ebp-40h]
004b01b0 e80bf71261      call    mscorlib_ni+0x2ef8c0 (615df8c0) (System.Collections.Generic.List`1[[System.__Canon, mscorlib]]..ctor(), mdToken: 060021e6)
004b01b5 8b45c0          mov     eax,dword ptr [ebp-40h]
004b01b8 8945e4          mov     dword ptr [ebp-1Ch],eax
...

우선 4개의 MethodTable을 조사하기 전에 먼저 GC Heap에 List 라는 문자열이 들어가는 객체가 있는지 한번 체크해 보자. 현재는 Breakpoint1 지점 즉 실제 객체 할당이 발생하지 않은 상태이므로, 결과는 당연히 0 객체를 표시하고 있다.

0:004> !dumpheap -type List
 Address       MT     Size

Statistics:
      MT    Count    TotalSize Class Name
Total 0 objects

다시 프로그램을 계속 진행시켜 BreakPoint2 상태로 이동하면, Managed Heap 상에 아래와 같이 4개의 객체가 존재함을 볼 수 있다.

0:004> g
0:008> !dumpheap -type List
 Address       MT     Size
02334d3c 616d2828       24     
02334d60 616b31bc       24     
02334d84 00213878       24     
02334dac 00213c40       24     

Statistics:
      MT    Count    TotalSize Class Name
616d2828        1           24 System.Collections.Generic.List`1[[System.Int32, mscorlib]]
616b31bc        1           24 System.Collections.Generic.List`1[[System.Double, mscorlib]]
00213c40        1           24 System.Collections.Generic.List`1[[genApp.Class2, genApp]]
00213878        1           24 System.Collections.Generic.List`1[[genApp.Class1, genApp]]
Total 4 objects

여기서 한가지 주목할 만한 점은 이곳에 표시된 MT 값이 (물론 당연하겠지만) 위의 Disassembly에서의 값과 각 타입별로 동일하다는 점이다. 자, 그러면 각 MT를 !dumpmt를 써서 조사해 보자. 출력 결과 중에 EEClass 값을 주의 깊게 살펴보자.

0:008> !dumpmt 616d2828
EEClass:         61355b14  <<===

Module:          612f1000
Name:            System.Collections.Generic.List`1[[System.Int32, mscorlib]]
mdToken:         02000364
File:            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 77
Number of IFaces in IFaceMap: 8

0:008> !dumpmt 616b31bc       
EEClass:         6134c8bc  <<===

Module:          612f1000
Name:            System.Collections.Generic.List`1[[System.Double, mscorlib]]
mdToken:         02000364
File:            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 77
Number of IFaces in IFaceMap: 8

0:008> !dumpmt 00213c40        
EEClass:         61352f98  <<===

Module:          612f1000
Name:            System.Collections.Generic.List`1[[genApp.Class2, genApp]]
mdToken:         02000364
File:            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 77
Number of IFaces in IFaceMap: 8

0:008> !dumpmt 00213878       
EEClass:         61352f98  <<===

Module:          612f1000
Name:            System.Collections.Generic.List`1[[genApp.Class1, genApp]]
mdToken:         02000364
File:            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 77
Number of IFaces in IFaceMap: 8

EEClass는 Execution Engine Class의 약자로 CLR에서 실제 생성되고 사용되는 클래스를 말한다. Generic Type의 경우 런타임시 새롭게 구체적 타입이 생성되므로 만약 이 구체적 타입이 같다면 동일한 EEClass를 갖는다고 볼 수 있다. 위의 !dumpmt 출력 결과를 보면, int 혹은 double 타입의 List는 서로 다른 EEClass를 가지고 있는데, Class1 혹은 Class2 의 List는 동일한 EEClass 주소를 가지고 있다. CLR은 C# Generics를 Type Instantiation 할 때, int, double, bool 등의 Value Type에 대해서는 별도의 클래스를 각각 생성하지만, Reference Type의 Generics 에 대해서는 하나의 클래스만을 생성하고 이를 공유하여 사용한다. 위의 결과를 통해 타입파라미터가 레퍼런스 타입인 List<Class1> 과 List<Class2> 는 CLR에서 동일한 EEClass를 사용함을 알 수 있다.

지금까지의 조사 과정을 통해 CLR은 Generic Type 을 갖는 메서드가 JIT 컴파일될 때 CLR 내 첫 Generic Type인 경우 Generic Type을 Type Instantiation 한다는 점과 만약 Type Parameter가 Reference 타입인 경우 하나의 Type Instantiation 으로 만들어진 EEClass를 공유해서 사용한다는 점을 알 수 있다.



본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.