사용자 도구

사이트 도구


kb:minidump

MiniDump

미니 덤프 파일은 크래시된 애플리케이션에 대한 가장 중요한 정보(스택 트레이스, 힙 정보…)만을 가지고 있는 파일이다. 사용자의 컴퓨터에서 애플리케이션이 다운되면서 미니 덤프를 생성하면, 사용자는 그것을 개발자에게 보낼 수 있다. 그러면 개발자는 그 덤프 파일을 이용해 버그를 잡는 것이다.

준비해야할 것들

  • 버전 5.1.2600 이상의 dbghelp.dll - 버전이 낮은 DLL에는 미니 덤프를 위해서 사용하는 Mini``Dump``Write``Dump 함수가 존재하지 않기 때문이다. 윈도우즈 2000의 경우, 디폴트로 존재하는 dbghelp.dll은 버전이 오래된 것이므로 주의해야한다. 여러 가지 문제를 피하기 위해서, 실행 파일이 있는 디렉토리에 같이 두는 것을 추천한다.
  • 해당 버전의 애플리케이션 바이너리와 심볼(PDB), OS 바이너리와 심볼 - SymbolServer 페이지를 참고하기 바란다.

미니 덤프 남기는 방법

아래에 있는 MiniDump.h와 MiniDump.cpp 두 파일을 프로젝트에다 집어넣은 다음, 메인 루프를 초기화하기 이전에 다음 코드를 집어넣어주면 된다. dbghelp 라이브러리 링크 해주는 거 까먹으면 낭패.

#pragma comment(lib, "dbghelp")
...
// 미니 덤프 초기화
cMiniDump::Install(cMiniDump::DUMP_LEVEL_LIGHT);
...

VisualCpp 8.0 기준으로 제작된 소스라, 낮은 버전에서 컴파일하고자 하는 경우, printf 류 함수들에서 에러가 발생할 것이다. ifdef로 감싸니까, 소스가 너무 지저분해져서, 하위 호환성은 포기했다. -_-

덤프를 남길 때, 콜스택 정보도 기록하도록 하고 있는데, 이는 여기에 있는 소스를 이용한다. 같이 첨부하는 것도 웃기는 일이라서 ifdef로 따로 빼놨다. HAS_STACK_WALKER 매크로 주위를 살펴보면 될 것이다.

MiniDump.h
////////////////////////////////////////////////////////////////////////////////
/// \file MiniDump.h
/// \author excel96
/// \date 2003.11.18
////////////////////////////////////////////////////////////////////////////////
 
#pragma once
 
#ifndef _WINDOWS_
    #include <windows.h>
#endif
 
////////////////////////////////////////////////////////////////////////////////
/// \class cMiniDump
/// \brief 미니 덤프를 실행하기 위한 클래스. 특별히 어떤 당위성이 있어서 만든 
/// 클래스는 아니고, 정적 변수와 함수를 가지고 있기 위해 만든 네임 스페이스 
/// 역할 클래스이다. 
///
/// 덤프의 초기화는 Install 함수를 통해 이루어진다. 프로그램 시작 부분 아무 
/// 곳에서나 cMiniDump::Install(...) 함수를 호출해주면 된다. 
///
/// <pre>
/// int main()
/// {
///     ...
///     cMiniDump::Install(cMiniDump::DUMP_LEVEL_LIGHT);
///     ...
/// }
/// </pre>
///
/// GUI를 사용할 수 있는 프로그램의 경우, 사용자에게 덤프 파일 생성 여부를 묻는
/// 것도 괜찮다고 생각해서, 대화창 콜백 함수를 하나 집어 넣었다. Install 함수를 
/// 통해 대화창 콜백 함수를 설정하면, 덤프 생성시 이를 확인해 먼저 대화창을 
/// 띄운다. 대화창이 IDOK로 끝나면, 덤프 파일을 생성하고, 그외의 값으로 끝나면 
/// 덤프 파일을 생성하지 않는다. 기본적인 대화창 함수를 구현하자면 대충 아래와 
/// 같을 것이다.
///
/// <pre>
/// BOOL CALLBACK CrashDialogProc(
///     HWND hDlg, UINT iMessage, WPARAM wParam, LPARAM /*lParam*/)
/// {
///     RECT parent, dlg;
///     int x, y;
/// 
///     switch (iMessage)
///     {
///     case WM_INITDIALOG:
///         ::GetWindowRect(::GetForegroundWindow(), &parent);
///         ::GetWindowRect(hDlg, &dlg);
///         x = (parent.left + parent.right - dlg.right) / 2;
///         y = (parent.top + parent.bottom - dlg.bottom) / 2;
///         ::MoveWindow(hDlg, x, y, dlg.right, dlg.bottom, FALSE);
//          ...
///         return TRUE;
///     case WM_COMMAND:
///         switch (LOWORD(wParam))
///         {
///         case IDOK: EndDialog(hDlg, IDOK); break;
///         case IDCANCEL: EndDialog(hDlg, IDCANCEL); break;
///         default: break;
///         }
///         return FALSE;
///     default:
///         break;
///     }
/// 
///     return FALSE;
/// }
/// </pre>
////////////////////////////////////////////////////////////////////////////////
 
