본문 바로가기

.NET/Debugging

.NET 쓰레드 콜 스택 덤프 (5)


http://blog.naver.com/techshare/100152100434

에서 퍼온 글입니다.

 
.NET 쓰레드 콜 스택 덤프 (5) - ICorDebug 인터페이스 사용법 개발자

2012/02/26 01:32

복사 http://blog.naver.com/techshare/100152100434

.NET 쓰레드 콜 스택 덤프 (1) - 다른 쓰레드의 스택 덤프 구하는 방법
.NET 쓰레드 콜 스택 덤프 (2) - Managed Stack Explorer 소스 코드를 이용한 스택 덤프 구하는 방법
.NET 쓰레드 콜 스택 덤프 (3) - MSE 소스 코드 개선
.NET 쓰레드 콜 스택 덤프 (4) - .NET 4.0을 지원하지 않는 MSE 응용 프로그램 원인 분석
.NET 쓰레드 콜 스택 덤프 (5) - ICorDebug 인터페이스 사용법

닷넷에서 쓰레드 호출 스택을 얻으려고 하는 시도를 하다가 어느 새 여기까지 와버렸군요. ^^; 지난 마지막 이야기에서 "mscordbi.dll"을 직접 사용하는 방법을 다뤄보겠다고 하면서 맺었는데... 진작에 테스트를 했었으나, 시간이 없어 미뤄오다가 이제서야 정리를 해서 공개를 합니다.

자... 그럼 이번에도 그다지 재미없는 글이 되겠지만 ^^ 본론으로 들어가 보도록 하겠습니다.




문제를 다시 정리해 보면, 현재 MSE 의 소스 코드로는 .NET 2.0 응용 프로그램의 Call Stack 만을 얻을 수 있을 뿐 .NET 4.0 의 콜 스택을 얻지 못한다는 단점이 있었습니다. 다행히 .NET 4.0 의 콜 스택을 얻는 방법으로 그 힌트를 CLR Stack Explorer 가 .NET 4.0 응용 프로그램에서도 동작한다는 것에서 얻었는데, 그래서 이번에는 CLR Stack Explorer 처럼 직접 mscordbi.dll 에서 제공되는 기능만으로 문제 해결을 해보려는 것입니다.

비록 MSE 응용 프로그램에서 예외는 ICorPublish에서 발생했지만, 사실 ICorPublish 의 기능 자체는 콜 스택 덤프를 뜨는 것과는 무관합니다. 정작 중요한 것은 ICorDebug 인터페이스인데요.

이에 대한 간략한 사용법은 다음의 예제에 공개되어 있습니다.

How to get a V2.0 ICorDebug object
; http://blogs.msdn.com/b/jmstall/archive/2005/01/15/353717.aspx

ICorDebug re-architecture in CLR 4.0
; http://blogs.msdn.com/b/rmbyers/archive/2008/10/27/icordebug-re-architecture-in-clr-4-0.aspx

처음 시도에서는 위의글에 설명된 CoCreateInstance로 직접 ICorDebug 를 생성해 보았습니다.

#include <cor.h>
#include <corhdr.h>
#include <cordebug.h>

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

    CoInitialize(NULL);

    {
        ICorDebug *pCorDebug;

        HRESULT hr = ::CoCreateInstance(CLSID_CorDebug, NULL, CLSCTX_INPROC_SERVER, IID_ICorDebug,
            (void **)&pCorDebug);
        if (hr != S_OK)
        {
            return 0;
        }
    }

    CoUninitialize();

문제가 대번에 나오더군요. Visual Studio의 출력창에서 .NET 2.0 용의 mscordbi.dll 파일이 로드가 되는 것을 확인할 수 있었습니다.

'ConsoleApp.exe': Loaded 'C:\Windows\SysWOW64\clbcatq.dll', Cannot find or open the PDB file
'ConsoleApp.exe': Loaded 'C:\Windows\SysWOW64\mscoree.dll', Cannot find or open the PDB file
'ConsoleApp.exe': Loaded 'C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscoreei.dll', Cannot find or open the PDB file
'ConsoleApp.exe': Loaded 'C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscordbi.dll', Cannot find or open the PDB file

이전에 설명했던 것처럼 ICorDebug 등에 대한 Class Factory는,

EXTERN_C const IID LIBID_CORDBLib;

EXTERN_C const CLSID CLSID_CorDebug;

#ifdef __cplusplus

class DECLSPEC_UUID("6fef44d0-39e7-4c77-be8e-c9f8cf988630")
CorDebug;

