C++/MFC 프로그램 크래시 자동 덤프(.dmp) 만들기: MiniDumpWriteDump + SetUnhandledExceptionFilter 실전 가이드

Prompt Architect 편집팀 · 2026-06-18 · 8분

TL;DR — 사용자 PC에서만 죽는 C++/MFC 프로그램, 재현이 안 돼 막막할 때. SetUnhandledExceptionFilter와 MiniDumpWriteDump로 크래시 순간을 .dmp 파일로 남겨 WinDbg로 원인을 추적하는 방법을 단계별로 정리했습니다.

"내 PC에서는 잘 되는데요"라는 함정

C++/MFC로 만든 데스크톱 프로그램을 배포하다 보면 가장 곤란한 상황이 있습니다. 개발자 PC에서는 멀쩡하게 돌아가는데, 특정 사용자 환경에서만 프로그램이 갑자기 사라지듯 종료되는 경우입니다. 로그도 안 남고, 에러 메시지조차 뜨지 않고, 그저 "프로그램이 그냥 꺼졌어요"라는 제보만 돌아옵니다.

이런 비결정적(non-deterministic) 크래시는 디버거를 붙여 재현할 수가 없기 때문에 원인 추적이 사실상 불가능에 가깝습니다. 메모리 접근 위반(Access Violation), 널 포인터 역참조, 스택 손상 같은 문제는 발생 환경의 미묘한 차이에 따라 나타났다 사라졌다 하기 때문입니다.

해결책은 의외로 단순합니다. 크래시가 발생하는 그 순간, 프로세스의 상태를 통째로 파일로 떠놓는 것입니다. 이렇게 저장된 파일을 미니덤프(minidump, .dmp)라고 부르며, 나중에 WinDbg나 Visual Studio에서 열어보면 마치 그 순간으로 시간을 되돌린 것처럼 콜스택과 변수 상태를 들여다볼 수 있습니다.

이 글에서는 Windows API 두 개, SetUnhandledExceptionFilterMiniDumpWriteDump만으로 자동 크래시 덤프 시스템을 구축하는 방법을 다룹니다.

핵심 원리: 처리되지 않은 예외를 가로채기

Windows에서 프로그램이 비정상 종료될 때는 보통 "처리되지 않은 예외(unhandled exception)"가 발생한 상태입니다. try/catch나 SEH로도 잡히지 않은 예외가 콜스택 최상단까지 올라오면, OS는 마지막으로 등록된 핸들러를 호출한 뒤 프로세스를 종료시킵니다.

여기서 두 가지 도구가 등장합니다.

  • SetUnhandledExceptionFilter: "처리되지 않은 예외가 발생하면 이 함수를 불러달라"고 OS에 등록하는 함수입니다. 즉, 프로세스가 죽기 직전 마지막으로 끼어들 수 있는 후크 지점입니다.
  • MiniDumpWriteDump: 현재 프로세스의 메모리·스레드·예외 정보를 .dmp 파일로 기록하는 함수입니다. DbgHelp 라이브러리에 들어 있습니다.

흐름은 이렇습니다. 평소에 예외 필터 함수를 등록해 두면 → 크래시가 나는 순간 OS가 그 함수를 호출 → 그 안에서 덤프 파일을 생성. 단 몇 줄이지만 디버깅 효율은 비교가 안 될 만큼 올라갑니다.

1단계: 헤더와 라이브러리 연결

먼저 DbgHelp 관련 헤더를 포함하고 라이브러리를 링크합니다. 프리컴파일 헤더(stdafx.h 또는 pch.h)에 넣어두면 편합니다.

// 크래시 덤프에 필요한 헤더
#include <Windows.h>
#include <DbgHelp.h>
#include <string>

// DbgHelp 라이브러리 링크 (대소문자 무관하지만 명시적으로 적어두면 안전)
#pragma comment(lib, "Dbghelp.lib")

#pragma comment(lib, ...)를 쓰면 프로젝트 설정의 링커 옵션을 따로 건드리지 않아도 됩니다. 팀원이 프로젝트를 받아 빌드할 때 링크 누락으로 헤매는 일을 줄여주는 작은 습관입니다.

2단계: 예외 필터 콜백 작성

크래시 순간에 호출될 콜백 함수를 정의합니다. 원문 예제를 그대로 베끼기보다, 파일명에 타임스탬프를 붙이고 핸들 누수를 막는 형태로 다듬어 보겠습니다.