class cMiniDump
{
public:
    /// 덤프할 데이터의 수준
    enum DumpLevel
    {
        DUMP_LEVEL_LIGHT,  ///< MiniDumpNormal을 사용
        DUMP_LEVEL_MEDIUM, ///< MiniDumpWithDataSegs를 사용
        DUMP_LEVEL_HEAVY   ///< MiniDumpWithFullMemory를 사용
    };
 
 
private:
    static DumpLevel s_DumpLevel;          ///< 덤프 레벨.
    static bool      s_AddTimeStamp;       ///< 날짜 기반 덤프 파일 이름 사용
    static TCHAR     s_AppName[_MAX_PATH]; ///< 덤프 파일 이름
    static TCHAR     s_CallStack[8192];    ///< 콜스택 문자열
    static TCHAR     s_Modules[8192];      ///< 모듈 문자열
    static LPCTSTR   s_DialogTemplate;     ///< 대화창 템플릿
    static DLGPROC   s_DialogProc;         ///< 대화창 프로시져
 
 
public:
    /// \brief 미니 덤프 기능을 초기화한다.
    static void Install(DumpLevel dumpLevel, bool addTimestamp=true, 
        LPCTSTR dialogTemplate=NULL, DLGPROC dialogProc=NULL);
 
    /// \brief 콜스택 문자열을 반환한다.
    static LPCTSTR GetCallStack() { return s_CallStack; }
 
    /// \brief 모듈 문자열을 반환한다.
    static LPCTSTR GetModules() { return s_Modules; }
 
 
private:
    /// \brief 예외에 대한 정보를 받아서, 미니 덤프 파일을 생성한다. 
    static LONG WINAPI WriteDump(PEXCEPTION_POINTERS exPtrs);
 
    /// \brief 생성 금지
    cMiniDump() {}
 
    /// \brief 복사 생성 금지
    cMiniDump(const cMiniDump&) {}
 
    /// \brief 대입 연산 금지
    cMiniDump& operator = (const cMiniDump&) { return *this; }
};
MiniDump.cpp
////////////////////////////////////////////////////////////////////////////////
/// \file MiniDump.cpp
/// \author excel96
/// \date 2003.11.18
///
/// 덤프 파일 옵션... from MSDN
///
/// - MiniDumpNormal
///     Include just the information necessary to capture stack traces for 
///     all existing threads in a process. 
///
/// - MiniDumpWithDataSegs 
///     Include the data sections from all loaded modules. This results in 
///     the inclusion of global variables, which can make the minidump file 
///     significantly larger. 
///
/// - MiniDumpWithFullMemory 
///     Include all accessible memory in the process. The raw memory data is 
///     included at the end, so that the Initial structures can be mapped 
///     directly without the raw memory information. This option can result 
///     in a very large file. 
///
/// - MiniDumpWithHandleData
///     Include high-level information about the operating system handles 
///     that are active when the minidump is made. 
///     \n Windows Me/98/95: This value is not supported.
///
/// - MiniDumpFilterMemory
///     Stack and backing store memory written to the minidump file should be 
///     filtered to remove all but the pointer values necessary to reconstruct 
///     a stack trace. Typically, this removes any private information. 
///
/// - MiniDumpScanMemory 
///     Stack and backing store memory should be scanned for pointer 
///     references to modules in the module list. If a module is referenced by 
///     stack or backing store memory, the ModuleWriteFlags member of the 
///     MINIDUMP_CALLBACK_OUTPUT structure is set to ModuleReferencedByMemory. 
///
/// - MiniDumpWithUnloadedModules
///     Include information from the list of modules that were recently 
///     unloaded, if this information is maintained by the operating system.
///     \n DbgHelp 5.1 and earlier: This value is not supported.
///
/// - MiniDumpWithIndirectlyReferencedMemory 
///     Include pages with data referenced by locals or other stack memory. 
///     This option can increase the size of the minidump file significantly.
///     \n DbgHelp 5.1 and earlier:  This value is not supported.
///
/// - MiniDumpFilterModulePaths
///     Filter module paths for information such as user names or important 
///     directories. This option may prevent the system from locating the 
///     image file and should be used only in special situations.
///     \n DbgHelp 5.1 and earlier:  This value is not supported.
///
/// - MiniDumpWithProcessThreadData
///     Include complete per-process and per-thread information from the 
///     operating system.
///     \n DbgHelp 5.1 and earlier:  This value is not supported.
///
/// - MiniDumpWithPrivateReadWriteMemory 
///     Scan the virtual address space for other types of memory to be 
///     included.
///     \n DbgHelp 5.1 and earlier:  This value is not supported.
////////////////////////////////////////////////////////////////////////////////
 
#include "MiniDump.h"
#include <dbghelp.h>
#include <stdio.h>
#include <tchar.h>
 
#define HAS_STACK_WALKER 1
 
#ifdef HAS_STACK_WALKER
    #include "StackWalker.h"
#endif
 
#ifndef Assert
    #include <assert.h>
    #define Assert assert
    #define LogToFile (void)(0);
#endif
 
cMiniDump::DumpLevel cMiniDump::s_DumpLevel          = cMiniDump::DUMP_LEVEL_LIGHT;
bool                 cMiniDump::s_AddTimeStamp       = true;
TCHAR                cMiniDump::s_AppName[_MAX_PATH] = {0,};
TCHAR                cMiniDump::s_CallStack[8192]    = {0,};
TCHAR                cMiniDump::s_Modules[8192]      = {0,};
LPCTSTR              cMiniDump::s_DialogTemplate     = NULL;
DLGPROC              cMiniDump::s_DialogProc         = NULL;
 
namespace
{
    /// \brief 예외의 원인에 대한 문자열을 반환한다.
    LPCTSTR GetFaultReason(PEXCEPTION_POINTERS exPtrs);
 
    /// \brief 사용자 정보를 반환한다.
    LPCTSTR GetUserInfo();
 