mscoree.dll에 정의되어 있습니다. 결국 mscoree.dll 측에서 적절하지 않은 버전의 mscordbi.dll을 로드한다는 것인데요. 따라서, mscoree.dll을 경유하지 말고 직접 mscordbi.dll을 로드하는 방법을 찾아야 했습니다. 일단 mscoree.dll에서 CoClass를 제공하는 이상 mscordbi.dll 자체에서는 숨겨진 다른 방식으로 인터페이스를 노출시켜 준다는 의미가 되는데요. Depends 도구를 이용하여 mscordbi.dll을 살펴보니, 친절하게도 CreateCordbObject 라는 이름으로 된 함수가 export 되어 있었습니다.



다행히, API 설명까지 공개되어 있군요. ^^

CreateCordbObject Function
; http://msdn.microsoft.com/en-us/library/cc994509.aspx

종합해서, 다음과 같이 특정 버전의 mscordbi.dll 로드가 가능했습니다.

===== C/C++ 버전 =====

typedef HRESULT (__stdcall *CordbCreateObjectFunc)(int iDebuggerVersion, IUnknown **ppCordb);

CComQIPtr<ICorDebug> pCorDebug;
HMODULE hModule = ::LoadLibrary(L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\mscordbi.dll");
if (hModule == NULL)
{
    break;
}

CordbCreateObjectFunc pCordbCreateObjectFunc = (CordbCreateObjectFunc)::GetProcAddress(hModule,"CreateCordbObject");

IUnknown *pUnk;
hr = pCordbCreateObjectFunc(CorDebugVersion_4_0, (IUnknown **)&pUnk);
if (hr != S_OK)
{
    break;
}

pCorDebug = pUnk;
if (pCorDebug == NULL)
{
    break;
}

===== C# 버전 =====
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr LoadLibrary(string lpFileName);

[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate int CordbCreateObjectFunc(int iDebuggerVersion, [Out, MarshalAs(UnmanagedType.Interface)] out object pUnknown);

IntPtr pModule = LoadLibrary("C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\mscordbi.dll");
if (pModule == null)
{
    return;
}

IntPtr pFunc = GetProcAddress(pModule, "CreateCordbObject");
if (pFunc == IntPtr.Zero)
{
    return;
}

CordbCreateObjectFunc pCordbCreateObjectFunc = Marshal.GetDelegateForFunctionPointer(pFunc, typeof(CordbCreateObjectFunc))
    as CordbCreateObjectFunc;
if (pCordbCreateObjectFunc == null)
{
    return;
}

// object pUnknown;
pCordbCreateObjectFunc(4, out pUnknown);

ICorDebug corDebug = pUnknown as ICorDebug;

참고로, 현재 CorDebugVersion_4_0 상수값에 대해서는 어떠한 웹 문서에도 찾아볼 수가 없었는데, 최신 버전의 Windows SDK 에 포함된 cordebug.idl 파일을 직접 검색해 보면 다음과 같이 정의되어 있는 것을 볼 수 있습니다.

E:\Program Files\Microsoft SDKs\Windows\v7.1\Include\cordebug.idl

    CorDebugInvalidVersion = 0,
    CorDebugVersion_1_0 = CorDebugInvalidVersion + 1,  // 1
    CorDebugVersion_1_1 = CorDebugVersion_1_0 + 1,     // 2
    CorDebugVersion_2_0 = CorDebugVersion_1_1 + 1,     // 3
    // CLR v4 - next major CLR version after CLR v2
    // Includes Silverlight 4
    CorDebugVersion_4_0 = CorDebugVersion_2_0 + 1,     // 4

실제로 이렇게 해서 로드를 해보면, 정확하게 4.0 버전의 mscordbi.dll 만 Visual Studio 출력창에 보입니다.

'ConsoleApp.exe': Loaded 'C:\Windows\System32\version.dll', Cannot find or open the PDB file
IMGSF01-DLL_PROCESS_ATTACH'ConsoleApp.exe': Loaded 'C:\Windows\System32\ntmarta.dll', Cannot find or open the PDB file
'ConsoleApp.exe': Loaded 'C:\Windows\System32\Wldap32.dll', Cannot find or open the PDB file
'ConsoleApp.exe': Loaded 'C:\Windows\System32\msimg32.dll', Cannot find or open the PDB file
'ConsoleApp.exe': Loaded 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscordbi.dll', Cannot find or open the PDB file
'ConsoleApp.exe': Loaded 'C:\Windows\System32\wtsapi32.dll', Cannot find or open the PDB file

성공입니다. ^^




mscordbi.dll 파일 경로를 직접 쓴다는 것은 아무래도 부담스러운 데요. 이 때문에 How to get a V2.0 ICorDebug object 글에서 설명한 CreateDebuggingInterfaceFromVersion 함수를 사용해 보았습니다.

CreateDebuggingInterfaceFromVersion Function
; http://msdn.microsoft.com/en-us/library/ms232068.aspx
(.NET 4.0 에서는 "obsolete" 로 명시되었지만, 여전히 동작합니다.)

처음에는 다음과 같이 사용해 보았는데요.

CComPtr<ICorDebug> pCorDebug;
hr = ::CreateDebuggingInterfaceFromVersion(CorDebugVersion_4_0, L"", (IUnknown **)&pCorDebug);

아쉽게도 hr == E_INVALIDARG 오류가 발생해서, 문서를 자세히 보니 두번째 szDebuggeeVersion 인자를 주어야 한다는 것을 알았습니다. 역시 문서에 보면 여기에 넘겨줄 문자열은 GetVersionFromProcessGetRequestedRuntimeVersion API 에서 구할 수 있다고 되어 있는데, 제 상황에서는 GetVersionFromProcess를 실행해 보았으나 "" 문자열만 반환되었습니다. (참고로, 문서에 보면 .NET 4.0 의 경우 GetVersionFromProcess API역시 "deprecated" 되었습니다.)

// 아래의 코드는 .NET 4.0 용에서 동작하지 않음.
wchar_t versionText[1024] = { 0 };
DWORD dwVersionLength = 0;
hr = GetVersionFromProcess((HANDLE)processHandle, versionText, 1024, &dwVersionLength);

// 그래서, .NET 4.0에 해당하는 문자열을 넣어주는 것으로 해결
hr = ::CreateDebuggingInterfaceFromVersion(CorDebugVersion_4_0, L"v4.0.30319", (IUnknown **)&pCorDebug);

==== C# 버전 ====
[DllImport("MSCorEE.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
static extern object CreateDebuggingInterfaceFromVersion(int iDebuggerVersion, string szDebuggerVersion);

ICorDebug corDebug = CreateDebuggingInterfaceFromVersion(4, "v4.0.30319") as ICorDebug;

어쨌든, Visual Studio 의 출력창에 로드된 DLL 을 확인해 보면, 정확하게 .NET 4.0 용의 mscordbi.dll 이 로드된 것을 확인할 수 있습니다.




ICorDebug 를 얻었으니, 이로부터 다음과 같이 ICorDebugProcess 인터페이스를 얻어올 수 있습니다.

CComPtr<ICorDebugProcess> pCorDebugProcess;
hr = pCorDebug->DebugActiveProcess(dwPid, true, &pCorDebugProcess);
if (hr != S_OK)
{
    break;
}

하지만, 세상일이 그렇게 간단하지 않더군요. ^^ 실제로 위와 같이 호출해 보면 다음과 같은 예외가 발생합니다.

hr == E_FAIL

First-chance exception at 0x000007fefe18cacd in ConsoleApp.exe: Microsoft C++ exception: HRException * __ptr64 at memory location 0x002ff558..
First-chance exception at 0x000007fefe18cacd in ConsoleApp.exe: Microsoft C++ exception: [rethrow] at memory location 0x00000000..
First-chance exception at 0x000007fefe18cacd in ConsoleApp.exe: Microsoft C++ exception: HRException * __ptr64 at memory location 0x002ff558..

ICorDebug를 처음 사용해 보는 저 같은 초보 프로그래머가 흔히 겪는 실수일 텐데요. ICorDebugProcess를 얻기 전에, 반드시 콜백 핸들러를 지정해 주어야만 저런 예외가 발생하지 않습니다.

그래서, 다른 소스 코드를 참조한 결과 일반적으로 다음과 같은 수순으로 연결되는 것을 볼 수 있었습니다.

pCorDebug->SetManagedHandler(this); // ICorDebugManagedCallback 를 구현한 인스턴스를 설정

hr = pCorDebug->CanLaunchOrAttach(processId, false);
if (hr != S_OK)
{
    break;
}

CComPtr<ICorDebugProcess> pCorDebugProcess;
hr = pCorDebug->DebugActiveProcess(processId, false, &pCorDebugProcess);
if (hr != S_OK)
{
    break;
}

일단, 위와 같이 ICorDebugProcess로 대상 프로세스에 Attach 를 정상적으로 시키면, 이제 현재 생성되어 있는 쓰레드 목록을 얻어야 하는데요. 이 작업은 ICorDebugController 를 얻음으로써 가능합니다.

CComPtr<ICorDebugProcess> pCorDebugProcess;
hr = pCorDebug->DebugActiveProcess(dwPid, false, &pCorDebugProcess);
if (hr != S_OK)
{
    break;
}

pCorDebugProcess->Continue(FALSE);

bool exit = false;
CComQIPtr<ICorDebugController> pCorDebugController = pCorDebugProcess;

ICorDebugController::EnumerateThreads 메서드를 이용하여 쓰레드를 열람할 수 있는데, 주의할 것은 곧바로 이 상태로 진입해서는 안된다는 것입니다. ICorDebugManagedCallback::CreateThread 이벤트 단계와 협업을 해야 하는데, 이 부분은 MSE 소스 코드를 보고서야 알게 된 사항이어서 이로 인한 문제를 해결하기까지 처음에 시간이 많이 걸렸습니다.

즉, 다음과 같이 WaitForSingleObject 등의 수단을 이용해서 기다렸다가 쓰레드를 열람해야 하는데,

::WaitForSingleObject(_threadStartEventHandle, INFINITE);

CComPtr<ICorDebugThreadEnum> pThreadsEnum;
hr = pCorDebugController->EnumerateThreads(&pThreadsEnum);
if (hr != S_OK)
{
    exit = true;
    break;
}

이에 대한 동기화 이벤트는 다음과 같이 ICorDebugManagedCallback::CreateThread 단계를 구성해서 처리를 해줘야 합니다.

virtual HRESULT STDMETHODCALLTYPE CreateThread(ICorDebugAppDomain *pAppDomain, ICorDebugThread *thread) 
{
    HRESULT hr;

    do
    {
        CComPtr<ICorDebugProcess> pProcess;
        hr = thread->GetProcess(&pProcess);
        if (hr != S_OK || pProcess == NULL)
        {
            break;
        }

        BOOL bQueued = FALSE;
        hr = pProcess->HasQueuedCallbacks(NULL, &bQueued);
        if (hr != S_OK)
        {
            break;
        }

        if (bQueued == FALSE)
        {
            ((CManagedStackTrace *)this)->SignalAttachedProcess(); // ::SetEvent(_threadStartEventHandle);
            return S_OK;
        }
        else
        {
            pAppDomain->Continue(FALSE);
            return S_OK;
        }

    } while (false);

    ((CManagedStackTrace *)this)->SignalAttachedProcess();  // ::SetEvent(_threadStartEventHandle);

    return S_OK;
}

솔직히, 위와 같은 처리 절차는 그냥은 알기 힘들고 MSE 소스 코드가 있었기 때문에 가능한 것 같습니다. 참고로, HasQueuedCallbacks 에 대한 처리는 다음의 문서를 참조하십시오.

Using ICorDebugProcess::HasQueuedCallbacks
; http://blogs.msdn.com/b/jmstall/archive/2005/07/27/hasqueuedcallbacks.aspx

그 다음, ICorDebugManagedCallback 의 중요 이벤트마다 적절하게 Continue 메서드를 호출해 주어야 하는 문제가 있습니다. 이를 몰랐을 때는, 처리하는 중에 다음과 같이 ICorDebugManagedCallback::DebuggerError 에서 오류를 만나서 당황했었는데요.

virtual HRESULT STDMETHODCALLTYPE DebuggerError(ICorDebugProcess *pProcess, HRESULT errorHR, DWORD errorCode)  
{
    // CORDBG_E_INTEROP_NOT_SUPPORTED 0x8013134D (-2146233523) 오류 발생
    return E_NOTIMPL;
}

다음과 같은 콜백 메서드에 대해서 S_OK 반환과 Continue 메서드를 호출해 주어야만 했습니다. (역시 MSE 소스 코드를 보고 알았습니다.)

virtual HRESULT STDMETHODCALLTYPE CreateProcess(ICorDebugProcess *pProcess) 
{
    pProcess->Continue(FALSE);
    return S_OK;
}

virtual HRESULT STDMETHODCALLTYPE LoadModule(ICorDebugAppDomain *pAppDomain, ICorDebugModule *pModule)  
{
    pAppDomain->Continue(FALSE);
    return S_OK;
}

virtual HRESULT STDMETHODCALLTYPE CreateAppDomain(ICorDebugProcess *pProcess, ICorDebugAppDomain *pAppDomain) 
{
    pAppDomain->Attach();
    pProcess->Continue(FALSE);
    return S_OK;
}

virtual HRESULT STDMETHODCALLTYPE DebuggerError(ICorDebugProcess *pProcess, HRESULT errorHR, DWORD errorCode)  
{
    pProcess->Continue(FALSE);
    return S_OK;
}

또 한가지 주의할 점은 위에서 pProcess->Continue 등의 함수에서 TRUE/FALSE 값은 fIsOutOfBand 값으로 나오는데요. 이에 대해서는 다음의 MSDN 문서를 봐야 합니다.

ICorDebugController::Continue Method
; http://msdn.microsoft.com/en-us/library/ms231588.aspx

Continue continues the process after a call to the ICorDebugController::Stop method.

When doing mixed-mode debugging, do not call Continue on the Win32 event thread unless you are continuing from an out-of-band event.

An in-band event is either a managed event or a normal unmanaged event during which the debugger supports interaction with the managed state of the process. In this case, the debugger receives the ICorDebugUnmanagedCallback::DebugEvent callback with its fOutOfBand parameter set to false.
An out-of-band event is an unmanaged event during which interaction with the managed state of the process is impossible while the process is stopped due to the event. In this case, the debugger receives the ICorDebugUnmanagedCallback::DebugEvent callback with its fOutOfBand parameter set to true.


결국 Continue 인자에 정상적인 TRUE/FALSE 값을 넘겨주려면 ICorDebugUnmanagedCallback 도 구현해 주어야 하고, 위에서 ICorDebug::SetManagedHandler 로 ICorDebugManagedCallback를 등록해 주었던 것처럼 ICorDebug::SetUnmanagedHandler 로 ICorDebugUnmanagedCallback도 등록해서 처리해 주어야 합니다.

일단, 제 경우에는 테스트 수준이다 보니 무조건 FALSE로 넘겼으나... 나중에 그거 마저 구현을 한 소스를 기회가 되면 올리겠습니다.

이 정도만 했으면 일단 .NET 4.0 응용 프로그램에 대해서도 정상적으로 Call Stack 을 얻을 수 있습니다. 나머지 소스 코드는 MSE C# 버전의 작업들을 지리하게 C/C++ 코드로 다시 작성한 코드들의 나열이기 때문에 이 글에서는 생략했지만, 첨부된 소스 코드에는 넣어두었으므로 참고하시기 바랍니다.

마지막으로, 제가 이번 테스트를 진행하면서 어려움을 겪은 것이 하나 더 있었는데 이를 말씀드려야 할 것 같습니다. ^^

위의 소스 코드로 작업을 하는데, 때로는 정상적으로 동작하다가도 어쩌다가 ICorDebug::DebugActiveProcess 를 호출했을 때 반환값이 hr == 0x8013132e 으로 나오는 이상한 문제가 발생했습니다.

검색도 해보았지만, 건수가 하나만 나올 뿐 답이 없더군요. ^^;

Error: attaching for debug to external process 
; http://community.sharpdevelop.net/forums/p/8929/24848.aspx

훗날, 저처럼 ICorDebug 가지고 놀다가 이런 시행 착오를 거치지 않으시길 바라면서 정리하자면... ^^;

바로, 대상 응용 프로그램이 이미 디버거에 붙어(Attach)있는 상태인 경우에 발생합니다. 제 경우에는, ICorDebug::DebugActiveProcess 를 호출하는 응용 프로그램을 디버깅하는 목적으로 그 자신의 콜 스택을 남기도록 스스로 프로세스를 재생성해서 테스트 하는 경우였습니다.

2개의 디버거가 하나의 EXE 프로세스를 제어할 수 없다는 것을 생각하면 응당 이해는 되지만... 습관대로 디버거를 시작했다가 저런 결과가 나오면 오류가 무엇인지 감이 안 잡히더군요. ^^ 어쨌든, 테스트 하실 때 이미 Visual Studio 로 디버깅 중인 프로세스를 다시 DebugActiveProcess로 테스트 하는 실수는 하지 마시기 바랍니다.

그나저나... 꽤나 쉽지 않은 분야인 것 같습니다. 디버깅이란 분야는!


보내기

덧글 쓰기 엮인글

닷넷개발 주식회사에이하나
닷넷플랫폼, 시스템개발, 오라클, SQL, 자바, 시스템통합, 웹서비스개발
www.a-hana.co.kr
MDS테크놀로지
임베디드 시스템 개발, 디버거, TRACE32, ARM, MIPS.
www.trace32.com
응용프로그램젠시스웍
원도우프로그램, 모바일어플, 웹사이트개발, 통신, 제어프로그램 전문, 다수구축사례
www.gensys.co.kr
Ads by | 가입신청