유저모드 덤프 디버깅
덤프 디버깅
만약 Windbg를 프로세스나 시스템에 붙여놓은 상황이었다면 디버거는 응용프로그램이 종료되기 전이나 시스템이 재부팅되기전에 문제가 발생한 상황을 먼저 잡아 분석할 기회를 제공한다.
즉, 라이브 디버깅을 하던 중에 문제가 발생한 경우라면 즉시 디버거의 여러 가지 기능을 이용해 문제를 분석할 수 있다.
디버거를 붙여놓지 않은 상황에서 응용프로그램이 갑자기 종료되거나 시스템이 재부팅될 때 윈도우는 크래시 덤프(Crash Dump)파일을 만들어 놓는다.
문제 발생 당시의 메모리를 그대로 파일로 저장해서 나중에 WinDbg로 분석할 수 있게 한다.
나중에 WinDbg로 덤프 파일을 열면 문제가 발생한 시점 그대로의 실행 상태, 메모리 상태 등의 디버깅 환경을 재현해준다.
이는 라이브 디버깅 중 문제가 발생했을 때 디버거가 잡아놓은 상황과 동일하다고 보면 된다.
크래시 덤프 파일은 메모리 덤프 파일이라고도 부른다.
WinDbg로 덤프 파일을 연 뒤 디버거가 친절히 설명해주는 메시지를 보거나 디버거가 하라는 대로 하는 것만으로도 기본적인 분석을 할 수 있다.
또한 F1키를 눌러 볼 수 있는 도움말에는 아주 많은 정보가 있어서, 적절히 이용만 하면 기본적인 디버깅을 할 수 있고 디버깅 능력을 향상시킬 수 있다.
덤프 파일 수집
윈도우 비스타부터 윈도우 10까지는 유저모드 덤프 파일 수집 방법이 동일하지만 윈도우 XP는 다르다.
(윈도우 XP의 유저모드 덤프 수집 방법은 책을 참고하자)
윈도우 10(윈도우 비스타 이후 버전을 대표로 총칭)의 경우 유저모드에서 응용프로그램 오류가 발생하면 MS로 오류 정보를 전송하고 시스템에는 덤프 파일을 남기지 않는다.
만약 덤프 파일을 수집하고 싶다면 별도의 설정을 해야한다.
가장 간단한 방법은 다음과 같다.
LocalDumps 레지스트리 키를 생성하고, DumpType 값을 생성한다.
- 키 생성 : HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps
- 값 생성 : DumpType = 2(REG_DWORD)
DumpType은 1이 '미니 덤프', 2가 '전체 덤프'이다.
DumpFolder 값을 통해 덤프 파일이 생성될 경로를 지정할 수도 있지만, 지정하지 않는 경우 기본값인 "%LOCALAPPDATA%\CrashDumps" 위치에 생성된다.
책의 실습 자료 중 하나인 MyApp.exe 프로그램을 통해 크래시를 발생시켜 덤프 파일이 생성되는 것을 실습했다.
Dafault 경로에 덤프 파일이 생성됐으며, 프로세스 이름과 당시의 PID가 조합된 파일명인 것을 알 수 있다.
또한, 파일 크기가 약 91MB인 것을 보면 프로세스 실행 당시 프로세스의 전체 메모리 사용량이 91MB 정도였음을 알 수 있다. (전체 덤프 설정)
이렇게 생성된 덤프 파일을 바로 열어서 분석하거나, 다른 위치로 복사해놓고 분석을 시작하면 된다.
덤프 파일 열기
확보한 크래시 덤프 파일을 WinDbg의 File 메뉴에서 Open Crash Dump... 를 선택해 열거나, 드래그 앤 드롭을 통해 열어본다.
Microsoft (R) Windows Debugger Version 10.0.17134.12 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Users\whwo0\AppData\Local\CrashDumps\MyApp.exe.9052.dmp]
User Mini Dump File with Full Memory: Only application data is available
************* Path validation summary **************
Response Time (ms) Location
Deferred SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols
Symbol search path is: SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows 10 Version 18362 MP (4 procs) Free x64
Product: WinNt, suite: SingleUserTS Personal
18362.1.amd64fre.19h1_release.190318-1202
Machine Name:
Debug session time: Sun Apr 12 16:21:29.000 2020 (UTC + 9:00)
System Uptime: 0 days 1:17:43.849
Process Uptime: 0 days 0:00:06.000
................................................
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(235c.1650): Access violation - code c0000005 (first/second chance not available)
*** WARNING: Unable to verify checksum for MyApp.exe
*** ERROR: Module load completed but symbols could not be loaded for MyApp.exe
MyApp+0x8f36:
00007ff7`9f138f36 88040a mov byte ptr [rdx+rcx],al ds:00000000`00000000=??
두껍게 표시한 부분 중 첫 번째 부분은 덤프 파일 경로와 이 덤프 파일이 어떤 유형의 덤프인지 표시한다.
현재 덤프 파일은 'User Mini Dump File with Full Memory' 라고 나오는데 이 '전체 메모리 미니 덤프' 파일은 프로그램이 사용하던 모든 메모리가 유효하다고 알려준다.
일반적으로 유저모드에서는 이 것을 '전체 덤프'라고 한다.
반면 일반적인 '미니 덤프'는 레지스터와 스택, 메모리 포인터만 유효하다.
따라서 '미니 덤프'를 통해서는 해당 덤프에 포함된 메모리의 일부만 확인할 수 있다.
- 전체 덤프 : User Mini Dump File with Full Memory: Only application data is available
- 미니 덤프 : User Mini Dump File: Only registers, stack and protions of memory are available
덤프 파일 유형을 확인해야 어느 선까지 확인할 수 있는지 알 수 있으므로, 유형 확인은 분석 첫 단계에 수행해야한다.
다음 두 번째 부분은 덤프가 발생한 운영체제의 종류를 표시한다.
Windows 10 Version 18362 x64임을 확인할 수 있다.
그 다음은 덤프가 발생한 시간을 표시한다.
각종 이벤트 로그를 해당 시간과 비교할 수도 있으며, 해당 시간대의 환경적인 특수성 (출-퇴근 시간대 등)을 파악하는 것은 꽤나 중요한 일이다.
그 다음 부분에 나오는 'Process Uptime'은 프로세스가 실행된 후 얼마나 오랫동안 동작하고 있었는지 알 수 있는 정보다.
이를 통해 실행 직후 문제가 발생했는지, 아니면 몇 시간 뒤에 문제가 발생한 것인지 등을 알 수 있다.
예제에 사용한 덤프에서는 프로세스가 시작된지 6초만에 문제가 발생했다.
그 다음은 .ecxr 명령을 통해 저장된 예외 정보에 접근할 수 있다는 메시지이다.
어떤 프로세스에 예기치 않은 오류가 발생하면 덤프 파일에는 예외에 대한 정보가 저장된다.
WinDbg는 .ecxr 명령을 제공해 예외 정보를 읽고, 예외가 발생한 당시의 상태로 돌려놓고 분석을 할 수 있도록 한다.
이처럼 WinDbg가 보여주는 메시지에는 분석을 어떻게 해야하는지 제시하는 내용들이 심심치 않게 나온다.
따라서 주의깊게 읽어보고 WinDbg에서 하라고 하는 것이 있으면 그대로 해보는 것이 좋다.
.ecxr 명령에 대한 설명은 WInDbg의 Help 메뉴를 펼치고 Index를 클릭해 도움말에서 찾을 수 있따.
조금만 설명해보자면 예외가 발생한 상황에 저장된 '컨텍스트 레코드(Context Record)'를 보여주고 디버거가 해당 내용을 참조하게 하는 명령이다.
※컨텍스트 레코드 : 단순하게 설명하면 CPU 레지스터 셋(Register Set)을 의미한다. 다시 말하자면 예외가 발생했을 당시에 저장된 레지스터 셋을 보여주고 이 상태로 되돌려준다.
.ecxr 명령을 수행하여 WinDbg가 문제가 발생했던 당시의 레지스터들을 로드하고 보여주도록 해보자.
0:000> .ecxr
rax=0000000000000055 rbx=0000000000000001 rcx=0000000000000000
rdx=0000000000000000 rsi=00000008ddaff9b0 rdi=00007ff79f40ee60
rip=00007ff79f138f36 rsp=00000008ddafe860 rbp=0000000000000111
r8=00007ff79f40e430 r9=7efefefefefeff67 r10=00007ff79f1399a0
r11=8101010101010100 r12=00000000000003f9 r13=0000000000020818
r14=0000000000000000 r15=00000000000003f9
iopl=0 nv up ei ng nz ac pe cy
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010291
MyApp+0x8f36:
00007ff7`9f138f36 88040a mov byte ptr [rdx+rcx],al ds:00000000`00000000=??
이제 k 명령(Display Stack Backtrace)을 통해 콜 스택을 조회하여 문제가 발생한 흐름을 살펴보자.
0:000> k
*** Stack trace for last set context - .thread/.cxr resets it
# Child-SP RetAddr Call Site
00 00000008`ddafe860 00007ff7`9f1399f4 MyApp+0x8f36
01 00000008`ddafe8a0 00007ff7`9f13c936 MyApp+0x99f4
02 00000008`ddafe8f0 00007ff7`9f13c61e MyApp+0xc936
03 00000008`ddafe930 00007ff7`9f13d962 MyApp+0xc61e
04 00000008`ddafe990 00007ff7`9f153363 MyApp+0xd962
05 00000008`ddafe9d0 00007ff7`9f15448d MyApp+0x23363
06 00000008`ddafea60 00007ff7`9f15678b MyApp+0x2448d
07 00000008`ddafebe0 00007ff7`9f14f20d MyApp+0x2678b
08 00000008`ddafec20 00007ff7`9f14fce8 MyApp+0x1f20d
09 00000008`ddafed20 00007ff8`105f5c0d MyApp+0x1fce8
0a 00000008`ddafed60 00007ff8`105f521c user32!UserCallWinProcCheckWow+0x2bd
0b 00000008`ddafeef0 00007ff8`105f4f88 user32!SendMessageWorker+0x22c
*** ERROR: Symbol file could not be found. Defaulted to export symbols for atcuf64.dll -
0c 00000008`ddafef90 00007fff`e6e73673 user32!SendMessageW+0xf8
0d 00000008`ddafeff0 00007fff`e6e5375b atcuf64!AtcQueryRegion+0x10d93
0e 00000008`ddaff0d0 00000135`e7dd00c6 atcuf64+0x375b
0f 00000008`ddaff200 00000000`00000000 0x00000135`e7dd00c6
윗 부분이 가장 최근에 호출된 부분이므로 MyApp 내부에서 문제가 발생했음을 확인알 수 있다.
하지만 심볼이 맞춰지지 않아 어떤 함수의 어떤 라인인지 알기 힘들다. (이 상태에서는 그냥 WinDbg를 꺼버리고 싶다.)
이제 심볼을 맞춰보자.
모듈 정보 보기
정확한 디버깅의 첫 단계는 문제 모듈의 정보를 정확히 확인하는 것이다.
파일 이름, 파일 버전, 파일 날짜 등을 정확히 확인해야한다.
그래야만 해당 모듈의 정확한 소스 코드와 심볼 파일을 찾을 수 있다.
모듈 정보 확인은 lm 명령을 사용한다. lm 명령은 원래 로드된 모든 모듈을 확인할 때 사용하는 명령이지만 vm 옵션을통해 특정 모듈 하나만 자세히 볼 수 있다.
0:000> lmvm MyApp
Browse full module list
start end module name
00007ff7`9f130000 00007ff7`9f566000 MyApp C (no symbols)
Loaded symbol image file: MyApp.exe
Image path: C:\Users\whwo0\Downloads\windbgwindbg2nd-master\windbgwindbg2nd-master\Ch2\Build\x64\Release\MyApp.exe
Image name: MyApp.exe
Browse all global symbols functions data
Timestamp: Fri May 25 20:03:39 2018 (5B08CE8B)
CheckSum: 00000000
ImageSize: 00436000
File version: 1.0.0.1
Product version: 1.0.0.1
File flags: 0 (Mask 3F)
File OS: 4 Unknown Win32
File type: 1.0 App
File date: 00000000.00000000
Translations: 0412.04b0
Information from resource tables:
CompanyName:
ProductName: MyApp 응용 프로그램
InternalName: MyApp
OriginalFilename: MyApp.EXE
ProductVersion: 1, 0, 0, 1
FileVersion: 1, 0, 0, 1
PrivateBuild:
SpecialBuild:
FileDescription: MyApp MFC 응용 프로그램
LegalCopyright: Copyright (C) 2008
LegalTrademarks:
Comments:
MyApp의 시작 주소와 끝 주소 실행 위치, 파일 이름, 만들어진 날짜/시간은 자주 참고하는 내용이므로 눈여겨 봐두자.
특히 만들어진 날짜/시간은 같은 시간에 만들어진 심볼 파일(pdb 파일)을 찾을 때 사용하므로 잘 확인한다.
여기서는 여기서는 2018년 5월 25일 20:03:39초에 만들어진 MyApp.pdb 파일을 준비해야한다. (exe파일이 먼저 생성하고 pdb가 생성되므로 초 단위는 1~2초 정도 차이가 날 수 있다.)
(no symbols) 부분은 MyApp.exe와 일치하는 심볼 파일이 로드되면 심볼 파일의 경로와 이름이 표시되는 곳이다. 현재는 심볼이 로드되지 않았음을 확인할 수 있다.
심볼 맞추기
예제의 심볼 파일을 사용 중인 심볼캐시 디렉토리 경로에 복사하자.
0:000> lmvm MyApp
Browse full module list
start end module name
00007ff7`9f130000 00007ff7`9f566000 MyApp C (private pdb symbols) c:\users\whwo0\downloads\windbgwindbg2nd-master\windbgwindbg2nd-master\ch2\build\x64\release\MyApp.pdb
Loaded symbol image file: MyApp.exe
Image path: C:\Users\whwo0\Downloads\windbgwindbg2nd-master\windbgwindbg2nd-master\Ch2\Build\x64\Release\MyApp.exe
Image name: MyApp.exe
Browse all global symbols functions data
Timestamp: Fri May 25 20:03:39 2018 (5B08CE8B)
CheckSum: 00000000
ImageSize: 00436000
File version: 1.0.0.1
Product version: 1.0.0.1
File flags: 0 (Mask 3F)
File OS: 4 Unknown Win32
File type: 1.0 App
File date: 00000000.00000000
Translations: 0412.04b0
Information from resource tables:
CompanyName:
ProductName: MyApp 응용 프로그램
InternalName: MyApp
OriginalFilename: MyApp.EXE
ProductVersion: 1, 0, 0, 1
FileVersion: 1, 0, 0, 1
PrivateBuild:
SpecialBuild:
FileDescription: MyApp MFC 응용 프로그램
LegalCopyright: Copyright (C) 2008
LegalTrademarks:
Comments:
다시 lmvm으로 확인하면 심볼이 로드된 것이 확인된다.
(나는 심볼 캐시 디렉터리에 복사한 심볼을 잘 못 읽어서, 실습 자료를 다운받은 최초 경로를 .sympath+로 추가해서 맞춰줬다. 아무래도 심볼 캐시 적용이 잘 안 된 것 같다.)
private 심볼은 소스 파일과 변수들의 정보를 모두 포함하는 심볼 파일이다.
빌드 후에 생성되는 심볼 파일은 이런 정보를 모두 포함하므로 private 심볼이다.
public 심볼의 경우 소스 파일과 변수들 정보를 제거해 조금 덜 자세한 정보를 담고 있는 심볼 파일이다.
운영체제 심볼 파일들은 public 심볼 파일로 제공된다.
콜 스택 보기
심볼까지 맞췄으니 콜 스택을 다시 살펴보자.
0:000> k
# Child-SP RetAddr Call Site
00 00000008`ddafe860 00007ff7`9f1399f4 MyApp!CMyAppDlg::MyStrCpy+0x56 [c:\github\windbgwindbg2nd\ch2\src\myapp\myappdlg.cpp @ 477]
01 00000008`ddafe8a0 00007ff7`9f13c936 MyApp!CMyAppDlg::OnButtonUserCrash+0x54 [c:\github\windbgwindbg2nd\ch2\src\myapp\myappdlg.cpp @ 491]
02 00000008`ddafe8f0 00007ff7`9f13c61e MyApp!_AfxDispatchCmdMsg+0xee [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\cmdtarg.cpp @ 78]
03 00000008`ddafe930 00007ff7`9f13d962 MyApp!CCmdTarget::OnCmdMsg+0x196 [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\cmdtarg.cpp @ 372]
04 00000008`ddafe990 00007ff7`9f153363 MyApp!CDialog::OnCmdMsg+0x32 [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\dlgcore.cpp @ 85]
05 00000008`ddafe9d0 00007ff7`9f15448d MyApp!CWnd::OnCommand+0x9b [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\wincore.cpp @ 2800]
06 00000008`ddafea60 00007ff7`9f15678b MyApp!CWnd::OnWndMsg+0x69 [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\wincore.cpp @ 2113]
07 00000008`ddafebe0 00007ff7`9f14f20d MyApp!CWnd::WindowProc+0x3f [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\wincore.cpp @ 2099]
08 00000008`ddafec20 00007ff7`9f14fce8 MyApp!AfxCallWndProc+0x135 [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\wincore.cpp @ 265]
09 00000008`ddafed20 00007ff8`105f5c0d MyApp!AfxWndProc+0x54 [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\wincore.cpp @ 417]
0a 00000008`ddafed60 00007ff8`105f521c user32!UserCallWinProcCheckWow+0x2bd
0b 00000008`ddafeef0 00007ff8`105f4f88 user32!SendMessageWorker+0x22c
*** ERROR: Symbol file could not be found. Defaulted to export symbols for atcuf64.dll -
0c 00000008`ddafef90 00007fff`e6e73673 user32!SendMessageW+0xf8
0d 00000008`ddafeff0 00007fff`e6e5375b atcuf64!AtcQueryRegion+0x10d93
0e 00000008`ddaff0d0 00000135`e7dd00c6 atcuf64+0x375b
0f 00000008`ddaff200 00000000`00000000 0x00000135`e7dd00c6
심볼을 맞춰놓은 상태이므로 운영체제 함수들과 MyApp.exe 함수들의 이름이 정확히 보인다.
콜 스택이 정확히 보이므로 함수들의 호출 흐름을 천천히 살펴본다.
콜 스택 확인을 통해서도 문제의 원인을 추정할 수 있으며, 최소한 어디 근처에서 문제가 발생한 것인지 파악할 수 있다.
콜 스택을 통해 확인되는 호출의 흐름은 참고만 할 뿐이고, 집중해서 봐야할 곳은 마지막에 문제가 발생한 함수인 CMyAppDlg::MyStrCpy() 이다.
콜 스택 창에서 해당 함수를 더블클릭하면 소스 코드가 나타난다.
(소스 코드가 나타나지 않는다면 해당 "c:\github\windbgwindbg2nd\ch2\src\myapp\myappdlg.cpp" 경로에 소스 파일이 없기 때문이므로 MyAppDlg.cpp 파일이 있는 경로를 .srcpath+ 명령 또는 File 메뉴의 Source File Path를 선택해 추가한다.
소스 코드가 나오고 표시된 소스 라인이 콜 스택에서 보았던 MyAppDlg.cpp의 477 라인인지 다시 한 번 확인한다.
간혹 심볼을 잘못 맞췄거나 심볼은 잘 맞췄지만 MyApp.exe를 빌드한 후 소스 파일이 수정된 경우 엉뚱한 소스 라인을 보여줄 수도 있기 때문이다.
이제 문제가 발생한 이유를 추리해야한다.
먼저 함수를 보면서 왜 문제가 발생했는지 생각해보자.
소스 코드를 보면 pDest[i] = pSrc[i] 에서 문제가 발생했다.
pDest 포인터가 잘못됐을 수도 있고, pSrc 포인터가 잘못됐을 수도 있다.
또는 i가 너무 크거나 너무 작아서, 또는 음수여서 문제가 발생했을 수도 있다.
분석의 첫 번째 단계는 일단 문제가 발생한 부분에 집중하는 것이다. 이 라인에서 문제가 발생할 수 있는 시나리오를 몇 가지 생각해놓고 소스 코드를 거꾸로 거슬로 올라가면서 시나리오를 만족하는 동작들이 있었는지 확인한다.
함수의 처음부터 문제가 발생한 라인까지 천천히 흐름을 살펴보면서 디버깅을 하기도 하지만, 흐름을 이렇게 가져가면 일반적으로는 정상적인 프름과 데이터를 가정하므로 문제가 발생한 부분이 왜 갑자기 문제를 만났는지 파악하기 어렵다.
따라서 문제가 발생한 라인부터 한 라인씩 거꾸로 올라가면서 문제의 흐름을 파악하는 연습을 하자.
로컬 창으로 변수 보기
pDest[i]와 pSrc[i]에 정확히 어떤 일이 일어났는지 확인해야 한다.
pDest 포인터, pSrc 포인터, i, dwSrcLen을 확인해야한다.
모두 입력 파라미터나 지역변수이므로 로컬 창에서 확인이 가능하다.
pSrc는 "UserCrash"라는 문자열이 들어있는 0x00007ff7`9f40e430 주소 값이 전달된 포인터이며,
pDest는 0x00000000`00000000 라는 주소 값이 전달됐는데 이는 참조할 수 없는 메모리다.
현재 i는 0이므로 문제가 발생한 코드 라인은 다음과 같은 상황이다.
pDest[i] = pSrc[i]; // *(0x00000000`00000000 + 0) = *(0x00007ff7`9f40e430 + 0)
pSrc[0]에 접근할 때는 0x00007ff7`9f40e430 주소의 메모리를 정상적으로 읽었으나, pDest[0]에 쓰려고 할 때 0x00000000`00000000 주소의 메모리에 쓰려고 하니 문제가 발생한 것이다.
하지만 pDest에 0x00000000`00000000 주소 값이 전달된 이유를 알아야 진정한 원인을 찾았다고 할 수 있다.
pDest는 CMyAppDlg::MyStrCpy() 함수의 첫 번째 파라미터로 전달되었다.
해당 함수는 CMyAppDlg::OnButtonUserCrash() 함수에서 호출했으므로 해당 함수를 살펴보자.
색깔 있는 라인은 MyStrCpy() 함수가 리턴되면 실행될 위치를 나타낸다.
여기서 알 수 있는 것은 현재 OnButtonCrash() 함수 내부에서 MyStrCpy() 함수를 호출했고, MyStrCpy()가 정상적으로 리턴한다면 색깔 있는 라인을 실행할 것이라는 것이다.
소스 코드에서 MyStrCpy()를 호출할 때 pBuffer[i]와 "UserCrash"를 전달했음을 알 수 있다.
일반적으로는 소스 코드만으로는 변수의 내용을 알 수 없으므로 문제가 발생한 원인을 정확히 파악하기 어렵다.
이 상태에서 로컬 창을 보면 당시의 변수들이 보인다.
pBuffer의 내용이 궁금하여 pBuffer 앞의 + 버튼을 누르면 pBuffer 배열의 내부 항목들이 보인다.
로컬 창을 보면 i 변수의 값이 1인 것이 보인다.
그렇다면 MyStrCpy로 전달된 값은 pBuffer[1]이라는 것이다.
그리고 pBuffer[i]의 값은 0x00000000`00000000이 들어있는 것을 볼 수 있다.
해당 값이 전달되어 문제가 발생 했지만, 이 역시 원인이 될 수는 없다.
pBuffer[1]에 왜 0x00000000`00000000이 들어갔는지가 진짜 이유다.
OnButtonUserCrash() 소스 코드를 자세히 보면 문제가 발생한 이유와 수정 방향을 찾을 수 있다.
*Buffer[2]는 CMyAppDlg::OnButtonUserCrash() 함수 진입부에서 초기화하는데, 두 번째 값을 NULL로 채우고 있다.
즉, 이 부분이 0x00000000`00000000 이라는 값을 최초로 제공한 부분이며 진짜 원인이다.
이제 진원지를 찾았으니, 어떻게 수정해야할지 생각해봐야한다.
두 번째 값을 NULL로 채우는 것이 의도된 것인지 아닌지 확인해야 한다.
- NULL이 아니라 어떤 메모리 주소가 들어가야 하는 것이었다면 이 자리에 NULL을 넣은 것이 잘못한 것이고, 이 것을 수정하면 문제가 해결될 것이다.
- for문에서는 2번 루프를 도는 것이 의도된 것인지 아닌지 확인한다. 1번만 돌아도 되는 것이면 2를 넣은 것이 잘못한 것이고, 이 것을 1로 수정하면 문제가 해결될 것이다.
- for문이 의도적으로 2번 이상 도는 것이었다면 pBuffer[i]의 유효성 검증이 필요한 것은 아니었는지 확인한다. 그런 경우 pBuffer[i] 값이 NULL인 경우 break; 를 수행하도록 하면 된다.
와치 창으로 메모리 보기
분석 중 전역변수를 봐야하는 경우가 있으면 와치(Watch)창을 활용한다.
예제에는 g_szBuffer 라는 전역변수가 있는데, 연습 삼아 내용을 확인해보자.
g_szBuffer에는 for 루프를 첫 번쨰 돌면서 MyStrCpy()에 의해 문자열이 복사되었으므로 "UserCrash" 문자열이 복사되어있다.
로컬 창에서 봤던 pBuffer[0]의 Value 값이 와치 창에서 보이는 g_szBuffer의 Location 값인 00007ff7`9f517e88 값과 동일함을 주의깊게 봐야한다.
메모리 창으로 메모리 보기
전역변수를 확인할 때 와치 창으로 보면 변수 타입에 대한 자세한 정보와 함께 확인할 수 있지만, 단순하게 확인하려면 메모리 창으로 확인할 수도 있다.
메모리 창을 띄운 후 Virtual 란에 MyApp!g_szBuffer라고 적어본다.
전역변수 g_szBuffer가 00007ff7`9f517e88로 시작하는 것을 알 수 있고, 데이터를 바이트 단위로 나열되어 있는 것을 볼 수 있다. 또한 오른 쪽에는 ASCII 문자로 보여주는 부분이 있어서 문자열일 경우 쉽게 확인할 수 있다.
중간의 Display format이 Byte로 되어 있어서 메모리 내용이 바이트 단위로 나열되어 있는데, 이 것을 LONG, LONG HEX 등으로 변경하면 원하는 형식으로 메모리 내용을 볼 수있다.
이를 잘 활용하도록 하자.
메모리 창에서는 전역변수 타입에 대한 정보는 알 수 없지만 전역변수의 실제 메모리 위치와 내용 앞 뒤에 위치하는 전역변수들의 내용도 확인할 수 있으므로 디버깅할 때 메모리 창을 유용하게 사용할 수 있다.
프로세스와 스레드 보기
프로세스와 스레드(Processes and Threads) 창은 현재 프로세스의 모든 스레드를 보여준다.
보기(View) 메뉴에서 Processes and Threads를 선택하면 아래와 같은 창이 나타난다.
여러 개의 스레드 중에서 두꺼운 글씨체로 표시된 스레드는 현재 디버거가 레지스터 셋과 콜 스택을 보여주고 있는 스레드다.
스레드 목록 중 다른 스레드를 더블클릭하면 WinDbg는 해당 스레드의 레지스터 컨텍스트로 변경되면서 스레드의 콜 스택을 보여준다.
이 기능은 여러 개의 스레드가 동시에 동작하면서 상호 간에 영향을 줘서 문제가 발생했을 때 유용하게 사용할 수 있다.
문제가 발생한 스레드의 확인과 동시에 다른 스레드들은 현재 어떤 일을 수행하고 있었는지를 확인한다면 문제의 원인을 더 쉽게 찾을 수 있지 않을까?