Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)
(시리즈 글이 2개 있습니다.)
.NET Framework: 308. .NET System.Threading.Thread 개체에서 Native Thread Id를 구할 수 있을까?
; https://www.sysnet.pe.kr/2/0/1244

.NET Framework: 452. .NET System.Threading.Thread 개체에서 Native Thread Id를 구하는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/1724




.NET System.Threading.Thread 개체에서 Native Thread Id를 구하는 방법 - 두 번째 이야기

(이 글에 사용된 모든 코드는 첨부 파일에 있습니다.)

예전에 한번 이에 대해서 글을 썼었죠!

.NET System.Threading.Thread 개체에서 Native Thread Id를 구할 수 있을까?
; https://www.sysnet.pe.kr/2/0/1244

오늘도 잠시 뭔가 테스트를 하다 Native 스레드 ID가 필요해서 다시 그 글을 보았습니다. (사실, 제가 쓴 글이지만 저도 시간 지나면 참고하게 됩니다. ^^;)

정리하면 다음과 같은 작업을 해야 합니다.

  1. Thread 객체로부터 private 필드인 "DONT_USE_InternalThread" 값을 구한다.
  2. "DONT_USE_InternalThread" 값이 가리키는 포인터의 64바이트 위치에 TEB 값을 읽는다. (참고로, 윈도우 8.1 + .NET 4.5가 설치된 제 PC의 경우 2번째의 64바이트 위치가 60바이트 위치로 바뀌었습니다.)
  3. TEB값에서 (0x6b4 + 0x4)에 위치한 Thread Id를 읽는다.

근데, "DONT_USE_InternalThread" 값의 의미가 도대체 뭘까요? 이에 대한 단서는 다음의 글이 거의 유일한 것 같습니다.

Thread, System.Threading.Thread, and !Threads (II)
; http://blogs.msdn.com/b/yunjin/archive/2005/08/29/457150.aspx

위의 글에 보면, DONT_USE_InternalThread 값으로부터 m_ExposedObject 값을 구하는 dt 명령어가 나옵니다.

0:043> dt mscorwks!Thread 0x00180c10 m_ExposedObject
   +0x0c0 m_ExposedObject : 0x00a71054 

(적어도 .NET 1.1 시절에는) mscorwks 모듈의 Thread 구조체로 존재하는 것으로 보이는 데요. 이 정보를 바탕으로 혹시 SSCLI 소스 코드를 뒤져보면 뭔가 나오지 않을까 싶었는데 역시나 ".\clr\src\vm\threads.h" 파일에서 그 정의를 찾을 수 있었습니다.

해당 파일에서 Thread 타입을 대상으로 필드만 대충 정리해 보면 다음과 같습니다.

class Thread: public ICLRTask
{
    static LONG     m_DetachCount;
    static LONG     m_ActiveDetachCount;  // Count how many non-background detached

    static volatile LONG     m_threadsAtUnsafePlaces;

    volatile ThreadState m_State;   // Bits for the state of the thread

    volatile ULONG       m_fPreemptiveGCDisabled;

    PTR_Frame            m_pFrame;  // The Current Frame
    PTR_Frame            m_pUnloadBoundaryFrame;

    // Roots to scan
    CLRRootBase         *m_pRoot;

    DWORD                m_dwLockCount;

    DWORD                m_ThreadId;

    IHostTask           *m_pHostTask;


    LockEntry           *m_pHead;
    LockEntry            m_embeddedEntry;

    PTR_Context          m_Context;

    alloc_context        m_alloc_context;

    StackingAllocator    m_MarshalAlloc;

    ThreadTasks          m_ThreadTasks;

    ThreadStateNoConcurrency m_StateNC;

#ifdef CALLDESCR_RETBUF
    BYTE            m_SmallVCRetVal[ENREGISTERED_RETURNTYPE_MAXSIZE];
#endif

private:
    DWORD m_dwBeginLockCount;  // lock count when the thread enters current domain
    DWORD m_dwBeginCriticalRegionCount;  // lock count when the thread enters current domain
    DWORD m_dwNonHostLockCount;
    DWORD m_dwCriticalRegionCount;
    DWORD m_dwDelayAbortCount;

    DWORD m_dwThreadAffinityCount;

#ifdef _DEBUG
    DWORD m_dwSuspendThread;
    EEThreadId m_Creater;
#endif

