[제목] 디버거를 통해서 본 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를 공유해서 사용한다는 점을 알 수 있다.
본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.