이 글은 Kuaaan님께서 정리하신 글입니다.
이 포스트에서는 WinDbg를 사용해 Application 메모리 덤프 (말하자면, 유저 메모리덤프)를 분석하는 방법을 설명합니다. WinDbg를 사용해본 적이 없는 초보자를 대상으로 하는 글이며, 메모리덤프 작성하는 방법을 모르시는 분은 아래의 글을 먼저 읽어 주세요.
http://kuaaan.tistory.com/213
우선, Null 포인터에 쓰기를 시도하여 Access Violation을 일으키는 샘플 프로그램을 하나 작성해 보겠습니다.
- #include "stdafx.h"
- #include <windows.h>
- void funcC(INT x, INT y, INT z)
- {
- LPINT pInt = NULL;
- *(pInt) = x + y + z;
- }
- void funcB(INT c, INT d, INT e)
- {
- funcC(c, d, e);
- }
- void funcA(INT a, INT b, INT c)
- {
- funcB(a, b, c);
- }
- int _tmain(int argc, _TCHAR* argv[])
- {
- funcA(1, 2, 3);
- return 0;
- }
위와 같은 프로그램을 실행시키면 "funcC" 함수에서 Access Violation을 발생시키고 비정상 종료되겠지요?
이제 메모리덤프 설정을 한 후 샘플 프로그램을 실행시켜 Access Violation이 발생할 때의 메모리덤프를 얻습니다. (메모리덤프를 얻는 방법은 여기 참고)
메모리덤프를 얻었다고 치고, 분석하는 방법을 간단하게 설명해 보겠습니다.
1. WinDbg 설치
메모리덤프를 분석할 PC(Debugger PC)에 WinDbg를 다운로드하고 설치합니다. 이부분은 생략.
2. pdb 파일 확보
디버깅할 모듈의 디버그심볼 (pdb 파일)을 확보합니다.
디버그심볼이란, 메모리의 어느 번지가 어느 함수 시작점이고, 어느 번지가 어느함수 몇번째줄이고, 어느 번지가 어떤 변수가 저장된 지점이라는 등 디버깅하는데 필요한 정보를 저장하고 있는 파일입니다. 우리가 디버그모드로 빌드한 것을 디버깅할 수 있는것도 사실은 디버그 심볼이 있기 때문에 가능한 일이지요.
메모리덤프를 분석하기 위한 관건은 바로 디버그심볼을 잘 관리하는 것입니다. 디버그심볼을 얻기 위한 설정은 아래의 포스트를 참고하세요.
http://kuaaan.tistory.com/104
3. WinDbg 실행
WinDbg를 실행합니다. 만약 Vista 이후의 OS라면 관리자 권한으로 실행해야 합니다.
4. Dump File Open
"File" 메뉴에서 "Open Crash Dump"를 선택하여 덤프파일을 Open니다. 그냥 덤프파일을 Drag & Drop해도 됩니다.
메모리덤프를 오픈하면 다음과 같은 초기화면이 열립니다.
5. pdb 경로 설정
디버거가 디버그 심볼을 인식할 수 있도록 pdb 경로를 설정합니다.
심볼 경로를 설정하는 방법은 인터넷에 잘 나와있습니다만 예를 들면 아래와 같이 합니다.
위의 설정은 두개의 Symbol Path를 지정합니다.
- Microsoft Symbol Server에서 필요한 심볼을 D:\Symbol\WebSymbol에 다운로드받아 사용해라. (이 심볼은 Win API등을 분석하는데 사용됩니다.)
- D:\MySymbol 폴더에 있는 심볼파일을 사용해라. (이 부분엔 VisualStudio에서 심볼이 생성되는 경로를 적어주면 되겠습니다.)
심볼 경로를 입력한 후 좌측 아래의 "Reload" 체크박스를 체크한 후 "OK"버튼을 클릭합니다. "Reload"를 체크하지 않았다면 다음의 WinDbg 명령으로도 대신할 수 있습니다.
만약, pdb가 메모리덤프와 동일 폴더에 존재한다면, 상기 심볼경로 설정은 하지 않아도 됩니다.
6. 메모리 덤프 분석
이제 메모리덤프를 분석할 준비가 끝났습니다.
CallStack을 분석하거나, Memory 번지를 열어보거나, Register 값을 확인하는 등, Crash가 발생한 상황의 정보들을 분석할 수 있지만, 일단 아래와 같은 마법의 주문을 외어서 WinDbg에게 분석을 시켜봅니다.
그러면 아래와 같이 WinDbg가 알아서 분석을 해줍니다.
와우, 크래쉬가 발생한 지점을 정확하게 보여주고 왜 죽었는지를 설명해주는군요. 이 "!analyze -v" 명령 한방이면 왠만한 상황의 디버깅은 끝납니다.
게다가 빌드 당시와 동일한 절대경로에 소스코드가 존재할 경우, 소스코드를 자동으로 찾아서 연결해줍니다. 따라서, 해당 모듈을 빌드한 PC에서 메모리덤프를 분석한다면, 알아서 소스코드가 연결됩니다.
만약 연결되지 않는다면 "File" > "Source File Path" 메뉴에서 아래와 같이 소스코드 경로를 설정해주어야 합니다. (소스코드 경로는 정확히 cpp파일이 있는 위치를 입력해도 되지만, Solution Dir을 입력해도 됩니다)
"!analyze -v" 명령을 이용한 자동분석으로 충분한 분석이 되지 않을 경우, 디버거의 여러가지 기능을 이용해 추가적인 분석을 진행합니다. 우선 "View" 메뉴를 눌러보면 아래와 같은 여러개의 추가 Window를 열 수 있습니다.
여기서 CallStack 을 선택하면 아래와 같이 Crash 발생시점의 CallStack 확인이 가능합니다.
위의 CallStack 창에서 특정 라인을 더블클릭하면 해당 라인의 Source Code가 연결됩니다.
만약 다른 Thread의 CallStack이 궁금하다면 "View" 메뉴에서 "Processes and Threads" 창을, 변수의 값을 확인하고 싶다면 "Locals"창을, 특정 메모리번지를 확인하고 싶다면 "Memory"창을 열면 됩니다. VisualStudio와 같은 다른 디버거 사용법과 크게 다를바 없습니다.
"!analyze -v" 실행했을 때의 레포트 예입니다.
레포트 중간중간에 파란색으로 하이퍼링크가 걸린 곳이 있는데 이부분을 클릭하면 추가적인 정보를 볼 수 있습니다.
CrashTest!funcC+17 [d:\my documents\1. test code\crashtest\crashtest\crashtest.cpp @ 11]
00407317 8901 mov dword ptr [ecx],eax
EXCEPTION_RECORD: ffffffff -- (.exr 0xffffffffffffffff)
ExceptionAddress: 00407317 (CrashTest!funcC+0x00000017)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00000000
Attempt to write to address 00000000
PROCESS_NAME: CrashTest.exe
ADDITIONAL_DEBUG_TEXT:
Use '!findthebuild' command to search for the target build information.
If the build information is available, run '!findthebuild -s ; .reload' to set symbol path and load symbols.
FAULTING_MODULE: 77b00000 ntdll
DEBUG_FLR_IMAGE_TIMESTAMP: 4c629f39
ERROR_CODE: (NTSTATUS) 0xc0000005 - 0x%08lx
EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - 0x%08lx
EXCEPTION_PARAMETER1: 00000001
EXCEPTION_PARAMETER2: 00000000
WRITE_ADDRESS: 00000000
FOLLOWUP_IP:
CrashTest!funcC+17 [d:\my documents\1. test code\crashtest\crashtest\crashtest.cpp @ 11]
00407317 8901 mov dword ptr [ecx],eax
FAULTING_THREAD: 000002f0
BUGCHECK_STR: APPLICATION_FAULT_NULL_POINTER_READ_NULL_POINTER_WRITE_WRONG_SYMBOLS
PRIMARY_PROBLEM_CLASS: NULL_POINTER_READ
DEFAULT_BUCKET_ID: NULL_POINTER_READ
LAST_CONTROL_TRANSFER: from 00407334 to 00407317
STACK_TEXT:
0012fefc 00407334 00000001 00000002 00000003 CrashTest!funcC+0x17 [d:\my documents\1. test code\crashtest\crashtest\crashtest.cpp @ 11]
0012ff10 00407354 00000001 00000002 00000003 CrashTest!funcB+0x14 [d:\my documents\1. test code\crashtest\crashtest\crashtest.cpp @ 16]
0012ff24 0040736e 00000001 00000002 00000003 CrashTest!funcA+0x14 [d:\my documents\1. test code\crashtest\crashtest\crashtest.cpp @ 21]
0012ff38 004011d2 00000001 002122e8 00212348 CrashTest!wmain+0xe [d:\my documents\1. test code\crashtest\crashtest\crashtest.cpp @ 26]
0012ff88 77a71194 7ffd5000 0012ffd4 77b5b495 CrashTest!__tmainCRTStartup+0x15e [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 327]
WARNING: Stack unwind information not available. Following frames may be wrong.
0012ff94 77b5b495 7ffd5000 778a33a8 00000000 kernel32!BaseThreadInitThunk+0x12
0012ffd4 77b5b468 00401229 7ffd5000 00000000 ntdll!RtlInitializeExceptionChain+0x63
0012ffec 00000000 00401229 7ffd5000 00000000 ntdll!RtlInitializeExceptionChain+0x36
STACK_COMMAND: ~0s; .ecxr ; kb
FAULTING_SOURCE_CODE:
7:
8: void funcC(INT x, INT y, INT z)
9: {
10: LPINT pInt = NULL;
> 11: *(pInt) = x + y + z; // Crash!!
12: }
13:
14: void funcB(INT c, INT d, INT e)
15: {
16: funcC(c, d, e);
SYMBOL_STACK_INDEX: 0
SYMBOL_NAME: CrashTest!funcC+17
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: CrashTest
IMAGE_NAME: CrashTest.exe
BUCKET_ID: WRONG_SYMBOLS
FAILURE_BUCKET_ID: NULL_POINTER_READ_c0000005_CrashTest.exe!funcC
WATSON_STAGEONE_URL: http://watson.microsoft.com/StageOne/CrashTest_exe/0_0_0_0/4c629f39/CrashTest_exe/0_0_0_0/4c629f39/c0000005/00007317.htm?Retriage=1
Followup: MachineOwner
---------
다른 부분은 그냥 읽여보면 되는데요... CallStack 설명한 부분을 잠시 보겠습니다.
0012fefc 00407334 00000001 00000002 00000003 CrashTest!funcC+0x17 [d:\my documents\1. test code\crashtest\crashtest\crashtest.cpp @ 11]
위의 정보의 의미는 다음과 같습니다.
- 제일 좌측의 숫자 (0012fefc) : 해당 함수 실행 시점의 EBP 값. 이 EBP가 가리키는 위치의 메모리 주소의 값들을 4바이트씩 끊어서 나타낸 것이 다음에 나오는 4개의 16진수입니다.
- 두번째 숫자 (00407334) : EBP가 가리키는 주소 (0012fefc) 의 4바이트 값. 해당 함수를 호출한 이전 함수의 EBP를 의미합니다.
- 세번째 ~ 다섯번째 숫자 (00000001 00000002 00000003) :CrashTest 모듈의 funcC 함수에 전달된 파라메터를 좌측부터 세개까지 나타냅니다.
- CrashTest!funcC+0x17 : CrashTest 모듈(exe 혹은 dll 이름)의 funcC 함수 시작점에서 0x17바이트 떨어진 지점을 의미합니다.
- [d:\my documents\1. test code\crashtest\crashtest\crashtest.cpp @ 11] : 아시죠? crashtest.cpp의 11번째 줄을 의미합니다.
7. 비고
어떤가요? 메모리 덤프 분석 어렵지 않죠?
메모리 덤프 분석을 하는데 있어서 가장 중요한 것은 Symbol 관리입니다. 모든 릴리즈되는 모듈의 바이너리와 pdb 파일이 이력관리되어야 하죠. 가장 좋은 방법은 Symbol Store라 불리우는 심볼서버를 구성하는 방법이구요, 심볼을 팀원간에 공유할 필요성이 별로 없다면 SVN 등으로 관리하는 방법도 괜찮을 것 같습니다.
pdb 파일은 파일 이름만 달라져도 Debugger가 인식하지 못하므로 Rename해선 안됩니다. 또한, 소스코드가 동일하더라도 Build한 TimeStamp가 달라지면 pdb를 인식하지 못하므로 정확히 빌드한 산출물 Binary와 함께 만들어진 pdb는 한 짝으로 관리되어야 합니다.
심볼스토어 구성하는 방법은 아래 포스트에 잘 설명되어 있네요.
http://nyolong.egloos.com/1370997