    volatile LONG m_dwForbidSuspendThread;

private:
    DWORD m_dwHashCodeSeed;

    DeadlockAwareLock   * volatile m_pBlockingLock;

public:

 private:
    LoadLevelLimiter *m_pLoadLimiter;

private:
    BOOL m_fSecurityStackwalk;

private:
    //
    static bool     s_fSysSuspendInProgress;

private:
    // Specifies type of thread abort.
    DWORD  m_AbortInfo;
    DWORD  m_AbortType;
    ULONGLONG  m_AbortEndTime;
    ULONGLONG  m_RudeAbortEndTime;
    BOOL   m_fRudeAbortInitiated;
    LONG   m_AbortController;

    static ULONGLONG s_NextSelfAbortEndTime;

    LONG  m_AbortRequestLock;

public:
#ifdef _DEBUG
    BOOL           m_fRudeAborted;
    DWORD          m_dwAbortPoint;
#endif


public:
    DWORD          m_ThrewControlForThread;  

    PTR_CONTEXT m_OSContext;    

private:
    SLink       m_LinkStore;
    
    DWORD       m_dwLastError;

private:
    static CLREvent * g_pGCSuspendEvent;

    DWORD       m_Win32FaultAddress;
    DWORD       m_Win32FaultCode;

    LONG        m_UserInterrupt;

#if defined(_DEBUG) && defined(TRACK_SYNC)
public:
    Dbg_TrackSync   *m_pTrackSync;

#endif

private:
    CLREvent        m_SafeEvent;
    CLREvent        m_UserSuspendEvent;
    CLREvent        m_DebugSuspendEvent;

    CLREvent        m_EventWait;
    WaitEventLink   m_WaitEventLink;

    HANDLE          m_ThreadHandle;

    HANDLE          m_ThreadHandleForClose;
    HANDLE          m_ThreadHandleForResume;
    BOOL            m_WeOwnThreadHandle;
    DWORD           m_OSThreadId;

    OBJECTHANDLE    m_ExposedObject;
    OBJECTHANDLE    m_StrongHndToExposedObject;

    DWORD           m_Priority;     // initialized to INVALID_THREAD_PRIORITY, set to actual priority when a
                                    // thread does a busy wait for GC, reset to INVALID_THREAD_PRIORITY after wait is over
    ULONG           m_ExternalRefCount;

    ULONG           m_UnmanagedRefCount;

    LONG            m_TraceCallCount;

    DWORD           m_fPromoted;

private:
    OBJECTHANDLE m_LastThrownObjectHandle;      // Unsafe to use directly.  Use accessors instead.

private:
    ADID m_pKickOffDomainId;

    ThreadExceptionState  m_ExceptionState;

    UINT_PTR             m_ProbeLimit;
    IA64_ONLY(UINT_PTR m_BSPProbeLimit);

    UINT_PTR             m_LastAllowableStackAddress;
    IA64_ONLY(UINT_PTR m_LastAllowableBackingStoreStackAddress);

    PTR_AppDomain m_pDomain;

    PTR_AppDomain m_pDomainAtTaskSwitch;

    PTR_CONTEXT m_debuggerFilterContext;

    CONTEXT *m_pProfilerFilterContext;

    volatile LONG m_hijackLock;
    DWORD m_debuggerCantStop;

    DWORD    m_debuggerWord;

    BOOL    m_fInteropDebuggingHijacked;

    DWORD m_profilerCallbackState;

private:

    AppDomainStack  m_ADStack;

    DomainFile* m_pLoadingFile;


    OBJECTHANDLE    m_AbortReason;
    ADID            m_AbortReasonDomainID;

protected:
    EEIntHashTable* m_pDLSHash;

private:
    int         m_PreventAsync;
    int         m_PreventThreadAbort;
    int         m_nNestedMarshalingExceptions;

    static LONG m_DebugWillSyncCount;

    #define CLEANUP_IPS_PER_CHUNK 4
    struct CleanupIPs {
        IUnknown    *m_Slots[CLEANUP_IPS_PER_CHUNK];
        CleanupIPs  *m_Next;
        CleanupIPs() {LEAF_CONTRACT; memset(this, 0, sizeof(*this)); }
    };
    CleanupIPs   m_CleanupIPs;

public:
        DWORD m_GCOnTransitionsOK;
    ULONG  m_ulForbidTypeLoad;


#define OBJREF_TABSIZE              256
        DWORD_PTR dangerousObjRefs[OBJREF_TABSIZE];      // Really objectRefs with lower bit stolen