// 처리되지 않은 예외 발생 시 OS가 호출하는 콜백
LONG WINAPI WriteCrashDump(EXCEPTION_POINTERS* pExceptionPtrs)
{
    // 1) 예외 정보 구조체 채우기
    MINIDUMP_EXCEPTION_INFORMATION dumpInfo = { 0 };
    dumpInfo.ThreadId          = ::GetCurrentThreadId();  // 크래시 난 스레드
    dumpInfo.ExceptionPointers = pExceptionPtrs;          // 예외 컨텍스트
    dumpInfo.ClientPointers    = FALSE;                    // 포인터는 현재 프로세스 기준

    // 2) 겹치지 않는 파일명 만들기 (시간 기반)
    SYSTEMTIME st;
    ::GetLocalTime(&st);
    wchar_t dumpPath[MAX_PATH] = { 0 };
    swprintf_s(dumpPath, L"crash_%04d%02d%02d_%02d%02d%02d.dmp",
               st.wYear, st.wMonth, st.wDay,
               st.wHour, st.wMinute, st.wSecond);

    // 3) 덤프 파일 핸들 생성
    HANDLE hDumpFile = ::CreateFileW(
        dumpPath, GENERIC_WRITE, FILE_SHARE_READ,
        NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hDumpFile != INVALID_HANDLE_VALUE)
    {
        // 4) 실제 덤프 기록
        ::MiniDumpWriteDump(
            ::GetCurrentProcess(),     // 대상 프로세스 핸들
            ::GetCurrentProcessId(),   // 대상 프로세스 ID
            hDumpFile,                 // 출력 파일 핸들
            MiniDumpNormal,            // 덤프 종류 (아래 설명 참고)
            &dumpInfo,                 // 예외 정보
            NULL, NULL);

        ::CloseHandle(hDumpFile);      // 핸들 반드시 정리
    }

    // 표준 처리(프로세스 종료)로 넘김
    return EXCEPTION_EXECUTE_HANDLER;
}

원문 예제와 비교하면 세 가지가 다릅니다. 파일명에 날짜·시각을 넣어 덮어쓰기를 방지했고, CloseHandle핸들 누수를 막았으며, 반환값을 EXCEPTION_EXECUTE_HANDLER로 두어 OS의 후속 종료 처리에 명확히 넘겼습니다.

3단계: 프로그램 시작 시 필터 등록

이제 이 콜백을 OS에 등록합니다. MFC라면 InitInstance()의 가장 앞부분, 콘솔/Win32라면 main이나 WinMain의 첫 줄에 두는 것이 핵심입니다.

BOOL CMyApp::InitInstance()
{
    // 다른 초기화보다 먼저 등록해야 초기 크래시도 잡힌다
    SetUnhandledExceptionFilter(WriteCrashDump);

    // ... 이후 기존 초기화 코드 ...
    return CWinApp::InitInstance();
}

여기서 가장 중요한 주의사항이 나옵니다. 필터가 등록되기 전에 발생한 예외는 잡을 수 없습니다. 즉, 등록 코드가 늦게 실행될수록 초기 구동 단계의 크래시를 놓칩니다. 그래서 가능한 한 진입점의 맨 앞에 배치해야 합니다.

덤프 종류(MINIDUMP_TYPE) 고르기

MiniDumpWriteDump의 네 번째 인자로 어떤 정보를 얼마나 담을지 정합니다.

// 가벼움: 콜스택 중심, 파일 작음 (수십 KB~수백 KB)
MiniDumpNormal

// 변수까지: 데이터 세그먼트 포함, 로컬 변수 일부 확인 가능
MiniDumpWithDataSegs

// 풀덤프: 전체 메모리 포함, 가장 정확하지만 파일이 매우 큼
MiniDumpWithFullMemory

여러 옵션을 OR 연산으로 조합할 수도 있습니다. 실무에서는 배포판에는 MiniDumpNormal로 가볍게, 재현이 어려운 특정 버그를 추적할 때만 MiniDumpWithDataSegs나 풀덤프로 올리는 식이 무난합니다. 풀덤프는 수백 MB가 나올 수 있어 사용자가 전송하기 부담스럽습니다.