    /// \brief 윈도우즈 버전을 반환한다.
    LPCTSTR GetOSInfo();
 
    /// \brief CPU 정보를 반환한다.
    LPCTSTR GetCpuInfo();
 
    /// \brief 메모리 정보를 반환한다.
    LPCTSTR GetMemoryInfo();
 
    /// \brief 윈도우즈 버전을 알아낸다.
    bool GetWinVersion(LPTSTR pszVersion, int *nVersion, LPTSTR pszMajorMinorBuild);
 
    /// \brief strrchr TCHAR 버전
    TCHAR* lstrrchr(TCHAR* str, TCHAR ch);
}
 
////////////////////////////////////////////////////////////////////////////////
/// \brief 미니 덤프 기능을 초기화한다.
/// \param dumpLevel 덤프 레벨
/// \param addTimeStamp 덤프 파일 이름에다가 덤프 파일이 생성된 날짜를
/// 집어넣는가의 여부.
/// \param dialogTemplate 대화창 템플릿
/// \param dialogProc 대화창 프로시져
////////////////////////////////////////////////////////////////////////////////
void cMiniDump::Install(
    DumpLevel dumpLevel, bool addTimeStamp, LPCTSTR dialogTemplate, DLGPROC dialogProc)
{
    Assert(s_AppName[0] == 0);
    Assert(dumpLevel >= DUMP_LEVEL_LIGHT);
    Assert(dumpLevel <= DUMP_LEVEL_HEAVY);
 
    s_DumpLevel      = dumpLevel;
    s_AddTimeStamp   = addTimeStamp;
    s_DialogTemplate = dialogTemplate;
    s_DialogProc     = dialogProc;
 
    // 모듈 경로를 알아낸다.
    // C:\somewhere\something.exe
    TCHAR szFileName[_MAX_PATH];
    ::GetModuleFileName(NULL, szFileName, _MAX_PATH);
 
    // 확장자를 제거한 모듈 경로를 준비해둔다.
    // C:\somewhere\something.exe -> C:\somewhere\something
    TCHAR* dot = lstrrchr(szFileName, '.');
    ::lstrcpyn(s_AppName, szFileName, (int)(dot - szFileName + 1));
 
    // 예외 처리 핸들러를 설정한다.
    ::SetUnhandledExceptionFilter(WriteDump);
}
 
