Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법 [링크 복사], [링크+제목 복사],
조회: 12467
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 5개 있습니다.)
디버깅 기술: 112. windbg - 닷넷 메모리 덤프에서 전역 객체의 내용을 조사하는 방법
; https://www.sysnet.pe.kr/2/0/11460

디버깅 기술: 115. windbg - 닷넷 메모리 덤프에서 정적(static) 필드 값을 조사하는 방법
; https://www.sysnet.pe.kr/2/0/11487

.NET Framework: 957. C# - static 필드의 정보가 GC Heap에 저장될까요?
; https://www.sysnet.pe.kr/2/0/12387

디버깅 기술: 182. windbg - 닷넷 메모리 덤프에서 AppDomain에 걸친 정적(static) 필드 값을 조사하는 방법
; https://www.sysnet.pe.kr/2/0/13004

디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
; https://www.sysnet.pe.kr/2/0/13414




Windbg - ThreadStatic 필드 값을 조사하는 방법

예전 글에서,

windbg - 닷넷 메모리 덤프에서 정적(static) 필드 값을 조사하는 방법
; https://www.sysnet.pe.kr/2/0/11487

정적 필드까지는 알아봤었는데요, 그렇다면 Thread마다 고유한 (이름만 비슷할 뿐 전혀 다른 성격의 저장소인) ThreadStatic인 경우에는 어떨까요?

테스트를 위해 다음과 같이 코드를 만들고,

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        CreateObject();
        GC.Collect();
        GC.Collect();

        Console.ReadLine();
    }

    private static void CreateObject()
    {
        MyClass.TlsRef = new Program();
        MyClass.TlsValue = 0xFF_EE_DD_CC_BB_AA_00_99;
        MyClass.StaticValue = 0x_AA_BB_CC_DD;
    }
}

class MyClass
{
    [ThreadStatic]
    public static Program? TlsRef;

    [ThreadStatic]
    public static ulong TlsValue;

    public static uint StaticValue;
}

실행한 다음, windbg로 attach 시켜 다음과 같이 static 변수를 덤프했던 경험을 살려 추적했더니,

0:014> !name2ee ConsoleApp1.dll!ConsoleApp1.MyClass
Module:      00007ffcba7fcf48
Assembly:    ConsoleApp1.dll
Token:       0000000002000007
MethodTable: 00007ffcba96cb38
EEClass:     00007ffcba94ed78
Name:        ConsoleApp1.MyClass

0:014> !DumpClass /d 00007ffcba94ed78
Class Name:      ConsoleApp1.MyClass
mdToken:         0000000002000007
File:            C:\temp\ConsoleApp1\bin\Debug\net7.0\ConsoleApp1.dll
Parent Class:    00007ffcba570e98
Module:          00007ffcba7fcf48
Method Table:    00007ffcba96cb38
Vtable Slots:    4
Total Method Slots:  5
Class Attributes:    100000  
NumInstanceFields:   0
NumStaticFields:     3
NumThreadStaticFields: 2
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffcba6cfc50  4000006       40        System.UInt32  1   static       2864434397 StaticValue
00007ffcba7feff8  4000004        0  ConsoleApp1.Program  0 TLstatic  TlsRef
    >> Thread:Value 8bcc:000001aac840df68 <<
00007ffcba6e3cf0  4000005       20        System.UInt64  1 TLstatic  TlsValue
    >> Thread:Value 8bcc:18441921395520307353 <<

오호... 이전의 static 변수 확인과 동일하게 잘 나옵니다. ^^

8bcc:000001aac840df68
 ==> 8bcc OS 스레드에 속한 값 000001aac840df68 == Program 인스턴스의 GC Heap 주소

8bcc:18441921395520307353
 ==> 16진수로 0xFFEEDDCCBBAA0099




여기서 재미있는 건, TLS도 역시 GC 입장에서는 root에 해당하는데요, 따라서 TLS 변수가 값 형식을 담고 있을 때는 GC 입장에서 관심 대상이 아니지만 참조 형식을 담고 있을 때는 GC가 반드시 관여를 해야 합니다.

즉, 위와 같은 경우에 TlsRef 변수에 Program 타입의 인스턴스를 가리키게 했는데요, 그렇다면 해당 인스턴스는 스레드가 종료될 때까지는 GC가 되어서는 안 됩니다.