        static DWORD_PTR OBJREF_HASH;
private:
        PEXCEPTION_REGISTRATION_RECORD * m_pExceptionList;

private:
    PTR_CONTEXT m_pSavedRedirectContext;

private:
    PTR_STATIC_DATA  m_pUnsharedStaticData;
    PTR_STATIC_DATA  m_pSharedStaticData;

    PTR_EEPtrHashTable m_pStaticDataHash;
    Crst               *m_pSDHCrst;           // Mutex protecting m_pStaticDataHash

private:
    BOOL m_fStressHeapCount;

public:
    size_t *m_pCleanedStackBase;

#ifdef STRESS_THREAD
public:
    LONG  m_stressThreadCount;
#endif

private:
    PVOID      m_pFiberData;

    TASKID     m_TaskId;
    CONNID     m_dwConnectionId;

#ifdef _DEBUG
private:
    static int MaxThreadRecord;
    static int MaxStackDepth;
    static const int MaxThreadTrackInfo;
    struct FiberSwitchInfo
    {
        unsigned __int64 timeStamp;
        DWORD threadID;
        size_t callStack[1];
    };
    FiberSwitchInfo *m_pFiberInfo[ThreadTrackInfo_Max];
    DWORD m_FiberInfoIndex[ThreadTrackInfo_Max];
#endif

private:
    DWORD   m_dwPrepareCer;

private:
    static int m_offset_counter;
    static const int offset_multiplier = 128;

    typedef struct {
        LPTHREAD_START_ROUTINE  lpThreadFunction;
        PVOID lpArg;
    } intermediateThreadParam;

#ifdef _DEBUG
private:
    BOOL m_bGCStressing; // the flag to indicate if the thread is doing a stressing GC
    BOOL m_bUniqueStacking; // the flag to indicate if the thread is doing a UniqueStack
#endif

private:
    DWORD m_dwAVInRuntimeImplOkayCount;

#ifdef _DEBUG
private:
    DWORD m_dwUnbreakableLockCount;

#endif // _DEBUG

private:
    LONG m_dwHostTaskRefCount;

private:
    Exception* m_pExceptionDuringStartup;

#if defined(USE_DBGHELP_TO_WALK_STACK_IN_DAC)
private:
    static HMODULE                 s_hDbgHelp;
    static PDBGHELP__STACKWALK     s_pfnStackWalk;
    static PDBGHELP__SYMINITIALIZE s_pfnSymInitialize;
    static PDBGHELP__SYMCLEANUP    s_pfnSymCleanup;

    static HANDLE s_hFakeProcess;
    static HANDLE s_hFakeThread;

#endif // USE_DBGHELP_TO_WALK_STACK_IN_DAC

#if defined(STRESS_HEAP) && defined(_DEBUG)
private:
    BYTE* m_pbDestCode;
    BYTE* m_pbSrcCode;

#endif // defined(STRESS_HEAP) && defined(_DEBUG)

#if defined(STACK_GUARDS_DEBUG)
private:
    BaseStackGuard *m_pCurrentStackGuard;
#endif

private:
    BOOL m_fCompletionPortDrained;

private:
    SIZE_T m_RequestedStackSize;

private:
    volatile PVOID m_WorkingOnThreadContext;

private:
    EXCEPTION_POINTERS m_SOExceptionInfo;

private:
    BOOL m_fAllowProfilerCallbacks;

private:
    volatile LONG m_dwThreadHandleBeingUsed;

private:
    static BOOL s_fCleanFinalizedThread;
}

중간 쯤에 보면 "OBJECTHANDLE m_ExposedObject;" 필드 정의가 나오고 그것이 대략 0xc0 위치라고 하니 Thread 타입이 얼마나 거대한지 대략 짐작케 합니다. 정리해 보면, CLI의 Thread 클래스는 m_ExposedObject로 .NET Thread 개체를 가리키고, .NET Thread 개체는 내부에 DONT_USE_InternalThread 필드로 CLI의 Thread 클래스를 가리킵니다.