////////////////////////////////////////////////////////////////////////////////
/// \brief 예외에 대한 정보를 받아서, 미니 덤프 파일을 생성한다. 
/// 
/// SetUnhandledExceptionFilter() API에 의해서 설정되고, 프로세스 내부에서 
/// Unhandled Exception이 발생될 경우, 호출되게 된다. 단 디버거가 붙어있는 
/// 경우, Unhandled Exception Filter는 호출되지 않는다. 이 말은 이 함수 
/// 내부를 디버깅할 수는 없다는 말이다. 이 함수 내부를 디버깅하기 위해서는 
/// 메시지 박스 또는 파일을 이용해야한다.
/// 
/// \param exPtrs 예외 정보
/// \return LONG 이 함수를 실행하고 난 다음, 취할 행동값. 자세한 것은 SEH
/// 문서를 참고하도록.
////////////////////////////////////////////////////////////////////////////////
LONG WINAPI cMiniDump::WriteDump(PEXCEPTION_POINTERS exPtrs)
{
    // based on dbghelp.h
    typedef BOOL (WINAPI *MINIDUMPWRITEDUMP)(
        HANDLE hProcess, DWORD dwPid, HANDLE hFile, MINIDUMP_TYPE DumpType,
        CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
        CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
        CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam
        );
 
#ifdef UNICODE
    fwprintf_s(stderr, 
        L"=============================================\n"
        L"unhandled excetpion triggerd! writing dump...\n"
        L"=============================================\n"
        );
#else
    fprintf_s(stderr, 
        "=============================================\n"
        "unhandled excetpion triggerd! writing dump...\n"
        "=============================================\n"
        );
#endif
 
#ifdef HAS_STACK_WALKER
    // 먼저 콜스택과 모듈들을 문자열로 만든다.
    cStackWalker msw;
    msw.ShowCallStack(GetCurrentThread(), exPtrs->ContextRecord);
    ::lstrcpyn(s_CallStack, msw.GetStackString(), _ARRAYSIZE(s_CallStack)-1);
    ::lstrcpyn(s_Modules, msw.GetModuleString(), _ARRAYSIZE(s_Modules)-1);
#endif
 
    // 대화창이 설정되어 있다면 보여준다.
    if (s_DialogTemplate != NULL && s_DialogProc != NULL)
    {
        if (DialogBox(NULL, s_DialogTemplate, HWND_DESKTOP, s_DialogProc) != IDOK)
            return EXCEPTION_EXECUTE_HANDLER;
    }
 
    HMODULE hDLL = NULL;
    TCHAR szDbgHelpPath[_MAX_PATH] = {0, };
    TCHAR szDumpPath[MAX_PATH * 2] = {0,};
    TCHAR szLogPath[MAX_PATH * 2] = {0,};
 
    // 덤프 파일 이름 += 시간 문자열
    ::lstrcat(szDumpPath, s_AppName);
    ::lstrcat(szLogPath, s_AppName);
    if (s_AddTimeStamp)
    {
        SYSTEMTIME t;
        ::GetLocalTime(&t);
 
        TCHAR szTail[_MAX_PATH];
#ifdef UNICODE
        swprintf(szTail, _ARRAYSIZE(szTail)-1, 
            L" %04d-%02d-%02d %02d-%02d-%02d",
            t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond);
#else
        _snprintf_s(szTail, _ARRAYSIZE(szTail)-1, _TRUNCATE,
            " %04d-%02d-%02d %02d-%02d-%02d",
            t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond);
#endif
 
        ::lstrcat(szDumpPath, szTail);
        ::lstrcat(szLogPath, szTail);
    }
    ::lstrcat(szDumpPath, _T(".dmp"));
    ::lstrcat(szLogPath, _T(".log"));
 
    // 먼저 실행 파일이 있는 디렉토리에서 DBGHELP.DLL을 로드해 본다.
    // Windows 2000 의 System32 디렉토리에 있는 DBGHELP.DLL 파일은 버전이 
    // 오래된 것일 수 있기 때문이다. (최소 5.1.2600.0 이상이어야 한다.)
    if (::GetModuleFileName(NULL, szDbgHelpPath, _MAX_PATH))
    {
        if (LPTSTR slash = ::lstrrchr(szDbgHelpPath, '\\'))
        {
            ::lstrcpy(slash + 1, _T("DBGHELP.DLL"));
            hDLL = ::LoadLibrary(szDbgHelpPath);
        }
    }
 
    // 현재 디렉토리에 없다면, 아무 버전이나 로드한다.
    if (hDLL == NULL) hDLL = ::LoadLibrary(_T("dbghelp.dll"));
 
    // DBGHELP.DLL을 찾을 수 없다면 더 이상 진행할 수 없다.
    if (hDLL == NULL)
    {
        LogToFile(szLogPath, _T("dbghelp.dll not found"));
        return EXCEPTION_CONTINUE_SEARCH;
    }
 
    // DLL 내부에서 MiniDumpWriteDump API를 찾는다.
    MINIDUMPWRITEDUMP pfnMiniDumpWriteDump = 
        (MINIDUMPWRITEDUMP)::GetProcAddress(hDLL, "MiniDumpWriteDump");
    if (pfnMiniDumpWriteDump == NULL)
    {
        LogToFile(szLogPath, _T("dbghelp.dll too old"));
        return EXCEPTION_CONTINUE_SEARCH;
    }
 
    // 파일을 생성한다.
    HANDLE hFile = ::CreateFile(
        szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, 
        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        LogToFile(szLogPath, _T("failed to create dump file '%s' (error %d)"), 
            szDumpPath, ::GetLastError());
        return EXCEPTION_CONTINUE_SEARCH;
    }
 
    MINIDUMP_EXCEPTION_INFORMATION ExParam;
    ExParam.ThreadId = ::GetCurrentThreadId();
    ExParam.ExceptionPointers = exPtrs;
    ExParam.ClientPointers = FALSE;
 
    MINIDUMP_TYPE dumptype = MiniDumpNormal;
    switch (s_DumpLevel)
    {
    case DUMP_LEVEL_LIGHT:  dumptype = MiniDumpNormal;         break;
    case DUMP_LEVEL_MEDIUM: dumptype = MiniDumpWithDataSegs;   break;
    case DUMP_LEVEL_HEAVY:  dumptype = MiniDumpWithFullMemory; break;
    }
 
    // 덤프 파일 생성 결과를 로그 파일에다 기록한다.
    if (pfnMiniDumpWriteDump(
        ::GetCurrentProcess(), ::GetCurrentProcessId(), 
        hFile, dumptype, &ExParam, NULL, NULL))
    {
#ifdef UNICODE
        FILE* log = NULL;
        if (_wfopen_s(&log, szLogPath, _T("a")) == 0)
#else
        FILE* log = NULL;
        if (fopen_s(&log, szLogPath, "a") == 0)
#endif
        {
            fprintf(log,
                "saved dump file to '%s'\n"
                "\n<fault reason>\n%s"
                "\n\n<user>\n%s"
                "\n\n<os>\n%s"
                "\n\n<cpu>\n%s"
                "\n\n<memory>\n%s",
                szDumpPath,
                GetFaultReason(exPtrs),
                GetUserInfo(),
                GetOSInfo(),
                GetCpuInfo(),
                GetMemoryInfo()
                );
 
#ifdef HAS_STACK_WALKER
            fprintf(log, "\n\n<stack>\n%s\n", GetCallStack());
            fprintf(log, "\n<modules>\n%s\n", GetModules());
#endif
            fclose(log);
        }
 
    }
    else
    {
        LogToFile(szLogPath, _T("failed to save dump file to '%s' (error %d)"), 
            szDumpPath, ::GetLastError());
        Assert(false);
    }
 
    ::CloseHandle(hFile);
 
    return EXCEPTION_EXECUTE_HANDLER;
}
 
