C++/MFC 프로그램 크래시 자동 덤프(.dmp) 만들기: MiniDumpWriteDump + SetUnhandledExceptionFilter 실전 가이드
TL;DR — 사용자 PC에서만 죽는 C++/MFC 프로그램, 재현이 안 돼 막막할 때. SetUnhandledExceptionFilter와 MiniDumpWriteDump로 크래시 순간을 .dmp 파일로 남겨 WinDbg로 원인을 추적하는 방법을 단계별로 정리했습니다.
"내 PC에서는 잘 되는데요"라는 함정
C++/MFC로 만든 데스크톱 프로그램을 배포하다 보면 가장 곤란한 상황이 있습니다. 개발자 PC에서는 멀쩡하게 돌아가는데, 특정 사용자 환경에서만 프로그램이 갑자기 사라지듯 종료되는 경우입니다. 로그도 안 남고, 에러 메시지조차 뜨지 않고, 그저 "프로그램이 그냥 꺼졌어요"라는 제보만 돌아옵니다.
이런 비결정적(non-deterministic) 크래시는 디버거를 붙여 재현할 수가 없기 때문에 원인 추적이 사실상 불가능에 가깝습니다. 메모리 접근 위반(Access Violation), 널 포인터 역참조, 스택 손상 같은 문제는 발생 환경의 미묘한 차이에 따라 나타났다 사라졌다 하기 때문입니다.
해결책은 의외로 단순합니다. 크래시가 발생하는 그 순간, 프로세스의 상태를 통째로 파일로 떠놓는 것입니다. 이렇게 저장된 파일을 미니덤프(minidump, .dmp)라고 부르며, 나중에 WinDbg나 Visual Studio에서 열어보면 마치 그 순간으로 시간을 되돌린 것처럼 콜스택과 변수 상태를 들여다볼 수 있습니다.
이 글에서는 Windows API 두 개, SetUnhandledExceptionFilter와 MiniDumpWriteDump만으로 자동 크래시 덤프 시스템을 구축하는 방법을 다룹니다.
핵심 원리: 처리되지 않은 예외를 가로채기
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_INFORMATION의ThreadId는 크래시가 난 스레드를 가리켜야 합니다. 콜백은 보통 해당 스레드 컨텍스트에서 호출되므로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의 프롬프트 분석 도구로 자신의 질문이 얼마나 구체적인지 점검해 보는 것도 방법입니다.