안녕하세요. 정말 오래간만에 포스팅을 합니다. 오늘 Yes24를 둘러보다가 Builder와 windbg에 관한 서평을 보고 생각난게 있어 이 글을 적게 되었습니다. ( http://blog.yes24.com/document/2388861 )
예전 회사에서는 많은 개발툴을 사용했었습니다. 기존에 개발된 모듈을 유지 보수 하기 위해서 C++ Builder, Delphi를, 새로 작성된 모듈은 VC++을 사용했습니다. C++Builder나 Delphi의 경우 굉장이 좋은 RAD 툴이지만 포스트모텀 디버깅이 힘들다는 단점을 가지고 있습니다. 즉 dump file을 통해서 디버깅을 하는 것이 쉽지 않지요. 하지만 방법이 없는건 아닙니다. 약간의 절차를 걸쳐야한다는 단점이 있을 뿐입니다.
일단 Build 옵션을 Release 모드 기준으로 설정하도록 하겠습니다. ( C++ Builder XE )
Map File Type 기본 옵션을 Map file with segments ==> Detailed segment map 으로 변경합니다. 옵션을 변경하면 각 함수에 대한 Address를 Map File 을 통해서 모두 확인 가능합니다.
좀 더 실전적인 확경을 위해서 실행 파일을 독립 실행 파일로 만든 후 Debugging 을 위한 작업을 진행하도록 하죠
이렇게 설정을 맞친 후 빌드를 하면 Release 폴더에 map File이 생성 되어 있을 겁니다. 저 같은 경우는 dumpdbg.map 파일이 생성되었습니다.
이제 이 생성된 Map File을 Windbg에서 인식 할 수 있는 DBG Format으로 변경해야합니다. 그렇게 하기 위해서는 maptodbg 라는 툴을 다운 받아 변환하시면 됩니다.
maptodbg.exe를 통해서 .dbg file을 생성하면 디버깅을 시작할 수 있습니다. 사용법은 굉장히 간단합니다.
사용 예 : map2dbg.exe dumpdbg.exe
생성된 dbg 파일을 통해서 PostMortem이 가능합니다. 테스트를 위해 제작한 dumpdbg.exe를 실행해 보도록 하겠습니다.
위에 있는 샘플은 WinDbg로 쉽게 배우는 Windows Debugging 윈도우 디버깅 에 나오는 샘플을 약간 수정하여 만든 녀석입니다. 디버깅을 시작 해보도록 하죠.
1. Null Pointer Exception
가장 위 버튼을 클릭하면 C++Builder와 델파이에서 가장 많이 볼 수 있는 아래와 같은 모습을 볼 수 있습니다.
VCL의 굉장히 좋은 예외 처리 때문에 위와 같은 오류 메시지를 볼 수 있죠 하지만 무엇 때문에 프로그램이 오류가 발생했고 어느 Address 인지도 알려 줍니다. 하지만 Address 만으로 Code와 매치하기는 굉장히 힘듭니다.
이제 Windbg를 사용해서 어디서 죽었는지 알아보면 됩니다.
0:001> ~*kvn
0 Id: 17d4.62c Suspend: 1 Teb: 7ffdd000 Unfrozen
# ChildEBP RetAddr Args to Child
00 0012eb50 77cf9418 77d0770a 000e0e28 00000001 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 0012eb88 77d049c4 00080f12 000e0e28 00000001 USER32!NtUserWaitMessage+0xc
02 0012ebb0 77d1a956 77cf0000 00168328 000e0e28 USER32!InternalDialogBox+0xd0 (FPO: [Non-Fpo])
03 0012ee70 77d1a2bc 0012efcc 00a9afac 00000010 USER32!SoftModalMessageBox+0x938 (FPO: [Non-Fpo])
04 0012efc0 77d463fd 0012efcc 00000028 000e0e28 USER32!MessageBoxWorker+0x2ba (FPO: [Non-Fpo])
05 0012f018 77d30853 000e0e28 00a9afac 00b3491c USER32!MessageBoxTimeoutW+0x7a (FPO: [Non-Fpo])
06 0012f038 77d46579 000e0e28 00a9afac 00b3491c USER32!MessageBoxExW+0x1b (FPO: [Non-Fpo])
*** WARNING: Unable to verify checksum for D:\TestCB\dumpDebugging\Release\Win32\dumpdbg.exe
07 0012f054 004a9e2e 000e0e28 00a9afac 00b3491c USER32!MessageBoxW+0x45 (FPO: [Non-Fpo])
08 0012f0dc 004a9f4a 00000010 0012f8d4 004a9f65 dumpdbg!_fastcall Forms::TApplication::MessageBox(const wchar_t *, const wchar_t *, int)+0xfe
09 0012f108 004a9d1b 0012f8d4 00000000 004c64d5 dumpdbg!_fastcall Forms::TApplication::ShowException(Sysutils::Exception *)+0x9a
... 생략
볼랜드 툴의 특성상 Stack 상에서 정확한 위치를 파악할 수 없습니다. 왜냐하면 Exception이 발생하고 Exception 처리를 통해서 Message Box가 출력된 상황이다 보니 Stack으로는 파악이 힘듭니다. 하지만 오류 메시지를 통해서 파악이 가능하죠. 오류 메시지에는 고맙게도 00401D26 Address에서 문제가 있다는 것을 알려줍니다. Last Stack이라는 최소의 정보이지만 이것만으로 굉장히 도움이 되는 경우가 많죠.
0:001> ln 00401d26
(00401cf4) dumpdbg!_fastcall TForm1::NullPointException(char *)+0x32 | (00401d48) dumpdbg!_fastcall TForm1::CreateDeadLockThread()
아까 생성해 둔 dbg 심볼 덕분에 어떤 함수에서 Crash가 발생한지 찾을 수 있습니다. 추가로 리버싱 코드를 보면
0:001> u 00401D26
dumpdbg!_fastcall TForm1::NullPointException(char *)+0x32:
00401d26 8819 mov byte ptr [ecx],bl
00401d28 40 inc eax
00401d29 41 inc ecx
00401d2a 42 inc edx
00401d2b 83f815 cmp eax,15h
00401d2e 72f4 jb dumpdbg!_fastcall TForm1::NullPointException(char *)+0x30 (00401d24)
00401d30 8b55d8 mov edx,dword ptr [ebp-28h]
00401d33 c6040200 mov byte ptr [edx+eax],0
ecx 즉 첫번째 파라미터 ( fastcall 이므로 )가 NULL 이기 때문에 발생한 것이라는 것을 확인 할 수 있습니다.
2. Spaking Thread
spkaing thread 는 CPU를 지속적으로 선점하는 Thread를 의미 합니다. Kernel Dump라면 user time/kernel time 을 통해서 확인 이 가능하시면 user mode dump에서는 stack을 통해서 확인을 합니다. 2 번째 버튼을 누르면 CPU가 비정상적으로 올라가는 것을 확인 할 수 있습니다. Stack을 보도록 하죠
0 Id: 6a0.109c Suspend: 1 Teb: 7ffdf000 Unfrozen
# ChildEBP RetAddr Args to Child
00 0012febc 77cf9418 004aa676 0012ff1c 004aa691 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
*** WARNING: Unable to verify checksum for D:\TestCB\dumpDebugging\Release\Win32\dumpdbg.exe
01 0012fef0 004a98a4 000d0e7c 0000000f 00000000 USER32!NtUserWaitMessage+0xc
02 0012ff44 0040184e 00000000 004f7048 000208b4 dumpdbg!_fastcall Forms::TApplication::HandleMessage()+0x1c
03 0012ff88 004f262b 00400000 00000000 000208b4 dumpdbg!wWinMain+0x5a
04 0012ffc0 7c7e7077 00330038 00300039 7ffdb000 dumpdbg!C2500_0+0x173
05 0012fff0 00000000 0040159c 00000000 78746341 kernel32!BaseProcessStart+0x23 (FPO: [Non-Fpo])
1 Id: 6a0.3f0 Suspend: 1 Teb: 7ffde000 Unfrozen
# ChildEBP RetAddr Args to Child
00 011dffb4 7c7db729 00000000 00000000 73fd045c dumpdbg!_stdcall ThreadFunc2(void *)+0x3
01 011dffec 00000000 00401a70 00000000 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
2 Id: 6a0.d88 Suspend: 1 Teb: 7ffdd000 Unfrozen
# ChildEBP RetAddr Args to Child
00 012dff3c 7c93d21a 7c7d23f1 00000000 012dff70 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 012dff40 7c7d23f1 00000000 012dff70 00000000 ntdll!NtDelayExecution+0xc (FPO: [2,0,0])
02 012dff98 7c7d2455 000186a0 00000000 012dffb4 kernel32!SleepEx+0x61 (FPO: [Non-Fpo])
03 012dffa8 00401a65 000186a0 012dffec 7c7db729 kernel32!Sleep+0xf (FPO: [Non-Fpo])
04 012dffb4 7c7db729 00000000 00000000 73fd045c dumpdbg!_stdcall ThreadFunc(void *)+0xd
05 012dffec 00000000 00401a58 00000000 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
3 Id: 6a0.1618 Suspend: 1 Teb: 7ffdc000 Unfrozen
# ChildEBP RetAddr Args to Child
00 013dff3c 7c93d21a 7c7d23f1 00000000 013dff70 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 013dff40 7c7d23f1 00000000 013dff70 00000000 ntdll!NtDelayExecution+0xc (FPO: [2,0,0])
02 013dff98 7c7d2455 000186a0 00000000 013dffb4 kernel32!SleepEx+0x61 (FPO: [Non-Fpo])
03 013dffa8 00401a65 000186a0 013dffec 7c7db729 kernel32!Sleep+0xf (FPO: [Non-Fpo])
04 013dffb4 7c7db729 00000000 00000000 73fd045c dumpdbg!_stdcall ThreadFunc(void *)+0xd
05 013dffec 00000000 00401a58 00000000 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
4 Id: 6a0.a20 Suspend: 1 Teb: 7ffda000 Unfrozen
# ChildEBP RetAddr Args to Child
00 0152ff3c 7c93d21a 7c7d23f1 00000000 0152ff70 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 0152ff40 7c7d23f1 00000000 0152ff70 00000000 ntdll!NtDelayExecution+0xc (FPO: [2,0,0])
02 0152ff98 7c7d2455 000186a0 00000000 0152ffb4 kernel32!SleepEx+0x61 (FPO: [Non-Fpo])
03 0152ffa8 00401a65 000186a0 0152ffec 7c7db729 kernel32!Sleep+0xf (FPO: [Non-Fpo])
04 0152ffb4 7c7db729 00000000 00000000 73fd045c dumpdbg!_stdcall ThreadFunc(void *)+0xd
05 0152ffec 00000000 00401a58 00000000 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
5 Id: 6a0.1198 Suspend: 1 Teb: 7ffd9000 Unfrozen
# ChildEBP RetAddr Args to Child
00 0162ff3c 7c93d21a 7c7d23f1 00000000 0162ff70 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 0162ff40 7c7d23f1 00000000 0162ff70 00000000 ntdll!NtDelayExecution+0xc (FPO: [2,0,0])
02 0162ff98 7c7d2455 000186a0 00000000 0162ffb4 kernel32!SleepEx+0x61 (FPO: [Non-Fpo])
03 0162ffa8 00401a65 000186a0 0162ffec 7c7db729 kernel32!Sleep+0xf (FPO: [Non-Fpo])
04 0162ffb4 7c7db729 00000000 00000000 73fd045c dumpdbg!_stdcall ThreadFunc(void *)+0xd
05 0162ffec 00000000 00401a58 00000000 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
# 6 Id: 6a0.147c Suspend: 1 Teb: 7ffd8000 Unfrozen
# ChildEBP RetAddr Args to Child
00 00fdffc8 7c981e40 00000005 00000004 00000001 ntdll!DbgBreakPoint (FPO: [0,0,0])
01 00fdfff4 00000000 00000000 00000008 000060c0 ntdll!DbgUiRemoteBreakin+0x2d (FPO: [Non-Fpo])
덤프 상에서 면 NtUserWaitMessage 를 호출 하는 것으로 보아 Main Thread임을 알 수 있습니다. 그리고DbgBreadPoint 가 호출 된것을 보면 windbg 에 의해서 만들어진 Thread 임을 확인 할 수 있습니다. 그리고 다른 Thread 모두 공통적으로 Sleep 함수를 통해서 제어 되고 있음을 알 수 있습니다. 하지만 유독 ThreadFunc2 만이 그렇지 않죠. 즉 CPU를 ThreadProc2라는 녀석이 선점하고 있음을 추정할 수 있습니다.
간단히 리버싱 코드를 보면
0:006> X dumpdbg!_stdcall ThreadFunc2*
00401a70 dumpdbg!_stdcall ThreadFunc2(void *) =
0:006> u 00401a70
dumpdbg!_stdcall ThreadFunc2(void *):
00401a70 55 push ebp
00401a71 8bec mov ebp,esp
00401a73 ebfe jmp dumpdbg!_stdcall ThreadFunc2(void *)+0x3 (00401a73)
00401a75 33c0 xor eax,eax
00401a77 5d pop ebp
00401a78 c20400 ret 4
무한 Loop를 돌고 있다는 것을 볼 수 있습니다.
3. Hang
Hang은 Application이 응답 없음 상태가 됨을 보여줍니다. 즉 Message Loop이 처리 되지 않고 있다는 것을 의미 합니다. Stack이 어떻게 보이는지 확인 해보도록 하겠습니다.
0 Id: ea8.eb4 Suspend: 1 Teb: 7ffdf000 Unfrozen
*** WARNING: Unable to verify checksum for D:\TestCB\dumpDebugging\Release\Win32\dumpdbg.exe
# ChildEBP RetAddr Args to Child
00 0012f6ac 004c6e09 0012f8f8 00aa22d0 00000000 dumpdbg!_fastcall TForm1::Button3Click(System::TObject *)
01 0012f6f8 00476d79 00aa22d0 0012f8f8 00aa22d0 dumpdbg!_fastcall Controls::TWinControl::WndProc(Messages::TMessage&)+0x56d
02 0012f724 004c6f5c 00040276 0012f970 00acf090 dumpdbg!_fastcall Stdctrls::TButtonControl::WndProc(Messages::TMessage&)+0x71
03 0012f874 004c6e09 00c30fbb 00acf090 0045dcdc dumpdbg!Controls::_17082+0x28
04 0012f8c0 004c64a7 0012f8d4 004c64bf 0012f8f0 dumpdbg!_fastcall Controls::TWinControl::WndProc(Messages::TMessage&)+0x56d
05 0012f8f0 00422466 00000111 00000276 00040276 dumpdbg!_fastcall Controls::TWinControl::MainWndProc(Messages::TMessage&)+0x2f
06 0012f908 77cf8734 00040286 00000111 00000276 dumpdbg!Classes::_17544+0x16
07 0012f934 77cf8816 00c30fbb 00040286 00000111 USER32!InternalCallWinProc+0x28
08 0012f99c 77d0927b 00000000 00c30fbb 00040286 USER32!UserCallWinProcCheckWow+0x150 (FPO: [Non-Fpo])
09 0012f9d8 77d092e3 00776860 007767f8 00000276 USER32!SendMessageWorker+0x4a5 (FPO: [Non-Fpo])
0a 0012f9f8 77187354 00040286 00000111 00000276 USER32!SendMessageW+0x7f (FPO: [Non-Fpo])
0b 0012fa18 77187436 00157ac8 00000000 0009009d COMCTL32!Button_NotifyParent+0x3d (FPO: [Non-Fpo])
0c 0012fa34 7718973b 00157ac8 00000001 0012fb2c COMCTL32!Button_ReleaseCapture+0xd7 (FPO: [Non-Fpo])
0d 0012fac4 77cf8734 00040276 00000202 00000000 COMCTL32!Button_WndProc+0x887 (FPO: [Non-Fpo])
0e 0012faf0 77cf8816 77188eb4 00040276 00000202 USER32!InternalCallWinProc+0x28
... 생략
MessageLoop 이 존재해야하는 Stack이 Button3Click 를 호출 후 멈춰 있음을 확인 할 수 있습니다. 이러한 이유에서 Application은 응답 없음 상태가 되었을 것입니다.
4. DeadLock
아래쪽 Group의 Create Thread를 실행하면 I'm alive 라는 글자가 Memo에 출력되는 것을 확인 할 수 있습니다. 이러한 상태에서 Enter Button을 누르게 되면 문자 출력이 멈추게 됩니다. 이유는 Critical Section을 Main Thread에서 선점하게 되기 때문에 발생하는 Dead Lock 때문입니다.
0 Id: a24.a28 Suspend: 1 Teb: 7ffdf000 Unfrozen
# ChildEBP RetAddr Args to Child
00 0012febc 77cf9418 004aa676 0012ff1c 004aa691 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
*** WARNING: Unable to verify checksum for D:\TestCB\dumpDebugging\Release\Win32\dumpdbg.exe
01 0012fef0 004a98a4 00000000 0000c0a6 00000001 USER32!NtUserWaitMessage+0xc
02 0012ff44 0040184e 00000000 004f7048 000208b4 dumpdbg!_fastcall Forms::TApplication::HandleMessage()+0x1c
03 0012ff88 004f262b 00400000 00000000 000208b4 dumpdbg!wWinMain+0x5a
04 0012ffc0 7c7e7077 00330038 00300039 7ffd5000 dumpdbg!C2500_0+0x173
05 0012fff0 00000000 0040159c 00000000 78746341 kernel32!BaseProcessStart+0x23 (FPO: [Non-Fpo])
1 Id: a24.bf8 Suspend: 1 Teb: 7ffde000 Unfrozen
# ChildEBP RetAddr Args to Child
00 011dff08 7c93df5a 7c94b24b 00000100 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 011dff0c 7c94b24b 00000100 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc (FPO: [3,0,0])
02 011dff94 7c931046 00ab3464 00401ad2 00ab3464 ntdll!RtlpWaitForCriticalSection+0x132 (FPO: [Non-Fpo])
03 011dff9c 00401ad2 00ab3464 00000000 0012f414 ntdll!RtlEnterCriticalSection+0x46 (FPO: [1,0,0])
04 011dffb4 7c7db729 00ab3060 00000000 0012f414 dumpdbg!_stdcall DeadLockThread(void *)+0x56
05 011dffec 00000000 00401a7c 00ab3060 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])
# 2 Id: a24.c8c Suspend: 1 Teb: 7ffdd000 Unfrozen
# ChildEBP RetAddr Args to Child
00 00fdffc8 7c981e40 00000005 00000004 00000001 ntdll!DbgBreakPoint (FPO: [0,0,0])
01 00fdfff4 00000000 00000000 00000008 000060c0 ntdll!DbgUiRemoteBreakin+0x2d (FPO: [Non-Fpo])
DeadLockThread 가 Critical Section를 선점하지 못하고 멈춰 있는 것을 확인 할 수 있습니다. 이러한 경우 Critical Section의 Owner를 찾아 그 원인을 파악하면 문제가 해결 됩니다.
0:002> !cs 00ab3464
-----------------------------------------
Critical section = 0x00ab3464 (+0xAB3464)
DebugInfo = 0x0015e508
LOCKED
LockCount = 0x1
OwningThread = 0x00000a28
RecursionCount = 0x1
LockSemaphore = 0x100
SpinCount = 0x00000000
Thread ID가 0xa28 인것을 확인 할 수 있습니다. 즉 main Message Loop Thread 인 것이 확인 가능합니다. Windbg에는 !cs Commnd 말고도 lock Object를 확인 해주는 !locks 라는 유용한 Command도 있습니다.
0:002> !locks
CritSec +ab3464 at 00ab3464
LockCount 1
RecursionCount 1
OwningThread a28
EntryCount 1
ContentionCount 1
*** Locked
Scanned 219 critical sections
!locks Command를 통해서도 역시 같은 결과가 얻어 지는 것을 확인 할 수 있습니다.
Windbg를 사용하고자 하시는 C++ Builder/Delphi User 에게 조금이나마 도움이 되었으면 합니다.
Enjoy Debugging