재미있는 것은, CLI의 Thread 정의를 보면 "DWORD m_OSThreadId;" 필드도 볼 수 있습니다. 그렇다면 ThreadObject를 덤프해 보면 분명히 Native Thread ID가 출력된다는 것인데요. 그래서 windbg를 통해 테스트를 해봤습니다.

0:004> .loadby sos clr

0:004> !threads
ThreadCount:      3
UnstartedThread:  0
BackgroundThread: 2
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 553c 00b73b28   203a220 Preemptive  0254D900:00000000 00b6e1f0 0     MTA 
   2    2 452c 00b811a0     2b220 Preemptive  00000000:00000000 00b6e1f0 0     MTA (Finalizer) 
   3    3 18a0 00ba9870   202b020 Preemptive  0250428C:00000000 00b6e1f0 0     MTA 

테스트는 3번 스레드로 DONT_USE_InternalThread == ThreadOBJ == 0x00ba9870이고 그것의 Native Thread ID는 0x18a0입니다. 그리고 덤프를 3번정도 해보니 정말로 Native Thread ID가 포함되어 있습니다.

0:003> dd 0xba9870
00ba9870  736c80a0 0202b020 00000000 04a7f0c0
00ba9880  00000000 00b6e1f0 00000000 00000003
00ba9890  00ba9894 00ba9894 00ba9894 00000000
00ba98a0  00000000 00000000 00b64228 7f3cf000
00ba98b0  0250428c 02505fe8 00002000 00000000
00ba98c0  00000000 00000000 00000000 00000000
00ba98d0  00000000 00000013 727e21b4 00ba9238
00ba98e0  00000000 00000000 00000000 00000000

0:003> dd
00ba98f0  00000000 00000000 04000180 00000000
00ba9900  00000000 00000000 00000000 00000000
00ba9910  3b802c20 00000000 00000000 00000000
00ba9920  00000000 00000000 ffffffff ffffffff
00ba9930  ffffffff ffffffff 00000000 00000000
00ba9940  00000000 00000000 00ba38e8 00000000
00ba9950  00000000 00000000 04a80000 04980000
00ba9960  04a00000 cccccccc cccccccc 00000175

0:003> dd
00ba9970  00000000 00000000 00000184 0000004b
00ba9980  00000000 00000214 00000000 00000218
00ba9990  00000000 0000021c 00000000 00000220
00ba99a0  00000000 00000000 000001bb 0000005e
00ba99b0  00000000 00000000 00000063 00000224
00ba99c0  ffffffff ffffffff 00000001 000018a0
00ba99d0  009e12f4 009e11d0 80000000 00000002
00ba99e0  00000000 00000000 0000006e 00000000

이것의 옵셋 위치는 0x15c입니다. (위의 테스트는 Windows 8.1 x64에서 x86 EXE로 테스트한 것입니다.)




하지만 역시 이번에도 0x15c 옵셋값은 하드 코딩할 수밖에 없습니다. 따라서 각종 닷넷 버전에 따라 이 옵셋은 바뀔 수 있고 심지어 .NET 서비스 팩에서조차 바뀔 가능성이 있습니다. 그래도 하드 코딩 값의 횟수가 ".NET System.Threading.Thread 개체에서 Native Thread Id를 구할 수 있을까?"에서 소개한 것보다 줄긴 했습니다. ^^

잘하면 이 하드 코딩 값을 없앨 수도 있지 않을까요? 가령, ^^ 두 번 정도 (원한다면 더 많이) 스레드를 생성해서 해당 옵셋값을 찾아내는 것을 생각해 볼 수 있습니다. 이런 기준을 가지고 다음의 클래스를 만들어 봤습니다.

public class OSThreadIdFinder
{
    const int _maxOffset = 2048; // 최대 2KB까지 thread id 탐색

    static int _osThreadIdOffset = 0;
    const int MUST_MATCH_COUNT = 2;

    [StructLayout(LayoutKind.Sequential)]
    public struct MEMORY_BASIC_INFORMATION
    {
        public IntPtr BaseAddress;
        public IntPtr AllocationBase;
        public uint AllocationProtect;
        public IntPtr RegionSize;
        public uint State;
        public uint Protect;
        public uint Type;
    }