흔히 빠지는 함정들

  • 릴리스 빌드 PDB 누락: 덤프는 떴는데 콜스택이 주소 숫자만 보이고 함수명이 안 나온다면, 빌드 시 생성된 .pdb 심볼 파일이 없어서입니다. 릴리스 빌드에서도 PDB를 생성하도록 설정하고(/DEBUG 링커 옵션), 빌드 산출물의 PDB를 버전별로 보관해야 합니다.
  • 콜백 안에서 너무 많은 일 하기: 크래시 순간의 프로세스는 이미 불안정한 상태입니다. 콜백 안에서 힙 할당, 복잡한 로깅, MFC 객체 생성 등을 하면 그 안에서 또 죽어버릴 수 있습니다. 덤프 기록만 최소한으로 하세요.
  • 스택 손상으로 콜백 자체가 호출 안 됨: 스택 오버플로처럼 스택 자체가 깨진 경우, 필터 함수를 실행할 스택조차 없어 호출되지 못할 수 있습니다. 이럴 땐 별도 스택을 확보하는 고급 기법이 필요합니다.
  • 다중 스레드 환경: MINIDUMP_EXCEPTION_INFORMATIONThreadId는 크래시가 난 스레드를 가리켜야 합니다. 콜백은 보통 해당 스레드 컨텍스트에서 호출되므로 GetCurrentThreadId()가 맞지만, 직접 다른 스레드 덤프를 뜰 때는 ID를 정확히 넘겨야 합니다.

덤프 파일 분석하기

생성된 .dmp 파일은 두 가지로 열 수 있습니다.

  • Visual Studio: .dmp를 더블클릭해 열고 "관리 코드/네이티브 디버깅 시작"을 누르면, 크래시 시점의 콜스택과 변수가 IDE에서 그대로 보입니다. 가장 친숙한 방법입니다.
  • WinDbg: Windows Debugging Tools에 포함된 강력한 분석 도구입니다. .dmp를 연 뒤 !analyze -v 명령을 입력하면 예외 코드, 폴트 발생 모듈, 콜스택을 자동 분석해 보고서를 띄워줍니다.

핵심은 덤프 파일의 빌드와 동일한 PDB를 심볼 경로에 연결하는 것입니다. 버전이 어긋나면 엉뚱한 함수명이 표시됩니다.

요약

  • 처리되지 않은 예외는 SetUnhandledExceptionFilter로 가로챈다.
  • 콜백 안에서 MiniDumpWriteDump로 크래시 순간을 .dmp에 기록한다.
  • 등록은 진입점 맨 앞에, 콜백은 최소한의 작업만.
  • 분석은 동일 버전의 PDB와 함께 VS나 WinDbg로.

이 구조만 심어두면, 사용자가 보내준 작은 덤프 파일 하나로 "재현 안 되는 버그"의 원인을 정확히 짚어낼 수 있습니다.

AI에게 물어볼 때 (프롬프트 팁)

크래시 덤프 작업은 AI에게 맥락을 충분히 주면 디버깅 시간이 크게 줄어듭니다. 막연히 "덤프 떴는데 왜 죽었어?"라고 묻기보다, 아래처럼 구체적으로 요청하세요.

  • 콜백 코드 리뷰 요청: "다음은 MFC 프로그램의 SetUnhandledExceptionFilter 콜백입니다. 크래시 순간 호출되는 함수인데, 핸들 누수·재진입 위험·스택 손상 시 동작 가능성 관점에서 검토하고 더 안전하게 고쳐주세요. 빌드는 Visual Studio 2022, x64 릴리스입니다." (코드 첨부)
  • WinDbg 출력 해석: "WinDbg에서 !analyze -v를 돌린 결과입니다. 예외 코드와 폴트 모듈을 해석하고, 의심되는 원인 3가지를 가능성 높은 순서로 정리한 뒤 각각 확인 방법을 알려주세요." (출력 붙여넣기)
  • 덤프 옵션 설계: "배포용 데스크톱 앱입니다. 사용자가 부담 없이 전송할 수 있는 크기와 디버깅 정확도를 동시에 만족하는 MINIDUMP_TYPE 조합을 추천하고, 각 옵션이 파일 크기와 분석 가능 범위에 미치는 영향을 표로 정리해주세요."

이처럼 환경(컴파일러·빌드 구성·아키텍처)과 목적을 함께 제시하면 AI가 추측을 줄이고 실제로 적용 가능한 답을 줍니다. 더 정교한 질문 설계가 궁금하다면 Prompt Architect의 프롬프트 분석 도구로 자신의 질문이 얼마나 구체적인지 점검해 보는 것도 방법입니다.