namespace
{
    /// \brief 예외의 원인에 대한 문자열을 반환한다.
    /// \param exPtrs 예외 구조체 포인터
    /// \return LPCTSTR 원인 문자열
    LPCTSTR GetFaultReason(PEXCEPTION_POINTERS exPtrs)
    {
        if (::IsBadReadPtr(exPtrs, sizeof(EXCEPTION_POINTERS))) 
            return _T("bad exception pointers");
 
        // 간단한 에러 코드라면 그냥 변환할 수 있다.
        switch (exPtrs->ExceptionRecord->ExceptionCode)
        {
        case EXCEPTION_ACCESS_VIOLATION:         return _T("EXCEPTION_ACCESS_VIOLATION");
        case EXCEPTION_DATATYPE_MISALIGNMENT:    return _T("EXCEPTION_DATATYPE_MISALIGNMENT");
        case EXCEPTION_BREAKPOINT:               return _T("EXCEPTION_BREAKPOINT");
        case EXCEPTION_SINGLE_STEP:              return _T("EXCEPTION_SINGLE_STEP");
        case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:    return _T("EXCEPTION_ARRAY_BOUNDS_EXCEEDED");
        case EXCEPTION_FLT_DENORMAL_OPERAND:     return _T("EXCEPTION_FLT_DENORMAL_OPERAND");
        case EXCEPTION_FLT_DIVIDE_BY_ZERO:       return _T("EXCEPTION_FLT_DIVIDE_BY_ZERO");
        case EXCEPTION_FLT_INEXACT_RESULT:       return _T("EXCEPTION_FLT_INEXACT_RESULT");
        case EXCEPTION_FLT_INVALID_OPERATION:    return _T("EXCEPTION_FLT_INVALID_OPERATION");
        case EXCEPTION_FLT_OVERFLOW:             return _T("EXCEPTION_FLT_OVERFLOW");
        case EXCEPTION_FLT_STACK_CHECK:          return _T("EXCEPTION_FLT_STACK_CHECK");
        case EXCEPTION_FLT_UNDERFLOW:            return _T("EXCEPTION_FLT_UNDERFLOW");
        case EXCEPTION_INT_DIVIDE_BY_ZERO:       return _T("EXCEPTION_INT_DIVIDE_BY_ZERO");
        case EXCEPTION_INT_OVERFLOW:             return _T("EXCEPTION_INT_OVERFLOW");
        case EXCEPTION_PRIV_INSTRUCTION:         return _T("EXCEPTION_PRIV_INSTRUCTION");
        case EXCEPTION_IN_PAGE_ERROR:            return _T("EXCEPTION_IN_PAGE_ERROR");
        case EXCEPTION_ILLEGAL_INSTRUCTION:      return _T("EXCEPTION_ILLEGAL_INSTRUCTION");
        case EXCEPTION_NONCONTINUABLE_EXCEPTION: return _T("EXCEPTION_NONCONTINUABLE_EXCEPTION");
        case EXCEPTION_STACK_OVERFLOW:           return _T("EXCEPTION_STACK_OVERFLOW");
        case EXCEPTION_INVALID_DISPOSITION:      return _T("EXCEPTION_INVALID_DISPOSITION");
        case EXCEPTION_GUARD_PAGE:               return _T("EXCEPTION_GUARD_PAGE");
        case EXCEPTION_INVALID_HANDLE:           return _T("EXCEPTION_INVALID_HANDLE");
            //case EXCEPTION_POSSIBLE_DEADLOCK:        return _T("EXCEPTION_POSSIBLE_DEADLOCK");
        case CONTROL_C_EXIT:                     return _T("CONTROL_C_EXIT");
        case 0xE06D7363:                         return _T("Microsoft C++ Exception");
        default:
            break;
        }
 
        // 뭔가 좀 더 복잡한 에러라면...
        static TCHAR szFaultReason[2048];
        ::lstrcpy(szFaultReason, _T("Unknown")); 
        ::FormatMessage(
            FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
            ::GetModuleHandle(_T("ntdll.dll")),
            exPtrs->ExceptionRecord->ExceptionCode, 
            0,
            szFaultReason,
            0,
            NULL);
 
        return szFaultReason;
    }
 
    /// \brief 사용자 정보를 반환한다.
    /// \return LPCTSTR 사용자 이름
    LPCTSTR GetUserInfo()
    {
        static TCHAR szUserName[200] = {0,};
 
        ZeroMemory(szUserName, sizeof(szUserName));
        DWORD UserNameSize = _ARRAYSIZE(szUserName) - 1;
 
        if (!::GetUserName(szUserName, &UserNameSize))
            ::lstrcpy(szUserName, _T("Unknown"));
 
        return szUserName;
    }
 
    /// \brief 윈도우즈 버전을 반환한다.
    /// \return LPCTSTR 윈도우즈 버전 문자열
    LPCTSTR GetOSInfo()
    {
        TCHAR szWinVer[50] = {0,};
        TCHAR szMajorMinorBuild[50] = {0,};
        int nWinVer = 0;
        ::GetWinVersion(szWinVer, &nWinVer, szMajorMinorBuild);
 
        static TCHAR szOSInfo[512] = {0,};
#ifdef UNICODE
        swprintf(szOSInfo, _ARRAYSIZE(szOSInfo)-1, L"%s (%s)",
            szWinVer, szMajorMinorBuild);
#else
        _snprintf_s(szOSInfo, _ARRAYSIZE(szOSInfo)-1, _TRUNCATE, "%s (%s)",
            szWinVer, szMajorMinorBuild);
#endif
        szOSInfo[_ARRAYSIZE(szOSInfo)-1] = 0;
        return szOSInfo;
    }
 
    /// \brief CPU 정보를 반환한다.
    /// \return LPCTSTR CPU 정보 문자열
    LPCTSTR GetCpuInfo()
    {
        // CPU 정보 기록
        SYSTEM_INFO    SystemInfo;
        GetSystemInfo(&SystemInfo);
 
        static TCHAR szCpuInfo[512] = {0,};
#ifdef UNICODE
        swprintf(szCpuInfo, _ARRAYSIZE(szCpuInfo)-1, 
            L"%d processor(s), type %d",
            SystemInfo.dwNumberOfProcessors, SystemInfo.dwProcessorType);
#else
        _snprintf_s(szCpuInfo, _ARRAYSIZE(szCpuInfo)-1, _TRUNCATE, 
            "%d processor(s), type %d",
            SystemInfo.dwNumberOfProcessors, SystemInfo.dwProcessorType);
#endif
        return szCpuInfo;
    }
 