    [DllImport("kernel32.dll")]
    static extern int VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, ref MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength);

    internal static bool Initialize()
    {
        int oldOffset = 0;
        int count = 0;

        int maxTry = 10;

        while (maxTry -- > 0) // (10 - 1)개의 스레드를 테스트하는 동안, 같은 위치의 thread id 옵셋 값을 연이어 MUST_MATCH_COUNT만큼 발견해야 한다.
        {
            int currentOffset = FindOffset();
            if (oldOffset == 0 || oldOffset != currentOffset)
            {
                count = 0;
                oldOffset = currentOffset;
                continue;
            }

            count ++;
            if (count == MUST_MATCH_COUNT)
            {
                _osThreadIdOffset = currentOffset;
                break;
            }
        }

        return _osThreadIdOffset != 0;
    }

    public class ThreadParam
    {
        public EventWaitHandle ChildWaitHandle { get; set; }
        public EventWaitHandle ParentWaitHandle { get; set; }
        public int ThreadId { get; set; }
    }

    public static int GetOsThreadId(Thread thread)
    {
        if (_osThreadIdOffset == 0)
        {
            return 0;
        }

        IntPtr ptr = GetThreadObjectAddress(thread);
        return Marshal.ReadInt32(ptr, _osThreadIdOffset);
    }

    private static int FindOffset()
    {
        EventWaitHandle ewhChild = new EventWaitHandle(false, EventResetMode.ManualReset);
        EventWaitHandle ewhParent = new EventWaitHandle(false, EventResetMode.ManualReset);

        Thread thread = new Thread(dummyFunc);

        ThreadParam threadParam = new ThreadParam { ChildWaitHandle = ewhChild, ParentWaitHandle = ewhParent };
        thread.Start(threadParam);
        ewhChild.WaitOne();

        IntPtr objValue = GetThreadObjectAddress(thread);
        if (objValue.ToInt64() == 0)
        {
            return 0;
        }

        IntPtr processHandle = Process.GetCurrentProcess().Handle;

        MEMORY_BASIC_INFORMATION mbi = new MEMORY_BASIC_INFORMATION();
        uint length = (uint)Marshal.SizeOf(mbi);
        int returnLength = VirtualQueryEx(processHandle, objValue, ref mbi, length);
        if (returnLength == 0)
        {
            return 0;
        }

        long pageLimit = mbi.BaseAddress.ToInt64() + mbi.RegionSize.ToInt64() - objValue.ToInt64();
        int maxOffset = (int)Math.Min((long)_maxOffset, pageLimit);

        try
        {
            for (int i = 0; i < maxOffset; i += 4)
            {
                int osThreadIdCandidate = Marshal.ReadInt32(objValue, i);
                if (osThreadIdCandidate == threadParam.ThreadId)
                {
                    return i;
                }
            }
        }
        catch { }
        finally
        {
            ewhParent.Set();
        }

        return 0;
    }

    private static IntPtr GetThreadObjectAddress(Thread thread)
    {
        try
        {
            FieldInfo fieldInfo = typeof(Thread).GetField("DONT_USE_InternalThread", BindingFlags.NonPublic | BindingFlags.Instance);
            if (fieldInfo == null)
            {
                return IntPtr.Zero;
            }

            return (IntPtr)fieldInfo.GetValue(thread);
        }
        catch
        {
            return IntPtr.Zero;
        }
    }

    private static void dummyFunc(object obj)
    {
        ThreadParam threadParam = obj as ThreadParam;
        if (threadParam == null)
        {
            return;
        }

        threadParam.ThreadId = AppDomain.GetCurrentThreadId();
        threadParam.ChildWaitHandle.Set();

        threadParam.ParentWaitHandle.WaitOne();
    }
}


이 클래스를 사용해 다음과 같이 OS Thread ID를 구할 수 있습니다.

OSThreadIdFinder.Initialize();

Thread newThread = new Thread(Run);
newThread.Start();

Console.WriteLine("OS Thrad ID: " + OSThreadIdFinder.GetOsThreadId(newThread));

이 정도면, 하드 코딩 값을 어느 정도는 없앴기 때문에 ".NET System.Threading.Thread 개체에서 Native Thread Id를 구할 수 있을까?" 글의 방식보다는 훨씬 안정적으로 Native Thread Id 값을 구할 수 있습니다.

