성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - GCHandle 구조체의 메모리 분석</h1> <p> <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.gchandle?view=net-8.0'>GCHandle</a>을 역어셈블하면 다음과 같이 별다른 메모리 할당이 없지만,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; namespace System.Runtime.InteropServices { public partial struct GCHandle { [MethodImpl(MethodImplOptions.InternalCall)] private static extern IntPtr InternalAlloc(object? value, GCHandleType type); [MethodImpl(MethodImplOptions.InternalCall)] internal static extern void InternalFree(IntPtr handle); #if DEBUG // The runtime performs additional checks in debug builds [MethodImpl(MethodImplOptions.InternalCall)] internal static extern object? InternalGet(IntPtr handle); #else internal static unsafe object? InternalGet(IntPtr handle) => Unsafe.As<IntPtr, object>(ref *(IntPtr*)(nint)handle); #endif [MethodImpl(MethodImplOptions.InternalCall)] internal static extern void InternalSet(IntPtr handle, object? value); [MethodImpl(MethodImplOptions.InternalCall)] internal static extern object? InternalCompareExchange(IntPtr handle, object? value, object? oldValue); } } </pre> <br /> 소스코드를 직접 찾아보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // <a target='tab' href='https://github.com/microsoft/referencesource/blob/master/mscorlib/system/runtime/interopservices/gchandle.cs'>https://github.com/microsoft/referencesource/blob/master/mscorlib/system/runtime/interopservices/gchandle.cs</a> namespace System.Runtime.InteropServices { // ...[생략]... [StructLayout(LayoutKind.Sequential)] [System.Runtime.InteropServices.ComVisible(true)] public struct GCHandle { // ...[생략]... // The actual integer handle value that the EE uses internally. <span style='color: blue; font-weight: bold'>private IntPtr m_handle;</span> #if MDA_SUPPORTED // The GCHandle cookie table. static private volatile GCHandleCookieTable s_cookieTable = null; static private volatile bool s_probeIsActive = false; #endif } } </pre> <br /> IntPtr 타입의 m_handle 필드를 하나 가지고 있습니다. windbg를 이용하면 좀 더 재미있는 사실들을 알 수 있는데요, 예를 들어, GCHandle로 다음과 같이 코드를 만들어,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Runtime.InteropServices; namespace ConsoleApp1; internal class Program { static unsafe void Main(string[] args) { { MyObject obj = new MyObject(); GCHandle gcHandle = GCHandle.Alloc(obj, GCHandleType.Pinned); <span style='color: blue; font-weight: bold'>GCHandle* pHandle = &gcHandle; Console.WriteLine($"{(nint)pHandle:x}");</span> fixed(<span style='color: blue; font-weight: bold'>int* pAge</span> = &(obj.Age)) { <span style='color: blue; font-weight: bold'>Console.WriteLine($"{(nint)pAge:x}");</span> } Console.ReadLine(); gcHandle.Free(); } } } public class MyObject { public int Age = 0; } </pre> <br /> 실행하면 화면에는 이런 결과가 출력됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 583bb7e8e0 // GCHandle의 스택 주소 1ee7640e100 // MyObject의 Age 필드가 위치한 힙 주소 </pre> <br /> 이때 windbg를 붙여 GCHandle 정보를 살펴보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>!name2ee System.Private.CoreLib.dll!System.Runtime.InteropServices.GCHandle</span> Module: 00007ff7b77d4000 Assembly: System.Private.CoreLib.dll Token: 00000000020004FC MethodTable: 00007ff7b7a8bcb0 EEClass: <span style='color: blue; font-weight: bold'>00007ff7b7a72cf0</span> Name: System.Runtime.InteropServices.GCHandle 0:000> <span style='color: blue; font-weight: bold'>!DumpClass /d 00007ff7b7a72cf0</span> Class Name: System.Runtime.InteropServices.GCHandle mdToken: 00000000020004FC File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.14\System.Private.CoreLib.dll Parent Class: 00007ff7b77d0f88 Module: 00007ff7b77d4000 Method Table: 00007ff7b7a8bcb0 Vtable Slots: 5 Total Method Slots: 5 Class Attributes: 100109 <span style='color: blue; font-weight: bold'>NumInstanceFields: 1</span> NumStaticFields: 0 MT Field Offset Type VT Attr Value Name <span style='color: blue; font-weight: bold'>00007ff7b7979270 40010ec 8 System.IntPtr 1 instance _handle</span> </pre> <br /> 소스코드에서 봤던 바로 그 _handle 필드를 볼 수 있습니다. 아울러 화면에 출력된 GCHandle의 스택 주소를 이용해 그 값이 현재 무엇인지 덤프해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>dq 583bb7e8e0 L1</span> 00000058`3bb7e8e0 <span style='color: blue; font-weight: bold'>000001ee`736d15f1</span> </pre> <br /> ...f1으로 1비트까지 설정된 주소를 가리키는, 아주 희한한 값이 나옵니다. 보통 64비트 시스템이면 8바이트 정렬일 것이고, 위의 경우 핸들이 추상화된 정보라는 것을 감안해도 정보를 담기 위해서는 적어도 1비트보다는 (보통은) 클 것이기 때문입니다. 확인을 위해 이 상태에서 gchandle 테이블을 나열해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>!gchandles</span> Handle Type Object Size Data Type 000001EE736D1178 WeakShort 000001ee7641eaf0 40 System.Buffers.TlsOverPerCoreLockedStacksArrayPool`1[[System.Byte, System.Private.CoreLib]] 000001EE736D1180 WeakShort 000001ee76419320 72 System.Threading.Thread ...[생략]... <span style='color: blue; font-weight: bold'>000001EE736D15F0 Pinned 000001ee7640e0f8 24 ConsoleApp1.MyObject</span> 000001EE736D15F8 Pinned 000001ee76400200 24 System.Object ...[생략]... </pre> <br /> 원래 주소는 000001EE736D15F0임을 알 수 있습니다. 그러니까, 아마도 1비트가 표시된 것은 직접적인 메모리 주소라기보다는 어차피 8바이트 정렬이기 때문에 그 이하의 비트를 부가적인 정보를 보관하는 용도로 사용했을 거라는 추측을 할 수 있습니다. (실제로 <a target='tab' href='https://www.sysnet.pe.kr/2/0/935#4115'>윈도우 핸들의 경우 Lockbit 표시로 활용</a>합니다.)<br /> <br /> 어쨌든, 000001ee`736d15f1의 실제 주소는 000001ee`736d15f0임을 알 수 있고, 해당 주소를 덤프해 보면 예상할 수 있는 그 값이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>dq 000001ee`736d15f0 L1</span> 000001ee`736d15f0 <span style='color: blue; font-weight: bold'>000001ee`7640e0f8</span> </pre> <br /> 000001ee`7640e0f8 값은 화면에 출력했던 MyObject의 Age 필드가 위치한 힙 주소(1ee7640e100)보다 정확히 0x08 앞에 위치한 값에 해당합니다. (왜냐하면 <a target='tab' href='https://www.sysnet.pe.kr/2/0/1176'>실제 데이터보다 앞선 MethodTable의 주소</a>를 가리키기 때문입니다.)<br /> <br /> 정리해 보면, GCHandle 구조체는 Handle 테이블의 주소를 가리키고, 다시 그 핸들 테이블은 원래 개체에 대한 주소를 담고 있습니다. <br /> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, GCHandle의 Pinning은 위의 코드에서처럼 명시적으로 사용하는 경우 외에도 JIT 컴파일러가 자동으로 Pinning하는 경우도 있습니다.<br /> <br /> 그 한 사례가 바로 코드에서 사용한 문자열 리터럴인데요, 간단하게 다음의 코드로 예를 들어 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // (반드시) .NET 7 이하의 환경에서 빌드 namespace ConsoleApp2; internal class Program { static void Main(string[] args) { Console.WriteLine(Program.Test()); Console.ReadLine(); } public static string Test() => <span style='color: blue; font-weight: bold'>"Hello"</span>; } </pre> <a name='gchandle_jit'></a> <br /> windbg에서 Program.Test 메서드의 jit 코드가 이렇게 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:014> <span style='color: blue; font-weight: bold'>!name2ee ConsoleApp2!ConsoleApp2.Program.Test</span> Module: 00007ff83c8dcf50 Assembly: ConsoleApp2.dll Token: 0000000006000007 MethodDesc: 00007ff83c8def50 Name: ConsoleApp2.Program.Test() JITTED Code Address: <span style='color: blue; font-weight: bold'>00007ff83c813bc0</span> 0:014> <span style='color: blue; font-weight: bold'>!U /d 00007ff83c813bc0</span> Normal JIT generated code ConsoleApp2.Program.Test() ilAddr is 000001B28A1B20C1 pImport is 00000256A37FC0E0 Begin 00007FF83C813BC0, size 2d c:\temp\ConsoleApp1\ConsoleApp2\Program.cs @ 13: >>> 00007ff8`3c813bc0 55 push rbp 00007ff8`3c813bc1 57 push rdi 00007ff8`3c813bc2 56 push rsi 00007ff8`3c813bc3 4883ec20 sub rsp,20h 00007ff8`3c813bc7 488bec mov rbp,rsp 00007ff8`3c813bca 833d27960c0000 cmp dword ptr [00007ff8`3c8dd1f8],0 00007ff8`3c813bd1 7405 je 00007ff8`3c813bd8 00007ff8`3c813bd3 e8e828c45f call coreclr!JIT_DbgIsJustMyCode (00007ff8`9c4564c0) <span style='color: blue; font-weight: bold'>00007ff8`3c813bd8 48b83084008cb2010000 mov rax,1B28C008430h 00007ff8`3c813be2 488b00 mov rax,qword ptr [rax]</span> 00007ff8`3c813be5 488d6520 lea rsp,[rbp+20h] 00007ff8`3c813be9 5e pop rsi 00007ff8`3c813bea 5f pop rdi 00007ff8`3c813beb 5d pop rbp 00007ff8`3c813bec c3 ret </pre> <br /> 예상할 수 있듯이, 위의 코드에서 1B28C008430h 값은 gc handle 테이블의 위치이고, 그 주소([rax])에 보관하고 있는 값은 실제 String 개체를 가리킵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:014> <span style='color: blue; font-weight: bold'>dq 1B28C008430h L1</span> 000001b2`8c008430 <span style='color: blue; font-weight: bold'>000001b2`8e815378</span> 0:014> <span style='color: blue; font-weight: bold'>!do 000001b2`8e815378</span> Name: System.String MethodTable: 00007ff83c7ffd18 EEClass: 00007ff83c7ea740 Tracked Type: false Size: 32(0x20) bytes File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.14\System.Private.CoreLib.dll String: Hello Fields: MT Field Offset Type VT Attr Value Name 00007ff83c77e8d8 4000308 8 System.Int32 1 instance 5 _stringLength 00007ff83c7a63a8 4000309 c System.Char 1 instance 48 _firstChar 00007ff83c7ffd18 4000307 100 System.String 0 static 000001b28e800218 Empty 0:014> <span style='color: blue; font-weight: bold'>db 000001b2`8e815378+0x0c LA</span> 000001b2`8e815384 48 00 65 00 6c 00 6c 00-6f 00 H.e.l.l.o. </pre> <br /> 여기서 재미있는 건, 저런 식으로 Pinning한 개체는 GC Handle 테이블에 등록되지 않습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:014> <span style='color: blue; font-weight: bold'>!gchandles</span> Handle Type Object Size Data Type <span style='color: blue; font-weight: bold'>000001B28A1C1178</span> WeakShort 000001b28e81ea98 40 System.Buffers.TlsOverPerCoreLockedStacksArrayPool`1[[System.Byte, System.Private.CoreLib]] ...[생략]... <span style='color: blue; font-weight: bold'>000001B28A1C1DF8</span> AsyncPinned 000001b28e81ef88 72 System.Threading.OverlappedData </pre> <br /> 보는 바와 같이 1B28C008430h 주소는 저 범위(000001B28A1C1178 ~ 000001B28A1C1DF8)에 없는데요, 대신 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12545'>POH 영역</a>에 위치하게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:014> <span style='color: blue; font-weight: bold'>!eeheap</span> ...[생략]... ======================================== Number of GC Heaps: 1 ---------------------------------------- Small object heap segment begin allocated committed allocated size committed size generation 0: 01f29fe0f320 01b28e800020 01b28e822fe8 01b28e831000 0x22fc8 (143304) 0x31000 (200704) generation 1: 01f29fe0f270 01b28e400020 01b28e400020 01b28e401000 0x1000 (4096) generation 2: 01f29fe0f1c0 01b28e000020 01b28e000020 01b28e001000 0x1000 (4096) Large object heap segment begin allocated committed allocated size committed size 01f29fe0f3d0 01b28ec00020 01b28ec00020 01b28ec01000 0x1000 (4096) <span style='color: blue; font-weight: bold'>Pinned object heap</span> segment begin allocated committed allocated size committed size 01f29fe0ec40 <span style='color: blue; font-weight: bold'>01b28c000020 01b28c008c18</span> 01b28c011000 0x8bf8 (35832) 0x11000 (69632) ------------------------------ GC Allocated Heap Size: Size: 0x2bbc0 (179136) bytes. GC Committed Heap Size: Size: 0x45000 (282624) bytes. Total bytes consumed by CLR: 0x295000 (2707456) </pre> <br /> 그런데, 저렇게 처리한 것이 잘 이해는 안 됩니다. 원래 POH 영역은 Pinning 시킨 데이터를 위한 공간입니다. 그런데, 문자열 리터럴의 경우 해당 문자열 자체는 generation 0 힙 위치에 저장하면서, 그 문자열을 가리키는 GCHandle 값만 (GCHandle 테이블이 아닌) POH 영역에 보관하고 있는 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> (2024-04-05: 업데이트)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Lesser known CLR GC Handles ; <a target='tab' href='https://www.awise.us/2024/03/31/gc-handle.html'>https://www.awise.us/2024/03/31/gc-handle.html</a> </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
7115
(왼쪽의 숫자를 입력해야 합니다.)