    /// \brief 메모리 정보를 반환한다.
    /// \return LPCTSTR 메모리 정보 문자열
    LPCTSTR GetMemoryInfo()
    {
        static const int ONE_K = 1024;
        static const int ONE_M = ONE_K * ONE_K;
        static const int ONE_G = ONE_K * ONE_K * ONE_K;
 
        MEMORYSTATUS MemInfo;
        MemInfo.dwLength = sizeof(MemInfo);
        GlobalMemoryStatus(&MemInfo);
 
        static TCHAR szMemoryInfo[2048] = {0,};
#ifdef UNICODE
        swprintf(szMemoryInfo, _ARRAYSIZE(szMemoryInfo)-1, 
            L"%d%% of memory in use.\n"
            L"%d MB physical memory.\n"
            L"%d MB physical memory free.\n"
            L"%d MB paging file.\n"
            L"%d MB paging file free.\n"
            L"%d MB user address space.\n"
            L"%d MB user address space free.",
            MemInfo.dwMemoryLoad, 
            (MemInfo.dwTotalPhys + ONE_M - 1) / ONE_M, 
            (MemInfo.dwAvailPhys + ONE_M - 1) / ONE_M, 
            (MemInfo.dwTotalPageFile + ONE_M - 1) / ONE_M, 
            (MemInfo.dwAvailPageFile + ONE_M - 1) / ONE_M, 
            (MemInfo.dwTotalVirtual + ONE_M - 1) / ONE_M, 
            (MemInfo.dwAvailVirtual + ONE_M - 1) / ONE_M);
#else
        _snprintf_s(szMemoryInfo, _ARRAYSIZE(szMemoryInfo)-1, _TRUNCATE,
            "%d%% of memory in use.\n"
            "%d MB physical memory.\n"
            "%d MB physical memory free.\n"
            "%d MB paging file.\n"
            "%d MB paging file free.\n"
            "%d MB user address space.\n"
            "%d MB user address space free.",
            MemInfo.dwMemoryLoad, 
            (MemInfo.dwTotalPhys + ONE_M - 1) / ONE_M, 
            (MemInfo.dwAvailPhys + ONE_M - 1) / ONE_M, 
            (MemInfo.dwTotalPageFile + ONE_M - 1) / ONE_M, 
            (MemInfo.dwAvailPageFile + ONE_M - 1) / ONE_M, 
            (MemInfo.dwTotalVirtual + ONE_M - 1) / ONE_M, 
            (MemInfo.dwAvailVirtual + ONE_M - 1) / ONE_M);
#endif
 
        return szMemoryInfo;
    }
 