뭐랄까... 좀 멋지게 말하면 thread id를 구하기 위해 machine learning을 집어 넣었다고 할 수도... (쿨럭!)




재미있는 것이 하나 더 있다면? DONT_USE_InternalThread 필드가 가리키는 C/C++ ThreadObject의 클래스 정의를 보면 ICLRTask 인터페이스를 상속받았음을 알 수 있습니다.

class Thread: public ICLRTask
{
    // ...[생략]...
}

C++/CLI의 도움을 받아 DONT_USE_InternalThread 필드의 IntPtr 값으로부터 인터페이스 타입으로 형변환을 하면 ICLRTask의 메서드를 호출하는 것이 가능합니다.

// ClassLibrary1.h

#pragma once

#include <mscoree.h>

using namespace System;
namespace ClassLibrary1 {

    public ref class Class1
    {
        public:
        
        __int64 LocksHeld(void *pTask)
        {
            ICLRTask *pClrTask = (ICLRTask *)pTask;

            SIZE_T lockCount = 0;
            pClrTask->LocksHeld(&lockCount);
            return lockCount;
        }

        __int64 GetPerThreadAllocation(void *pTask)
        {
            ICLRTask *pClrTask = (ICLRTask *)pTask;

            COR_GC_THREAD_STATS memUsage;

            int size = sizeof(COR_GC_THREAD_STATS);
            memset(&memUsage, 0, size);
            pClrTask->GetMemStats(&memUsage);

            return memUsage.PerThreadAllocation;
        }
    };
}

재미있는 시도이긴 한데, ICLRTask로 별다르게 구해올 수 있는 유용한 정보가 없습니다. 그나마 제가 선택한 것이 LocksHeld, GetMemStats였는데 아래와 같이 사용해 보면 사실상 우리가 원하는 그런 정보는 아닌 듯 합니다.

static object _lockClass = new object();

static void Main(string[] args)
{
    lock (_lockClass)
    { 
        IntPtr threadObjectAddress = OSThreadIdFinder.GetThreadObjectAddress(newThread);
        ClassLibrary1.Class1 cl = new ClassLibrary1.Class1(); // C++/CLI 클래스
        long curLocks = cl.LocksHeld(threadObjectAddress.ToPointer());

        Console.WriteLine("LocksHeld: " + curLocks); // 출력 결과: 0 (1을 기대했는데!)
    }

    unsafe
    {
        IntPtr threadObjectAddress = OSThreadIdFinder.GetThreadObjectAddress(newThread);
        ClassLibrary1.Class1 cl = new ClassLibrary1.Class1();
        long perThreadAllocation = cl.GetPerThreadAllocation(threadObjectAddress.ToPointer());

        Console.WriteLine("Old: " + perThreadAllocation); // 출력 결과: 16384

        GC.Collect();
        List<int> list = new List<int>();
        for (int i = 0; i < 1024; i++)
        {
            list.Add(i);
        }

        long newPerThreadAllocation = cl.GetPerThreadAllocation(threadObjectAddress.ToPointer());
        Console.WriteLine("New: " + newPerThreadAllocation);             // 출력 결과: 16384
        Console.WriteLine(newPerThreadAllocation - perThreadAllocation); // 출력 결과: 0 (변화를 기대했는데!)
    }
}




그 외에 제가 시도한 방법이 하나 더 있었는데요. Managed Thread의 DONT_USE_InternalThread로부터 TEB 값을 읽어들인 후 현재 실행 중인 모든 Native 스레드를 열람해서 같은 TEB 값을 찾는 것이었습니다. 일단 Native 스레드를 열람하는 코드에서,

foreach (ProcessThread item in Process.GetCurrentProcess().Threads)
{
    // ...[item.Id == Native 스레드 ID]...               
}