실제로 해당 개체가 아직 살아 있는지 다음과 같이 확인해 볼 수 있습니다.

0:014> !dumpheap -type ConsoleApp1.Program
         Address               MT           Size
    01aac840df68     7ffcba7feff8             24 

Statistics:
          MT Count TotalSize Class Name
7ffcba7feff8     1        24 ConsoleApp1.Program
Total 1 objects, 24 bytes

편의상 1개의 개체만 만들었으므로 쉽게 판별할 수 있는데요, ^^ 이것의 root 개체가 무엇인지 보면,

0:014> !gcroot 01aac840df68
Caching GC roots, this may take a while.
Subsequent runs of this command will be faster.

HandleTable:
    000001aac5501358 (strong handle)
          -> 01aac840df80     System.Object[] 
          -> 01aac840df68     ConsoleApp1.Program 

Found 1 unique roots.

특이하게도 "strong handle" 성격의 HandleTable에 등록돼 있습니다. 전체 핸들 테이블은 !gchandles로 확인할 수 있는데,

0:014> !gchandles
          Handle Type                  Object     Size             Data Type
000001AAC5501178 WeakShort   000001aaca811860       40                  System.Buffers.TlsOverPerCoreLockedStacksArrayPool`1[[System.Byte, System.Private.CoreLib]]
...[생략]...
000001AAC55011F8 WeakShort   000001aac840c260       72                  System.Threading.Thread
000001AAC5501798 WeakLong    000001aaca811168      152                  System.RuntimeType+RuntimeTypeCache
...[생략]...
000001AAC55017F8 WeakLong    000001aac8409b68       64                  Interop+Advapi32+EtwEnableCallback
000001AAC5501318 Strong      000001aaca812fe8      144                  System.Object[]
000001AAC5501320 Strong      000001aaca808320       72                  System.Threading.Thread
000001AAC5501328 Strong      000001aaca811ac0       32                  System.Object[]
000001AAC5501330 Strong      000001aaca806d20       72                  System.Threading.Thread
000001AAC5501338 Strong      000001aaca8067c8       72                  System.Threading.Thread
000001AAC5501340 Strong      000001aaca808020      144                  System.Object[]
000001AAC5501348 Strong      000001aaca806020       72                  System.Threading.Thread
000001AAC5501350 Strong      000001aac5c08400     2072                  System.Object[]
000001AAC5501358 Strong      000001aac840df80       32                  System.Object[]
000001AAC5501360 Strong      000001aac5c04428    16344                  System.Object[]
000001AAC5501368 Strong      000001aac8412fe8      144                  System.Object[]
000001AAC5501370 Strong      000001aac840ea90       72                  System.Threading.Thread
...[생략]...
000001AAC55015F0 Pinned      000001aaca812988       25                  System.Byte[]
000001AAC55015F8 Pinned      000001aac8400200       24                  System.Object
000001AAC5501BF0 Dependent   000001aaca8121d0      456 0000000000000000 System.Buffers.TlsOverPerCoreLockedStacksArrayPool`1+ThreadLocalArray[[System.Byte, System.Private.CoreLib]][]
000001AAC5501BF8 Dependent   000001aac840bb48      456 0000000000000000 System.Buffers.TlsOverPerCoreLockedStacksArrayPool`1+ThreadLocalArray[[System.Char, System.Private.CoreLib]][]
000001AAC5501DF0 AsyncPinned 000001aaca812aa0       72                  System.Threading.OverlappedData
000001AAC5501DF8 AsyncPinned 000001aaca811d50       72                  System.Threading.OverlappedData

Statistics:
              MT    Count    TotalSize Class Name
00007ffcba5793b8        1           24 System.Object
...[생략]...
00007ffcba69aa68       14        36792 System.Object[]
Total 60 objects

Handles:
    Strong Handles:       27
    Pinned Handles:       2
    Async Pinned Handles: 2
    Weak Long Handles:    12
    Weak Short Handles:   15
    Dependent Handles:    2
... GCHandle type. Only 4 types are exposed this way: Normal, Pinned, Weak and WeakTrackResurrection
...
Weak and WeakTrackResurrection types are internally called short and long weak handles

보는 바와 같이 Strong 타입의 유형으로 000001AAC5501358 위치에 System.Object[] 인스턴스로 000001aac840df80 값을 담고 있습니다. 따라서 (만약 gcroot 명령어가 없었다고 해도) 모든 GC Handle Table의 Strong 유형에 해당하는 값을 대상으로 다음과 같은 식으로 조사하다 보면 운이 좋게 우리가 찾고 있는 개체를 참조하고 있는 항목을 발견할 수 있습니다.

0:014> dq 000001AAC5501358 L1
000001aa`c5501358  000001aa`c840df80

0:014> !DumpArray /d 000001aa`c840df80
Name:        System.Object[]
MethodTable: 00007ffcba69aa68
EEClass:     00007ffcba69a9d0
Size:        32(0x20) bytes
Array:       Rank 1, Number of elements 1, Type CLASS
Element Methodtable: 00007ffcba5793b8
[0] 000001aac840df68