    /// \brief 윈도우즈 버전을 알아낸다.
    ///
    /// This table has been assembled from Usenet postings, personal observations, 
    /// and reading other people's code.  Please feel free to add to it or correct 
    /// it.
    ///
    /// <pre>
    /// dwPlatFormID  dwMajorVersion  dwMinorVersion  dwBuildNumber
    /// 95            1               4                 0            950
    /// 95 SP1        1               4                 0            >950 && <=1080
    /// 95 OSR2       1               4               <10            >1080
    /// 98            1               4                10            1998
    /// 98 SP1        1               4                10            >1998 && <2183
    /// 98 SE         1               4                10            >=2183
    /// ME            1               4                90            3000
    ///
    /// NT 3.51       2               3                51
    /// NT 4          2               4                 0            1381
    /// 2000          2               5                 0            2195
    /// XP            2               5                 1            2600
    /// 2003 Server   2               5                 2            3790
    ///
    /// CE            3
    /// </pre>
    ///
    /// \param pszVersion 버전 문자열을 집어넣을 포인터
    /// \param nVersion 버전 숫자값을 집어넣을 포인터
    /// \param pszMajorMinorBuild 빌드 문자열을 집어넣을 포인터
    /// \return bool 무사히 실행한 경우에는 true, 뭔가 에러가 생긴 경우에는 false
    bool GetWinVersion(LPTSTR pszVersion, int *nVersion, LPTSTR pszMajorMinorBuild)
    {
        // from winbase.h
#ifndef VER_PLATFORM_WIN32s
    #define VER_PLATFORM_WIN32s 0
#endif
 
#ifndef VER_PLATFORM_WIN32_WINDOWS
    #define VER_PLATFORM_WIN32_WINDOWS 1
#endif
 
#ifndef VER_PLATFORM_WIN32_NT
    #define VER_PLATFORM_WIN32_NT 2
#endif
 
#ifndef VER_PLATFORM_WIN32_CE
    #define VER_PLATFORM_WIN32_CE 3
#endif
 
        static LPCTSTR WUNKNOWNSTR     = _T("Unknown Windows Version");
        static LPCTSTR W95STR          = _T("Windows 95");
        static LPCTSTR W95SP1STR       = _T("Windows 95 SP1");
        static LPCTSTR W95OSR2STR      = _T("Windows 95 OSR2");
        static LPCTSTR W98STR          = _T("Windows 98");
        static LPCTSTR W98SP1STR       = _T("Windows 98 SP1");
        static LPCTSTR W98SESTR        = _T("Windows 98 SE");
        static LPCTSTR WMESTR          = _T("Windows ME");
        static LPCTSTR WNT351STR       = _T("Windows NT 3.51");
        static LPCTSTR WNT4STR         = _T("Windows NT 4");
        static LPCTSTR W2KSTR          = _T("Windows 2000");
        static LPCTSTR WXPSTR          = _T("Windows XP");
        static LPCTSTR W2003SERVERSTR  = _T("Windows 2003 Server");
        static LPCTSTR WCESTR          = _T("Windows CE");
 
        static const int WUNKNOWN      = 0;
        static const int W9XFIRST      = 1;
        static const int W95           = 1;
        static const int W95SP1        = 2;
        static const int W95OSR2       = 3;
        static const int W98           = 4;
        static const int W98SP1        = 5;
        static const int W98SE         = 6;
        static const int WME           = 7;
        static const int W9XLAST       = 99;
        static const int WNTFIRST      = 101;
        static const int WNT351        = 101;
        static const int WNT4          = 102;
        static const int W2K           = 103;
        static const int WXP           = 104;
        static const int W2003SERVER   = 105;
        static const int WNTLAST       = 199;
        static const int WCEFIRST      = 201;
        static const int WCE           = 201;
        static const int WCELAST       = 299;
 
        if (!pszVersion || !nVersion || !pszMajorMinorBuild) return false;
 
        ::lstrcpy(pszVersion, WUNKNOWNSTR);
        *nVersion = WUNKNOWN;
 
        OSVERSIONINFO osinfo;
        osinfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
 
        if (!GetVersionEx(&osinfo)) return false;
 
        DWORD dwPlatformId   = osinfo.dwPlatformId;
        DWORD dwMinorVersion = osinfo.dwMinorVersion;
        DWORD dwMajorVersion = osinfo.dwMajorVersion;
        DWORD dwBuildNumber  = osinfo.dwBuildNumber & 0xFFFF; // Win 95 needs this
 
        TCHAR buf[50] = {0, };
#ifdef UNICODE
        swprintf(buf, _ARRAYSIZE(buf), L"%u.%u.%u", 
            dwMajorVersion, dwMinorVersion, dwBuildNumber);
#else
        _snprintf_s(buf, _ARRAYSIZE(buf), _TRUNCATE, "%u.%u.%u", 
            dwMajorVersion, dwMinorVersion, dwBuildNumber);
#endif
        ::lstrcpy(pszMajorMinorBuild, buf);
 
        if ((dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) && (dwMajorVersion == 4))
        {
            if ((dwMinorVersion < 10) && (dwBuildNumber == 950))
            {
                ::lstrcpy(pszVersion, W95STR);
                *nVersion = W95;
            }
            else if ((dwMinorVersion < 10) && 
                ((dwBuildNumber > 950) && (dwBuildNumber <= 1080)))
            {
                ::lstrcpy(pszVersion, W95SP1STR);
                *nVersion = W95SP1;
            }
            else if ((dwMinorVersion < 10) && (dwBuildNumber > 1080))
            {
                ::lstrcpy(pszVersion, W95OSR2STR);
                *nVersion = W95OSR2;
            }
            else if ((dwMinorVersion == 10) && (dwBuildNumber == 1998))
            {
                ::lstrcpy(pszVersion, W98STR);
                *nVersion = W98;
            }
            else if ((dwMinorVersion == 10) && 
                ((dwBuildNumber > 1998) && (dwBuildNumber < 2183)))
            {
                ::lstrcpy(pszVersion, W98SP1STR);
                *nVersion = W98SP1;
            }
            else if ((dwMinorVersion == 10) && (dwBuildNumber >= 2183))
            {
                ::lstrcpy(pszVersion, W98SESTR);
                *nVersion = W98SE;
            }
            else if (dwMinorVersion == 90)
            {
                ::lstrcpy(pszVersion, WMESTR);
                *nVersion = WME;
            }
        }
        else if (dwPlatformId == VER_PLATFORM_WIN32_NT)
        {
            if ((dwMajorVersion == 3) && (dwMinorVersion == 51))
            {
                ::lstrcpy(pszVersion, WNT351STR);
                *nVersion = WNT351;
            }
            else if ((dwMajorVersion == 4) && (dwMinorVersion == 0))
            {
                ::lstrcpy(pszVersion, WNT4STR);
                *nVersion = WNT4;
            }
            else if ((dwMajorVersion == 5) && (dwMinorVersion == 0))
            {
                ::lstrcpy(pszVersion, W2KSTR);
                *nVersion = W2K;
            }
            else if ((dwMajorVersion == 5) && (dwMinorVersion == 1))
            {
                ::lstrcpy(pszVersion, WXPSTR);
                *nVersion = WXP;
            }
            else if ((dwMajorVersion == 5) && (dwMinorVersion == 2))
            {
                ::lstrcpy(pszVersion, W2003SERVERSTR);
                *nVersion = W2003SERVER;
            }
        }
        else if (dwPlatformId == VER_PLATFORM_WIN32_CE)
        {
            ::lstrcpy(pszVersion, WCESTR);
            *nVersion = WCE;
        }
 
        return true;
 
#undef VER_PLATFORM_WIN32s
#undef VER_PLATFORM_WIN32_WINDOWS
#undef VER_PLATFORM_WIN32_NT
#undef VER_PLATFORM_WIN32_CE
    }
 
    /// \brief strrchr TCHAR 버전
    /// \param str 검색할 문자열
    /// \param ch 찾고자 하는 글자
    /// \return TCHAR* 주어진 글자를 찾은 경우 해당 위치 포인터를 반환하고, 
    /// 찾지 못한 경우에는 NULL을 반환한다.
    TCHAR* lstrrchr(TCHAR* str, TCHAR ch)
    {
        TCHAR* start = str;
        while (*str++) ;
        while (--str != start && *str != ch) ;
        return *str == ch ? str : NULL;
    }
}

미니 덤프 분석

일반적인 시나리오