스레드 ID로로부터 Thread Handle 값을 얻어내고, 이후 NtQueryInformationThread API를 이용해 TEB 값을 구할 수 있습니다. 최종 코드는 다음과 같습니다.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        [DllImport("ntdll.dll", SetLastError = true, EntryPoint = "NtQueryInformationThread")]
        static extern int NtQueryInformationThread(IntPtr pHandle, _THREAD_INFORMATION_CLASS infoClass, ref ThreadBasicInformation instance, int sizeOfInstance, out int length);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, int dwThreadId);

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool CloseHandle(IntPtr hObject);

        static void Main(string[] args)
        {
            Thread newThread = new Thread(Run);
            newThread.Start();

            foreach (ProcessThread item in Process.GetCurrentProcess().Threads)
            {
                IntPtr pHandle = OpenThread(ThreadAccess.QUERY_INFORMATION, false, item.Id);

                Console.Write("TID: " + item.Id + ", ");

                int sizeOfTbi = 0;
                int length = 0;
                int result = 0;
                int error = 0;

                ThreadBasicInformation tbi = new ThreadBasicInformation();
                sizeOfTbi = Marshal.SizeOf(tbi);

                result = NtQueryInformationThread(pHandle, _THREAD_INFORMATION_CLASS.ThreadBasicInformation, ref tbi, sizeOfTbi, out length);
                if (result == 0)
                {
                    Console.WriteLine("TEB: " + tbi.TebBaseAddress.ToString("x"));
                }
                else
                {
                    error = Marshal.GetLastWin32Error();
                }

                if (error != 0)
                { 
                    Console.WriteLine("Failed: " + error);
                }
                CloseHandle(pHandle);
            }
        }

        static void Run()
        {
            while (true)
            {
                Thread.Sleep(1000);
            }
        }
    }

    [Flags]
    public enum ThreadAccess : int
    {
        TERMINATE = (0x0001),
        SUSPEND_RESUME = (0x0002),
        GET_CONTEXT = (0x0008),
        SET_CONTEXT = (0x0010),
        SET_INFORMATION = (0x0020),
        QUERY_INFORMATION = (0x0040),
        SET_THREAD_TOKEN = (0x0080),
        IMPERSONATE = (0x0100),
        DIRECT_IMPERSONATION = (0x0200),
    }

    public enum _THREAD_INFORMATION_CLASS
    {
        ThreadBasicInformation,
        ThreadTimes,
        ThreadPriority,
        ThreadBasePriority,
        ThreadAffinityMask,
        ThreadImpersonationToken,
        ThreadDescriptorTableEntry,
        ThreadEnableAlignmentFaultFixup,
        ThreadEventPair,
        ThreadQuerySetWin32StartAddress,
        ThreadZeroTlsCell,
        ThreadPerformanceCount,
        ThreadAmILastThread,
        ThreadIdealProcessor,
        ThreadPriorityBoost,
        ThreadSetTlsArrayAddress,
        ThreadIsIoPending,
        ThreadHideFromDebugger
    }

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct ClientId
    {
        IntPtr UniqueProcess;
        IntPtr UniqueThread;
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct ThreadBasicInformation
    {
        public int ExitStatus;
        public IntPtr TebBaseAddress;
        public ClientId ClientId;
        public IntPtr AffinityMask; // x86 == 4, x64 == 8
        public int Priority;
        public int BasePriority;
    }
}

참고로, NtQueryInformationThread은 ntdll.dll에서 export하고 있는 API인데 C/C++에서는 이렇게 코딩할 수 있습니다.

#pragma comment(lib,"ntdll.lib")

typedef long NTSTATUS;
typedef long KPRIORITY;

typedef struct _CLIENT_ID
{
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
} CLIENT_ID, *PCLIENT_ID;

typedef struct _THREAD_BASIC_INFORMATION
{
    NTSTATUS                ExitStatus;
    PVOID                   TebBaseAddress;
    CLIENT_ID               ClientId;
    KAFFINITY               AffinityMask;   // KAFFINITY ==> ULONG_PTR ==> unsigned __int64
    KPRIORITY               Priority;       // KPRIORITY ==> LONG ==> long (C++)
    KPRIORITY               BasePriority;
} THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;

typedef enum _THREADINFOCLASS
{
    ThreadBasicInformation,
    ThreadTimes,
    ThreadPriority,
    ThreadBasePriority,
    ThreadAffinityMask,
    ThreadImpersonationToken,
    ThreadDescriptorTableEntry,
    ThreadEnableAlignmentFaultFixup,
    ThreadEventPair_Reusable,
    ThreadQuerySetWin32StartAddress,
    ThreadZeroTlsCell,
    ThreadPerformanceCount,
    ThreadAmILastThread,
    ThreadIdealProcessor,
    ThreadPriorityBoost,
    ThreadSetTlsArrayAddress,
    ThreadIsIoPending,
    ThreadHideFromDebugger,
    ThreadBreakOnTermination,
    MaxThreadInfoClass
}   THREADINFOCLASS;