0:014> !do 000001aac840df68
Name:        ConsoleApp1.Program
MethodTable: 00007ffcba7feff8
EEClass:     00007ffcba7dfd98
Tracked Type: false
Size:        24(0x18) bytes
File:        C:\temp\ConsoleApp1\bin\Debug\net7.0\ConsoleApp1.dll
Fields:
None




여기서 또 재미있는 것은, GC Handle Table의 위치입니다. !gchandles의 결과로 출력되는 항목 중 가장 첫 번째와 마지막의 주솟값은,

0:014> !gchandles
          Handle Type                  Object     Size             Data Type
000001AAC5501178 WeakShort   000001aaca811860       40                  System.Buffers.TlsOverPerCoreLockedStacksArrayPool`1[[System.Byte, System.Private.CoreLib]]
...[생략]...
000001AAC5501DF8 AsyncPinned 000001aaca811d50       72                  System.Threading.OverlappedData

000001AAC5501178 ~ 000001AAC5501DF8 범위인데, 이 영역은 GC Heap의 어떤 정보에도 속하지 않습니다.

0:014> !eeheap
Loader Heap:
----------------------------------------
System Domain:        7ffd1a585f70
LowFrequencyHeap:     7ffcbaa10000(10000:a000) ...[생략]... 7ffcba570000(3000:1000) Size: 0x10a000 (1089536) bytes total, 0x3000 (12288) bytes wasted.
HighFrequencyHeap:    7ffcbaa20000(10000:f000) ...[생략]... (1265664) bytes total, 0x3000 (12288) bytes wasted.
StubHeap:             7ffcba57d000(3000:1000) Size: 0x1000 (4096) bytes total.
IndirectionCellHeap:  7ffcba580000(6000:1000) Size: 0x1000 (4096) bytes total.
LookupHeap:           7ffcba58f000(4000:1000) Size: 0x1000 (4096) bytes total.
ResolveHeap:          7ffcba5c4000(57000:1000) Size: 0x1000 (4096) bytes total.
DispatchHeap:         7ffcba593000(31000:1000) Size: 0x1000 (4096) bytes total.
CacheEntryHeap:       7ffcba586000(9000:1000) Size: 0x1000 (4096) bytes total.
Total size:           Size: 0x245000 (2379776) bytes total, 0x6000 (24576) bytes wasted.
----------------------------------------
Domain 1:             01aac3a8d640
No unique loader heaps found.
----------------------------------------
JIT Manager:          01aac3a8fcd0
LoaderCodeHeap:       7ffcba730000(80000:a000) Size: 0xa000 (40960) bytes total.
Total size:           Size: 0xa000 (40960) bytes total.
----------------------------------------

========================================
Number of GC Heaps: 1
----------------------------------------
Small object heap
         segment            begin        allocated        committed allocated size   committed size  
generation 0:
    01ead984f950     01aaca800020     01aaca814fe8     01aaca821000 0x14fc8 (85960)  0x21000 (135168)
generation 1:
    01ead984f1c0     01aac7c00020     01aac7c00020     01aac7c11000                  0x11000 (69632) 
generation 2:
    01ead984f320     01aac8400020     01aac84132e0     01aac8421000 0x132c0 (78528)  0x21000 (135168)
Large object heap
         segment            begin        allocated        committed allocated size   committed size  
    01ead984f3d0     01aac8800020     01aac8800020     01aac8801000                  0x1000 (4096)   
Pinned object heap
         segment            begin        allocated        committed allocated size   committed size  
    01ead984ec40     01aac5c00020     01aac5c08c18     01aac5c11000 0x8bf8 (35832)   0x11000 (69632) 
------------------------------
GC Allocated Heap Size:    Size: 0x30e80 (200320) bytes.
GC Committed Heap Size:    Size: 0x65000 (413696) bytes.

Total bytes consumed by CLR: 0x2b4000 (2834432)

출력 결과에 따라 짐작해 보면, 가장 앞서 있는 "Pinned object heap" 영역이 01aac5c00020이라고 나오므로 (GC Handle의 범위인) 000001AAC5501178 ~ 000001AAC5501DF8은 그것보다 앞선 영역에 특별하게 할당돼 있는 것입니다.

암튼, GC도 참... 어지간히 꽤나 많은 일을 하는 것 같습니다. ^^




마지막으로, 위의 경우에는 운이 좋았는지 DumpClass의 결과에 ThreadStatic 변수의 내용이 함께 출력되었지만, 이게 언제나 가능한 것은 아닙니다. 일례로, 예제를 다음과 같은 식으로 바꿨더니,

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        MyClass.Obj = new Program();
        MyClass.N1 = 1;
        MyClass.N2 = 2;
        MyClass.N3 = 3;
        MyClass.N4 = 4;
        MyClass.N5 = 5;
        MyClass.N6 = 6;
        MyClass.N7 = 7;

        Console.ReadLine();
    }
}

class MyClass
{
    [ThreadStatic]
    public static Program? Obj;

    [ThreadStatic]
    public static int N1;

    [ThreadStatic]
    public static long N2;

    [ThreadStatic]
    public static int N3;

    [ThreadStatic]
    public static int N4;

    [ThreadStatic]
    public static int N5;

    [ThreadStatic]
    public static int N6;

    [ThreadStatic]
    public static int N7;
}

아래와 같은 식으로 windbg가 출력을 했습니다.

0:015> !DumpClass /d 00007ffcba94edd8
Class Name:      ConsoleApp1.MyClass
mdToken:         0000000002000007
File:            C:\temp\ConsoleApp1\bin\Debug\net7.0\ConsoleApp1.dll
Parent Class:    00007ffcba580e98
Module:          00007ffcba80cf48
Method Table:    00007ffcba976b28
Vtable Slots:    4
Total Method Slots:  5
Class Attributes:    100000  
NumInstanceFields:   0
NumStaticFields:     8
NumThreadStaticFields: 8
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffcba80ef98  4000004        0  ConsoleApp1.Program  0 TLstatic  Obj
    >> Thread:Value <<
00007ffcba6ae8d0  4000005       28         System.Int32  1 TLstatic  N1
    >> Thread:Value <<
00007ffcba6f2058  4000006       20         System.Int64  1 TLstatic  N2
    >> Thread:Value <<
00007ffcba6ae8d0  4000007       2c         System.Int32  1 TLstatic  N3
    >> Thread:Value <<
00007ffcba6ae8d0  4000008       30         System.Int32  1 TLstatic  N4
    >> Thread:Value <<
00007ffcba6ae8d0  4000009       34         System.Int32  1 TLstatic  N5
    >> Thread:Value <<
00007ffcba6ae8d0  400000a       38         System.Int32  1 TLstatic  N6
    >> Thread:Value <<
00007ffcba6ae8d0  400000b       3c         System.Int32  1 TLstatic  N7
    >> Thread:Value <<

보다시피 ">> Thread:Value <<" 문자열만 출력될 뿐 실제 값은 나오지 않고 있는 것입니다. 이와 관련해 검색해 보면,

Inspecting thread static value for generic class failed in SOS
; https://github.com/dotnet/diagnostics/issues/818

제네릭 유형인 경우에도 값이 안 나온다는 정도의 이슈만 나옵니다. 뭔가, ThreadStatic 값을 가져오는 코드가 그다지 매끄럽게 작성된 것은 아닌 듯합니다.

따라서, 아마도 실제 응용 프로그램의 덤프에서 ThreadStatic 값을 확인할 수 있는 경우는 쉽지 않을 것입니다. 그래도 값 형식이 아닌, 참조 형식인 경우라면 그나마 Handle Table을 뒤져 보거나, gcroot를 이용해 유추할 수 있는 정도로 유추는 할 수 있습니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/5/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1788정성태10/22/201424286VC++: 82. COM 프로그래밍에서 HRESULT 타입의 S_FALSE는 실패일까요? 성공일까요? [2]
1787정성태10/22/201432570오류 유형: 252. COM 개체 등록시 0x8002801C 오류가 발생한다면?
1786정성태10/22/201434037디버깅 기술: 65. 프로세스 비정상 종료 시 "Debug Diagnostic Tool"를 이용해 덤프를 남기는 방법 [3]파일 다운로드1
1785정성태10/22/201423137오류 유형: 251. 이벤트 로그 - Load control template file /_controltemplates/TaxonomyPicker.ascx failed [1]
1784정성태10/22/201430733.NET Framework: 472. C/C++과 C# 사이의 메모리 할당/해제 방법파일 다운로드1
1783정성태10/21/201424512VC++: 81. 프로그래밍에서 borrowing의 개념
1782정성태10/21/201421267오류 유형: 250. 이벤트 로그 - Application Server job failed for service instance Microsoft.Office.Server.Search.Administration.SearchServiceInstance
1781정성태10/21/201421999디버깅 기술: 64. new/delete의 짝이 맞는 경우에도 메모리 누수가 발생한다면?
1780정성태10/15/201425853오류 유형: 249. The application-specific permission settings do not grant Local Activation permission for the COM Server application with CLSID
1779정성태10/15/201421084오류 유형: 248. Active Directory에서 OU가 지워지지 않는 경우
1778정성태10/10/201419510오류 유형: 247. The Netlogon service could not create server share C:\Windows\SYSVOL\sysvol\[도메인명]\SCRIPTS.
1777정성태10/10/201422528오류 유형: 246. The processing of Group Policy failed. Windows attempted to read the file \\[도메인]\sysvol\[도메인]\Policies\{...GUID...}\gpt.ini
1776정성태10/10/201419596오류 유형: 245. 이벤트 로그 - Name resolution for the name _ldap._tcp.dc._msdcs.[도메인명]. timed out after none of the configured DNS servers responded.
1775정성태10/9/201420834오류 유형: 244. Visual Studio 디버깅 (2) - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
1774정성태10/9/201427746개발 환경 구성: 246. IIS 작업자 프로세스의 20분 자동 재생(Recycle)을 끄는 방법
1773정성태10/8/201430993.NET Framework: 471. 웹 브라우저로 다운로드가 되는 파일을 왜 C# 코드로 하면 안되는 걸까요? [1]
1772정성태10/3/201419809.NET Framework: 470. C# 3.0의 기본 인자(default parameter)가 .NET 1.1/2.0에서도 실행될까? [3]
1771정성태10/2/201428946개발 환경 구성: 245. 실행된 프로세스(EXE)의 명령행 인자를 확인하고 싶다면 - Sysmon [4]
1770정성태10/2/201422805개발 환경 구성: 244. 매크로 정의를 이용해 파일 하나로 C++과 C#에서 공유하는 방법 [1]파일 다운로드1
1769정성태10/1/201425557개발 환경 구성: 243. Scala 개발 환경 구성(JVM, 닷넷) [1]
1768정성태10/1/201420352개발 환경 구성: 242. 배치 파일에서 Thread.Sleep 효과를 주는 방법 [5]
1767정성태10/1/201425708VS.NET IDE: 94. Visual Studio 2012/2013에서의 매크로 구현 - Visual Commander [2]
1766정성태10/1/201423841개발 환경 구성: 241. 책 "프로그래밍 클로저: Lisp"을 읽고 나서. [1]
1765정성태9/30/201427487.NET Framework: 469. Unity3d에서 transform을 변수에 할당해 사용하는 특별한 이유가 있을까요?
1764정성태9/30/201423721오류 유형: 243. 파일 삭제가 안 되는 경우 - The action can't be comleted because the file is open in System
1763정성태9/30/201425251.NET Framework: 468. PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법파일 다운로드1
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...