개념
프로그램 실행 중 예외처리 되지 않은 오류나 크래쉬 등이 발생했을 때, 해당 시점의 프로그램 상태 정보를 담고 있는 .dmp 확장자를 가진 파일을 말한다.
덤프 파일은 그 시점에서의 변수, 메모리, 콜스택 등의 정보를 가지는데 이를 Visual studio에서 실행하면 디버거를 통해 디버깅이 가능하다. 덤프 파일은 시스템에 의해 자동으로 생성되는 것이 아니라 직접 프로그램 내에 코드를 작성해야 한다.
대략적인 원리
VS 디버거 동작
우선 VS에서 디버깅이 어떻게 수행 되는 것인지 알 필요가 있다. 디버거로 프로그램을 실행하면 중단점을 설정하여 변수 값을 확인하거나 다른 스택 프레임으로 이동하는 등의 동작을 할 수 있다. 프로그램을 실행한다는 것은 빌드를 통해 만들어낸 바이너리 파일을 실행한다는 것인데, 보통 이 바이너리 파일에는 소스코드 정보가 담겨 있지 않다고 한다.
디버거가 우리에게 소스코드 레벨에서의 디버깅 환경을 제공하려면 두 가지가 필요하다.
- pdb 파일
- 소스 파일
디버거는 소스코드 레벨에서 설정된 중단점 정보를 얻어내고, 바이너리에서 그에 대응하는 곳은 어디인지 알아야 할 필요가 있다. 이를 위해 디버거는 pdb 파일을 참조한다.
pdb 파일(program database)
pdb 파일은 기호 파일 혹은 심볼 파일이라고도 불린다. 이 파일은 다음과 같은 디버깅에 필요한 정보를 담고 있으며, 보통 빌드 시 생성된다.
(빌드 시 생성되지 않는다면 프로젝트 속성 - 링커 - 디버깅 확인)
- 변수, 함수 이름, 바이너리와 소스코드 간 맵핑 데이터 및 소스 라인 번호 등 (디버거가 읽을 수 있는 형태)
- 변수 및 메모리 정보
pdb 파일이 소스 파일 자체를 포함하는 것은 아니기 때문에, 화면에 소스 코드를 보여주면서 디버깅 하려면 소스파일 또한 필요하다. 그렇지 않으면 아마 다음과 비슷한 화면이 나오며 어셈블리 레벨에서만 디버깅이 가능할 것이다.
ms에서 제공하는 모듈에 대한 pdb 파일은 ms 심볼 서버를 이용하면 다음과 같이 정해진 경로에 다운된다. 하지만 소스 파일이 없다면 소스 레벨 디버깅은 안된다.
(심볼 서버는 덤프 디버깅에 필요한 pdb 파일을 내려받을 수 있는 서버를 말하는 것 같다. 외부 라이브러리에 대한 pdb 파일은 가지고 있지 않을 수 있으므로)
참고로 Debug - Windows - Modules를 클릭하면 심볼(pdb) 로드 상태와 소스 파일 존재 여부(User Code)를 확인할 수 있다. 아마 두 데이터가 모두 있어야 소스 코드 레벨에서 디버깅이 가능할 것으로 보임
덤프 파일
덤프 파일은 크래쉬 발생 상황에 대한 메모리, 스레드, 프로세스 등의 상태를 저장하고 있다.
VS 디버거는 덤프, pdb, 소스 파일을 이용해 크래쉬 상태를 재현하고 디버깅 환경을 구현한다.
물론 정상적인 디버깅을 위해선 덤프 파일을 생성한 exe 파일과 버전이 일치하는 pdb, 소스 파일이 필요하다.
덤프 파일 생성
덤프 파일은 크래쉬 발생 시 원래 시스템이 처리할 에러를 내 프로그램이 처리하도록 하고, 이때 인자로 넘어온 현재 메모리 상태 등의 정보를 직접 파일에 write하여 덤프 파일을 생성하는 방식이다.
크게 다음과 같은 단계로 나뉜다.
- 크래쉬 발생 시 시스템이 아닌 내 프로그램이 처리하도록 설정
- 크래쉬 발생 시 덤프 파일 생성할 콜백 함수 설정
- 콜백 함수 구현
- 파일 생성 및 인자로 넘어온 데이터 write
#include <DbgHelp.h>
bool BeginDump();
bool EndDump();
LONG WINAPI UnHandledExceptionFilter(_EXCEPTION_POINTERS* pExceptionInfo); // 콜백 함수
// 최상위 예외 처리기 백업할 전역 변수
LPTOP_LEVEL_EXCEPTION_FILTER PrevExceptionFilter = NULL;
bool BeginDump()
{
// 1번 단계
// SetErrorMode: 프로그램 에러 발생 시 시스템에서 발생하는 메시지/대화상자 발생 여부를 제어
// SEM_FAILCRITICALERRORS: 에러 발생 시 시스템에서 처리하지 않고 프로세스에게 오류를 전달 (대신 처리하도록)
SetErrorMode(SEM_FAILCRITICALERRORS);
// 2번 단계
// SetUnhandledExceptionFilter: 크래쉬 발생 시 인자로 전달한 함수가 콜백된다. 인자로 전달한 함수가 최상위 예외 처리기가 되는 듯 하다.
// 크래쉬 발생 시 크래쉬 위치 정보를 얻기 위함이라 한다.
// 반환 값은 이전 최상위 예외 처리기이다.
PrevExceptionFilter = SetUnhandledExceptionFilter(UnHandledExceptionFilter);
return true;
}
bool EndDump()
{
// 설정 기존으로 되돌리기
SetUnhandledExceptionFilter(PrevExceptionFilter);
return true;
}
///////////////
// 3번 단계
///////////////
// Unhandled exception 발생 시 호출될 콜백 함수 타입
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
);
LONG WINAPI UnHandledExceptionFilter(_EXCEPTION_POINTERS* pExceptionInfo)
{
// 덤프 데이터 write에 필요한 함수를 담고 있는 dll 로드
HMODULE hDll = LoadLibrary(TEXT("DBGHELP.DLL"));
if (hDll != NULL)
{
// dll에서 MiniDumpWriteDump 함수 가져온다.
MINIDUMPWRITEDUMP WriteDumpFunc = reinterpret_cast<MINIDUMPWRITEDUMP>(GetProcAddress(hDll, "MiniDumpWriteDump"));
if (WriteDumpFunc != NULL)
{
// 덤프 파일 이름 설정 ( MyWinFramework_YYMMDD_HHMMSS.dmp )
// 덤프 파일은 exe 파일의 버전 및 빌드와 일치해야 하기 때문에 exe 파일의 버전을 얻을 수 있다면 그 정보를 활용할 수 있으면 하면 좋다.
// 근데 우선은 날짜 시간만
TCHAR DumpName[MAX_PATH] = { NULL, };
SYSTEMTIME SystemTime;
ZeroMemory(&SystemTime, sizeof(SystemTime));
GetLocalTime(&SystemTime);
_snwprintf_s(DumpName, MAX_PATH, TEXT("MyWinFramework_%02d%02d%02d_%02d%02d%02d.dmp"),
SystemTime.wYear % 1000, SystemTime.wMonth, SystemTime.wDay,
SystemTime.wHour, SystemTime.wMinute, SystemTime.wSecond
);
// 파일 생성
HANDLE hDump = CreateFile(DumpName,
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
// 덤프에 exception 정보 write
if (hDump != NULL)
{
MINIDUMP_EXCEPTION_INFORMATION DumpExceptionInfo;
ZeroMemory(&DumpExceptionInfo, sizeof(DumpExceptionInfo));
DumpExceptionInfo.ThreadId = GetCurrentThreadId();
DumpExceptionInfo.ExceptionPointers = pExceptionInfo;
DumpExceptionInfo.ClientPointers = FALSE;
BOOL bSuccess = WriteDumpFunc(GetCurrentProcess(), GetCurrentProcessId(),
hDump, MINIDUMP_TYPE::MiniDumpNormal, &DumpExceptionInfo,
NULL, NULL);
CloseHandle(hDump);
if (bSuccess == TRUE)
return EXCEPTION_EXECUTE_HANDLER;
else
return EXCEPTION_CONTINUE_SEARCH;
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
그리고 다음과 같이 프로그램의 시작과 끝에 덤프 생성 설정
int main()
{
BeginDump();
...
EndDump();
return 0;
}
만약 위 프로그램 빌드 후 생성된 exe 파일 실행 후 크래시가 발생한다면 dmp 파일이 생성된다.
이 dmp 파일을 비주얼 스튜디오로 실행하면 다음과 같이 우측의 Debug with Native Only를 클릭하여 디버깅이 가능하다.
만약 이 프로그램이 유저에게 배포 되었다면, 유저가 dmp 파일을 가지고 있을 것이다.
해당 덤프를 어떤 경로로든 받으면, 해당 덤프 버전과 일치하는 pdb, 소스 파일을 찾아 VS로 디버깅을 하면된다.
이를 위해선 덤프 파일 이름에 버전 정보를 포함해야 하지 않을까 싶다.
만약 pdb 이름을 변경하거나 삭제해보면 다음과 같이 pdb를 찾을 수 없다고 심볼 서버에서 로드하거나 다른 방법을 찾아보라고 할 것이다. 그리고 콜스택 쪽을 보면 함수 이름이라던지 그런 정보가 나와 있지 않다. 이는 소스와 바이너리의 관계를 찾을 수 없어 그냥 함수 이름 대신 주소로 표현된 것이다. pdb 없이 디버깅 하려면 콜스택에서 스택 프레임 우클릭 후 어셈블리로 디버깅해야 한다.
레퍼런스
'C,C++ > Debugging things' 카테고리의 다른 글
dumpbin을 사용하여 .lib 파일 내의 함수 정보 얻기 (0) | 2023.04.27 |
---|---|
윈도우 메모리 릭 디버깅 슬라이드셰어 (0) | 2021.10.04 |
[C++] CRT 라이브러리로 디버깅 시 메모리 누수 찾기 (0) | 2020.02.17 |
[c++] Visual studio 2019 환경에서 디버깅 하기 / debugging, memory, disassemble, 디버그, 디버거 (0) | 2019.10.15 |
댓글