일단 유저의 컴퓨터에서 뭘 받을지를 정해야한다. 크래시 로그 파일(스택 워커 등을 이용해 프로그래머가 만드는 로그)만 받기를 원한다면 PDB를 배포해야 한다. 단 이때 Private Symbol을 배포하면 곤란하므로, WinDbg에 딸려오는 pdbcopy를 이용해 Public Symbol을 만들어 배포하자.

pdbcopy PrivateSymbolName.pdb PublicSymbolName.pdb -p

그게 아니라 그냥 덤프 파일을 받기를 원한다면 PDB 파일을 배포할 필요는 없다. 사실

MiniDumpNormal

정도의 옵션으로 덤프를 생성하면 덤프 파일 크기가 그리 크지 않기 때문에, 덤프 파일을 받는 게 낫다.

유저로부터 덤프 파일을 받았다면, 그 덤프 파일에 해당하는 PDB와 EXE를 찾는다. 요건 심볼 스토어를 이용하면 그나마 작업이 좀 쉬워질 것 같기는 한데, 아직까지는 해보질 않아서 잘 모르겠다. _NT_SYMBOL_PATH 설정과 WinDbg 설치는 필수다. 2008에서는 좀 나아졌는지 모르겠는데, 2005 VC 디버거는 WinDbg보다 기능이 딸린다. 뭐 왠만하면 그냥 VC 사용해도 되기는 하는데…

실행 파일, PDB, DMP 파일을 모두 같은 경로에다 집어넣고 그 디렉토리로 가서 아래와 같은 명령어를 입력한다.

windbg -y . -i . DumpFileName.dmp

원래 명령어는 이런 느낌이다.

windbg -y SymbolPath -i ImagePath -z DumpFileName 

보다시피 EXE, PDB, DMP 파일을 모두 같은 경로에 집어넣어두었기 때문에 ”.” 경로를 입력한 것이다.

WinDbg .reload /i /f 명령어를 입력한다. 심볼 로딩하라는 명령이다. 별 문제없이 심볼 로딩이 끝나면 .ecxr 명령어를 입력한다. 그다음 콜스택(ALT+6)을 보면 다운된 위치를 알 수 있다. 혹은 !analyze -v 명령어를 이용해도 된다.

EXE 파일의 경로가 실제 유저의 컴퓨터에서의 위치와 다르면 심볼 로딩이 잘 되지 않는다. 예를 들어 유저가 프로그램을 C:\XXX\ 디렉토리에다 설치했다면 프로그래머의 컴퓨터에서도 그 경로와 똑같은 위치에다 EXE, PDB, DMP 파일을 복사한 다음 디버깅을 해야한다는 이야기다. 이유가 뭘까?

미니 덤프에서 자주 보는 정보 자동으로 뽑아내기

Dmitry Vostokov 님의 블로그에 자세한 내용이 나와있다. 간단하게 설명하자면 다음과 같다.

필요한 정보들을 뽑아내는 WinDbg 스크립트를 만든다.

$$
$$ MiniDmp2Txt: Dump information from minidump into log
$$
.logopen /d /u
.echo "command> ||"
||
.echo "command> vertarget"
vertarget
.echo "command> r (before analysis)"
r
.echo "command> kv (before analysis)"
kv 100
.echo "command> !analyze -v"
!analyze -v
.echo "command> r"
r
.echo "command> kv"
kv 100
.echo "command> ub eip"
ub eip
.echo "command> u eip"
u eip
.echo "command> uf eip"
uf eip
.echo "command> lmv"
lmv
.echo "command> !sysinfo cpuinfo"
!sysinfo cpuinfo
.echo "command> !sysinfo cpuspeed"
!sysinfo cpuspeed
.echo "command> !sysinfo cpumicrocode"
!sysinfo cpumicrocode
.echo "command> !sysinfo gbl"
!sysinfo gbl
.echo "command> !sysinfo machineid"
!sysinfo machineid
.echo "command> !sysinfo registers"
!sysinfo registers
.echo "command> !sysinfo smbios -v"
!sysinfo smbios -v
.logclose
$$
$$ MiniDmp2Txt: End of File
$$

이 파일을 C:\Scripts\MiniDmp2Txt.txt라는 이름으로 저장했다고 하자. 그 다음 WinDbg로 크래시 덤프를 열면서 위에서 작성한 스크립트를 호출한다.

windbg -y "SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols;." -z "DumpFileName.dmp" -c "$$><C:\Scripts\MiniDmp2Txt.txt;q" -Q -QS -QY -QSY

그러면 WinDbg가 실행되면서 스크립트 안에 있는 명령을 쭉 실행하면서 로그 파일을 만들게 된다. -y 옵션은 심볼 패스를 지정하는 옵션으로서 _NT_SYMBOL_PATH 값이 잘 설정되어있다면 생략해도 된다.

덤프 파일을 생성하는 데 사용할 수 있는 도구들

  • DrWatson
    • 덤프 파일 생성을 위한 기본 유틸리티.
    • 덤프 파일 생성을 위한 VB 스크립트.
    • 디버거를 붙인 다음, 메뉴를 통해 덤프 파일을 생성할 수 있다. 디버깅을 중지하면서 프로세스가 종료되므로 자주 사용하지 않는 것이 좋다. AdPlus를 이용하라.
  • userdump.exe
    • OEM 서포트 툴에 포함되어 있는 도구로서, 한 프로세스의 풀 덤프 파일을 생성하는 데 사용할 수 있다. 좀 더 자세한 것은 여기를 참고하시라.

링크

kb/minidump.txt · 마지막으로 수정됨: 2014/11/12 12:38 저자 excel96