extern "C"
{
    NTSTATUS WINAPI NtQueryInformationThread(
        _In_       HANDLE ThreadHandle,
        _In_       THREADINFOCLASS ThreadInformationClass,
        _Inout_    PVOID ThreadInformation,
        _In_       ULONG ThreadInformationLength,
        _Out_opt_  PULONG ReturnLength
        );

}

THREAD_BASIC_INFORMATION ThreadInfo;
DWORD ntstatus = NtQueryInformationThread(
    GetCurrentThread(), 
    ThreadBasicInformation,
    &ThreadInfo,
    sizeof(THREAD_BASIC_INFORMATION),
    0);

마이크로소프트에서는 kernel32.dll을 통해 NtQueryInformationThread 대신 GetThreadInformation API를 제공하고 있는데요.

GetThreadInformation function
; https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadinformation

아쉽게도 문서에 설명된 것처럼 ThreadInformationClass로 전달할 수 있는 인자가 ThreadMemoryPriority로 제한되기 때문에 ThreadBasicInformation 값으로는 오류가 반환됩니다.

int size = sizeof(THREAD_BASIC_INFORMATION);
THREAD_BASIC_INFORMATION threadInfo;

// 아래의 API는 동작하지 않음.
BOOL Success = GetThreadInformation(GetCurrentThread(),
    (THREAD_INFORMATION_CLASS)0,
    &threadInfo,
    sizeof(threadInfo));

DWORD dwError = ::GetLastError();

// 이렇게 ThreadMemoryPriority로만 사용이 제한됨.
//MEMORY_PRIORITY_INFORMATION MemPrio;
//Success = GetThreadInformation(GetCurrentThread(),
//    ThreadMemoryPriority,
//    &MemPrio,
//    sizeof(MemPrio));

아직은 NtQueryInformationThread API가 비공식이지만 사용법에 대해서는 low-level 작업하는 분야에서는 제법 사용되고 있으므로 마이크로소프트 측에서도 쉽게 바꿀 수는 없을 것입니다. ^^

스레드 열거하기
; http://www.jiniya.net/wp/archives/7676

_THREAD_BASIC_INFORMATION Struct Reference
; http://processhacker.sourceforge.net/doc/struct___t_h_r_e_a_d___b_a_s_i_c___i_n_f_o_r_m_a_t_i_o_n.html

허긴... 그렇다고는 해도 ntdll.dll에 있던 NtCurrentTeb API의 경우 32비트에만 제공되었고 64비트에서는 삭제되는 이변이 발생하긴 했습니다.

암튼, 위의 방법은 .NET Thread 개체로부터 TEB 주소를 구할 때 사용하면 좋을 것 같습니다. 만약 다른 스레드가 아닌 현재 스레드의 TEB를 구하는 것이라면 다음의 방법도 고려해 볼 수 있겠고. ^^

.NET x64 응용 프로그램에서 Teb 주소를 구하는 방법
; https://www.sysnet.pe.kr/2/0/1388





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

[연관 글]






[최초 등록일: ]
[최종 수정일: 5/18/2023]

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

비밀번호

댓글 작성자
 



2017-01-22 06시06분
글의 중간에 !threads 명령의 출력 결과로 나온 ThreadObj 값에서 곧바로 System.Threading.Thread 인스턴스를 얻는 방법

Getting the managed System.Threading.Thread instance for a native thread object (ThreadObj)
; http://blog.steveniemitz.com/getting-the-managed-system-threading-thread-instance-for-a-native-thread-object-threadobj/

x64의 경우 +0x228, x86의 경우 x0160
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13601정성태4/19/2024196닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/2024276닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/2024300닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/2024335닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드1
13597정성태4/15/2024396닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/2024774닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/2024895닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20241016닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241050닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241207C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241168닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241073Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241143닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241197닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신파일 다운로드1
13587정성태3/27/20241156오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241297Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241096Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241048개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241158Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241420Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/20241587개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20241137닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/20241494오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20241629닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20241875닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...