이 글의 대상은 윈도우 프로그래밍에 대해서 어느 정도 이해를 하고 있는 사람들을 대상으로 합니다. 기본적으로 핸들과 커널 오브젝트에 대한 개념에 대해 알고 있어야 하며(모른다면 여기로), 디버깅에 대해서 약간의 기본적 지식을 가지고 계시는 것이 좋습니다.
본 포스트는 디버깅에 관련된 툴들의 사용법에 대해서 다루고 있으며, 자세한 사용법 보다는 기본적인 사용법위주로 문제 해결에 관련된 부분만 다루고 있습니다. 보다 자세한 정보를 원하시면 해당 툴에 관련된 링크를 따라 가시면 많은 도움이 될 것입니다.
프로그래밍을 하다 보면 원하든 원하지 않든(거의 이 경우가 대부분이만) 종종 자원을 흘리고 다니는 경우가 있다. 여기서 말하는 자원이란 것은 파일이든, 메모리든 핸들이든 여러가지가 될 수 있지만 오늘은 특히 '핸들(handle)'의 누수를 어떻게 찾아 낼 수 있는가에 대해서 알아 보도록 하겠다.
핸들의 누수를 찾기 위해서는 아래와 같은 준비물이 필요하다 :
- 핸들을 흘리고 있을 것이라 짐작되는 실행 프로그램
1. 이 어플리케이션에서 핸들이 새고 있을까요? 가장 먼저 해야 할 일은 어플리케이션이 핸들을 질질 흘리고 다니는지 아닌지 판단 해야한다. 판단하는 방법으로는 작업관리자를 이용하면 된다. 여기서 잠깐 작업관리자에 대한 사족을 달자면, 이 녀석은 프로세스에 대한 각종 유용한 정보를 알려 주는 녀석으로써, 앞으로도 디버깅 관련 포스팅에 약방에 감초 처럼 등장 할 녀석이다.
맨위의 매뉴에서 '보기>열선택'(영어로는 어떻게 쓰여져 있는지 모르겠다) 순서로 클릭해 들어가면 수많은 체크박스들이 보일 것이다. 그것들 중에 관심있는 박스에 체크를 하고 확인을 누르면 되겠다. 여기서는 핸들이 누수 되고 있는지 아닌지를 찾을 것이기에 '핸들 수'에 체크를 하자. 그럼 이제 부터 작업관리자의 프로세스 탭에서 '핸들'에 관련된 정보를 볼 수 있을 것이다.
핸들이 누수 되고 있을 거라고 생각 되는 프로세스의 핸들 숫자를 살펴 보자. 핸들 카운트가 늘어나고 있는가? 여기서 한가지 유의 할 점은 핸들 카운트가 늘어난다고 해서 무조건 누수라고 봐서는 안된다는 것이다. 단순히 사용하는 핸들이 많아서 핸들 카운트가 늘어 나는 것일 수도 있고, 캐싱이나 등을 위해서 일부러 핸들을 클로즈하지 않고 있는 것일 수 있다. 프로세스가 아무것도 하지 않고 아이들(idle) 상태에 있으면서도 핸들 카운트가 증가하고 있거나 감소하지 않는다면 의심해 볼만 하다.
2. 어떤 핸들이 새고 있을까요?
프로세스에서 핸들이 누수되고 있다고 확신을 가지고 있다면(위에서 어렵게 이야기 했지만, 핸들이 누수되고 있다는 사실을 판단하는 것은 직관적으로도 알 수 있는 쉬운 작업이다) 어떤 타입의 핸들이 누수되고 있는지 파악한다면 잘못을 바로 잡는데 상당히 시간을 절약 할 수 있을 것이다. 이럴 때 사용 할 수 있는 것이 바로 "Process Explorer"이라는 녀석이다.
이 녀석은 현재 선택된 프로세스에서 만들어 지고 있는 핸들의 타입에 대한 정보들을 일목요연하게 보여준다. 만일 이 녀석으로 프로세스를 감시 하고 있는데 줄어들지 않고 늘어 나기만 하는 핸들이 있다면 바로 그녀석이 누수의 원인. 어떤 타입의 핸들이 새고 있는지 알고, 핸들의 이름도 알 수 있으니 코드를 수정하는 것은 시간 문제다. 아래의 스크린샷은 파일 핸들을 지속적으로 만들어 내고 있는 프로세스를 모니터링 하고 있는 것이다 :
위에 보이는 HeapMemoryLeak라는 프로그램은 필자가 의도적으로 핸들 및 메모리등의 자원을 누수 하도록 만든 프로그램이다. 아래쪽 윈도우를 보면 똑같이 생긴 File 타입의 핸들들이 중복해서 만들어 지고 있는 것을 볼 수 있다.
3. Windbg 사용하기(Make use of Resource leak detection tools) 지금까지 1번과 2번의 과정에서 프로세스의 핸들 누수 여부를 결정하는 일, 어떤 타입의 핸들이 누수 되는지 구분하는 방법에 대해 알아 보았다. 하지만 핸들 누수라는 것이 언제나 정기적으로 발생하는 것도 아니고, 복잡한 환경에서 구동되는 프로그램이라면 동일한 파라메터에 동일한 시간이 지났음에도 불구하고 누수되는 양이나 누수되는 곳이 다를 수도 있다. 이럴 때 유용하게 사용 할 수 있는 것이 Windbg의 !htrace다.
htrace는 Windbg의 확장 기능으로 핸들의 OPEN과 CLOSE에 대해서 함수 호출 스택, 핸들을 열거나 닫은 프로세스 아이디, 쓰레드 아이디등을 추적 할 수 있으며, 결론적으로는 열리기는 했으되 닫히지 않은 모든 핸들들만을 선별하여 위에서 언급한 정보들을 제공 할 수 있다. 열리기만 하고 닫히지 않은 핸들은 누수되고 있을 확률이 높고, 어디서 만들었는지에 대한 정보도 제공해 주니 이 어찌 좋지 아니한가.
htrace에 대해서 시작하기 전에 windbg의 사용법에 대해서 기본적인 것을 알아 보자 :
1. Windbg.exe를 실행 한다.
커맨드로 실행 해도 좋고, 시작 메뉴에서 부터 실행 해도 좋다. 일단 실행 하자.
2. 디버깅할 프로그램에 attach 하거나 Windbg에서 executable 파일을 로드한다.
디버깅하기 위해서는 이미지를 로드 해야 한다. Windbg가 로드하는 방법으로는 두 가지가 있는데 구동중인 프로세스에 Windbg를 붙이는 방법과 Windbg에서 디버깅할 프로세스를 로드 하는 방법이 있다.
구동 중인 프로세스에 붙이기(Attach to a running process)
프로세스가 실행 중이어야 한다.
프로세스 아이디를 이용하여 붙일 수 있다.
windbg.exe -p PID
혹은 프로세스 이름을 이용하여 붙일 수도 있다. 하지만 동일한 이름의 프로세스가 2개 이상이면 안 된다.
windbg.exe -pn ProcessName
혹은 GUI 모드에서 File>Attatch 를 하면 된다.
Windbg에서 executable파일 로드하기(Spawning new process)
Windbg.exe [-o] ProgramName
혹은 GUI 모드에서 File>Open Executable 을 하면 된다.
간단하게 나마 Windbg의 실행 방법에 대해서 알아 보았다. 이제 windbg에서 핸들이 누수되고 있을 것이라고 의심되는 프로세스를 로드하는 것은 끝났다. 이제 어떻게 htrace를 이용 할 것인가?
CommandLine: E:\Project\HeapMemoryLeak\Debug\HeapMemoryLeak.exe
Symbol search path is: *** Invalid ***
****************************************************************************
* Symbol loading may be unreliable without a symbol search path. *
* Use .symfix to have the debugger choose a symbol path. *
* After setting your symbol path, use .reload to refresh symbol locations. *
****************************************************************************
Executable search path is:
ModLoad: 00400000 0041b000 HeapMemoryLeak.exe
...
(1774.368): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffd6000 ecx=00000003 edx=00000008 esi=00251f48 edi=00251eb4
eip=7c931230 esp=0012fb20 ebp=0012fc94 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
...
7c931230 cc int 3
프로세스를 로드하면 제일 먼저 나오는 화면이다. 심볼 패스가 제대로 로드 안되었다는 에러메시지가 눈에 거슬리긴 하지만 무시하도록 하자. 가장 먼저 해야 할 일은 htrace를 활성화 시키는 것이다 :
간단하게 하기 위해 많은 부분을 생략 했다. 위의 로그에 나와 있는 부분들은 너무 직관적이라 따로 설명하지 않도록 하겠다. 다만 Handle = <value> - OPEN 의 경우 핸들 생성 콜을 나타내고 반대로 CLOSE라는 것이 있다는 것을 알아 두자. 동일한 핸들 값에 OPEN만 있고 CLOSE가 없는 녀석들이 바로 우리가 찾는 녀석들이다. 여기서 분명히 불평하시는 분 있을 것이다. 저 많은 로그들 사이에서 어떻게 특정녀석들을 찾냐고... 그래서 htrace는 획기적은 툴을 하나 더 제공한다. 바로 -diff 옵션이다.
바로 이 -diff를 사용하면 OPEN만 있고 CLOSE가 없는 모든 녀석들을 찾아 낼 수가 있다 :
0:001> !htrace -diff
Handle tracing information snapshot successfully taken.
0x32 new stack traces since the previous snapshot.
Ignoring handles that were already closed...
Outstanding handles opened since the previous snapshot:
--------------------------------------
0:001> !htrace -diff
Handle tracing information snapshot successfully taken.
0x32 new stack traces since the previous snapshot.
Ignoring handles that were already closed...
Outstanding handles opened since the previous snapshot:
-------------------------------------- Handle = 0x00000690 - OPEN
Thread ID = 0x00000368, Process ID = 0x00001774
0x00000690의 값을 가진 핸들이 여전히 close 되지 않고 버티고 있는 것을 볼 수 있다. 이 프로그램은 파일을 열고 닫지를 않으니 모든 핸들이 줄줄줄 새는 것이 당연하다. 그리고 콜 스택을 살펴보면 tmain 함수에서 호출하는 fopen을 따라 핸들을 생성하는 것을 알 수 있다.
이상 핸들 누수를 어떻게 감지하는지 알아 보았습니다. 글을 적고 있는 필자도 그렇게 위의 툴들에 해박한 지식을 가지고 있는 것이 아니라 공부하고 있는 처지이기에 많이 부족한 글이라 생각합니다만 이 정도 만으로도 충분히 어느정도의 핸들 누수는 잡아 내는데 도움을 줄 수 있을 것이라 생각하며 이만 포스트를 접도록 하겠습니다..
메모리 누수되는 부분이 있는지만 확인하려다가 그냥 핸들수도 체크항목에 넣어본건데 당황스러운 결과가 나왔다. -_-; 다행히 메모리 누수는 없는 것으로 판단이 됐지만 핸들 누수는 조금 의외였다. 혹시 방법이 있을까 싶어서 이리저리 검색해보다가 WinDbg로 확인하는 방법을 알아냈다. 이거 아니었으면 큰일 날 뻔했다. 그것도 연말에 말이쥥~ ^^;
아래와 같은 순서로 진행했다.
1. WinDbg를 실행한다.
: 아래와 같이 실행할 파일을 선택한다.
2. 아래와 같이 선택한 프로그램이 실행되며 pause 상태로 초기화된다.
: !htrace -enable 명령을 입력하여 htrace를 활성화한다.
: 이 명령은 이후 핸들의 open/close 상태를 기록하도록 한다.
: 명령이 성공하면 아래와 같이 성공 메시지가 출력된다.
3. 현재 pause 상태이므로
g 명령을 입력하거나 메뉴에서 Degug > Go (F5)를 선택하여 프로그램을 진행시킨다.
4. 일정 시간동안 프로그램을 수행한 후
메뉴의 Debug > Break 를 선택하여 pause 상태로 만든다.
그리고나서 명령창에 !htrace 를 입력한다.
5. !htrace 를 입력하면 아래와 같이 핸들값들에 대한 open/close 로그가 출력된다
: Open/close 한 모듈과 함수명도 출력이 된다.
: 프로그램에 따라 상당히 많은 양이 출력된다.
6. !htrace 명령을 수행하면 로그가 나오긴 하는데 상당히 많은 로그가 출력된다.
글쎄 이걸 일일이 찾기는 좀 힘들어 보인다. 이 때는 !htrace -diff 명령을 수행한다.
: 이 명령은 open 후 close 되지 않은 핸들을 출력한다.
7. !htrace -diff 명령의 결과 아래와 같이 출력된다.
: CProcess::KillProcess 함수에서 open한 핸들이 close되지 않았음을 알 수 있다.
: Open 되었으나 close 되지 않은 핸들과 어느 모듈에서 호출했는지까지 보여준다.
주의할 점은 !htrace -enable 명령을 내린 시점부터 현시점까지
close되지 않았기 때문에 누수일 확률이 높다는 것이지 무조건 누수는 아니라는 것이다.
(당연한 얘기지만 ...^^;)
8. 진행조건
: WinDbg의 기본 환경은 미리 세팅한 상태이다. (File > Symbol File Path 항목)
: 테스트를 진행한 프로그램의 PDB 파일은 실행 파일과 같은 폴더에 위치해 있다.
: 만일 !htrace -diff 명령을 내려도 로그가 너무 많다면 일단 3번 항목을 수행하여
프로그램을 진행시킨다. 그 후 적절한 시점 (프로그램에 따라 개발자가 판단해야함)에
4번 항목을 수행하여 프로그램을 pause 시키고 2번 항목 (!htrace -enable)부터 수행한다.
WinDbg는 프로그램 비정상 종료시 Dr.Watson 로그를 분석할 때만 이용했는데 이런 기능도 있는지 미쳐몰랐다. 아무튼 이 놈때문에 쉽게 잡을 수 있어서 다행이다. 이젠 놀아야쥐 ~ ^^;