본문 바로가기

도구의발견

Handle leak 찾기

이 글의 대상은 윈도우 프로그래밍에 대해서 어느 정도 이해를 하고 있는 사람들을 대상으로 합니다. 기본적으로 핸들과 커널 오브젝트에 대한 개념에 대해 알고 있어야 하며(모른다면 여기로), 디버깅에 대해서 약간의 기본적 지식을 가지고 계시는 것이 좋습니다.

본 포스트는 디버깅에 관련된 툴들의 사용법에 대해서 다루고 있으며, 자세한 사용법 보다는 기본적인 사용법위주로 문제 해결에 관련된 부분만 다루고 있습니다. 보다 자세한 정보를 원하시면 해당 툴에 관련된 링크를 따라 가시면 많은 도움이 될 것입니다.


프로그래밍을 하다 보면 원하든 원하지 않든(거의 이 경우가 대부분이만) 종종 자원을 흘리고 다니는 경우가 있다. 여기서 말하는 자원이란 것은 파일이든, 메모리든 핸들이든 여러가지가 될 수 있지만 오늘은 특히 '핸들(handle)'의 누수를 어떻게 찾아 낼 수 있는가에 대해서 알아 보도록 하겠다.

핸들의 누수를 찾기 위해서는 아래와 같은 준비물이 필요하다 :
 - 핸들을 흘리고 있을 것이라 짐작되는 실행 프로그램

LeakyApp.cpp
0.00MB

 - 윈도우를 깔면 기본적으로 제공되는 작업관리자(Task mananger)
 - Process Explorer(http://technet.microsoft.com/en-us/sysinternals/bb896653.aspx)
 - Debugging Tools for Windows 패키지에 포함 되어 있는 Windbg
   (http://www.microsoft.com/whdc/DevTools/Debugging/default.mspx)
 ※ 링크가 깨어져 있을 경우는 구글에서 검색하시면 금방 찾을 수 있을 겁니다.

위의 준비물이 다 준비 되었다면 핸들 누수를 찾아 보도록 하자.

1. 이 어플리케이션에서 핸들이 새고 있을까요?

가장 먼저 해야 할 일은 어플리케이션이 핸들을 질질 흘리고 다니는지 아닌지 판단 해야한다. 판단하는 방법으로는 작업관리자를 이용하면 된다. 여기서 잠깐 작업관리자에 대한 사족을 달자면, 이 녀석은 프로세스에 대한 각종 유용한 정보를 알려 주는 녀석으로써, 앞으로도 디버깅 관련 포스팅에 약방에 감초 처럼 등장 할 녀석이다.

맨위의 매뉴에서 '보기>열선택'(영어로는 어떻게 쓰여져 있는지 모르겠다) 순서로 클릭해 들어가면 수많은 체크박스들이 보일 것이다. 그것들 중에 관심있는 박스에 체크를 하고 확인을 누르면 되겠다. 여기서는 핸들이 누수 되고 있는지 아닌지를 찾을 것이기에 '핸들 수'에 체크를 하자. 그럼 이제 부터 작업관리자의 프로세스 탭에서 '핸들'에 관련된 정보를 볼 수 있을 것이다.

핸들이 누수 되고 있을 거라고 생각 되는 프로세스의 핸들 숫자를 살펴 보자. 핸들 카운트가 늘어나고 있는가? 여기서 한가지 유의 할 점은 핸들 카운트가 늘어난다고 해서 무조건 누수라고 봐서는 안된다는 것이다. 단순히 사용하는 핸들이 많아서 핸들 카운트가 늘어 나는 것일 수도 있고, 캐싱이나 등을 위해서 일부러 핸들을 클로즈하지 않고 있는 것일 수 있다. 프로세스가 아무것도 하지 않고 아이들(idle) 상태에 있으면서도 핸들 카운트가 증가하고 있거나 감소하지 않는다면 의심해 볼만 하다.

2. 어떤 핸들이 새고 있을까요?

프로세스에서 핸들이 누수되고 있다고 확신을 가지고 있다면(위에서 어렵게 이야기 했지만, 핸들이 누수되고 있다는 사실을 판단하는 것은 직관적으로도 알 수 있는 쉬운 작업이다) 어떤 타입의 핸들이 누수되고 있는지 파악한다면 잘못을 바로 잡는데 상당히 시간을 절약 할 수 있을 것이다. 이럴 때 사용 할 수 있는 것이 바로

"Process Explorer"

이라는 녀석이다.

이 녀석은 현재 선택된 프로세스에서 만들어 지고 있는 핸들의 타입에 대한 정보들을 일목요연하게 보여준다. 만일 이 녀석으로 프로세스를 감시 하고 있는데 줄어들지 않고 늘어 나기만 하는 핸들이 있다면 바로 그녀석이 누수의 원인. 어떤 타입의 핸들이 새고 있는지 알고, 핸들의 이름도 알 수 있으니 코드를 수정하는 것은 시간 문제다. 아래의 스크린샷은 파일 핸들을 지속적으로 만들어 내고 있는 프로세스를 모니터링 하고 있는 것이다 :

위에 보이는 HeapMemoryLeak라는 프로그램은 필자가 의도적으로 핸들 및 메모리등의 자원을 누수 하도록 만든 프로그램이다. 아래쪽 윈도우를 보면 똑같이 생긴 File 타입의 핸들들이 중복해서 만들어 지고 있는 것을 볼 수 있다.

"Process Explorer"를 사용하는 방법에 대해 좀 더 자세히 알고 싶다면, http://technet.microsoft.com/en-us/sysinternals/bb896653.aspx 를 참고 하자.

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를 활성화 시키는 것이다 :

0:001> !htrace -h
!htrace [handle [max_traces]]
!htrace -enable [max_traces]
!htrace -disable
!htrace -snapshot
!htrace -diff
0:001> !htrace -enable
Handle tracing enabled.
Handle tracing information snapshot successfully taken.

htrace가 활성화 되고나면 윈도우는 모든 핸들의 생성과 소멸에 관련된 호출에 대해서 기록하기 시작한다.
이제는 프로세스가 자원을 누수 할 수 있도록 프로세스를 구동 시키자(지금까지 프로세스는 블록킹 상태였다).

0:001> g

'g'를 누르게 되면 프로세스는 구동상태로 들어가게 되고, 자원을 누수하는데 필요한 자신의 일을 할 것이다. 자원 누수를 경험 할 수 있는 적정 시간이 경과 되고 난후 디버깅을 잠시 멈춰 주자. 맨위의 GUI 메뉴를 살펴 보면 '일시 정지' 버튼을 누르면 된다 :

(1774.254): Break instruction exception - code 80000003 (first chance)
eax=7ffd6000 ebx=00000001 ecx=00000002 edx=00000003 esi=00000004 edi=00000005
eip=7c931230 esp=003affcc ebp=003afff4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
ntdll!DbgBreakPoint:
7c931230 cc              int     3

프로세스가 정상적으로 블록킹 상태로 빠졌다면 이제 !htrace 명령으로 그동안 생성되고 소멸된 핸들을 찾아내면 된다 :

0:001> !htrace
--------------------------------------
Handle = 0x00000690 - OPEN
Thread ID = 0x00000368, Process ID = 0x00001774
0x7c801a4f: kernel32!CreateFileA+0x0000002b
0x102c43f5: MSVCR90D!open+0x000008a5
0x102c3e97: MSVCR90D!open+0x00000347
0x102c4fe0: MSVCR90D!sopen_s+0x00000020
0x102552bd: MSVCR90D!flsbuf+0x00000ddd
0x102565f5: MSVCR90D!fsopen+0x000001d5
0x10256644: MSVCR90D!fopen+0x00000014
0x0041147d: HeapMemoryLeak!main+0x0000003d
0x00411ad8: HeapMemoryLeak!__tmainCRTStartup+0x000001a8
--------------------------------------
...
Parsed 0x4 stack traces.
Dumped 0x4 stack traces.

간단하게 하기 위해 많은 부분을 생략 했다. 위의 로그에 나와 있는 부분들은 너무 직관적이라 따로 설명하지 않도록 하겠다. 다만 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
0x7c801a4f: kernel32!CreateFileA+0x0000002b
0x102c43f5: MSVCR90D!open+0x000008a5
0x102c3e97: MSVCR90D!open+0x00000347
0x102c4fe0: MSVCR90D!sopen_s+0x00000020
0x102552bd: MSVCR90D!flsbuf+0x00000ddd
0x102565f5: MSVCR90D!fsopen+0x000001d5
0x10256644: MSVCR90D!fopen+0x00000014
0x0041147d: HeapMemoryLeak!main+0x0000003d
0x00411ad8: HeapMemoryLeak!__tmainCRTStartup+0x000001a8
--------------------------------------
... 

0x00000690의 값을 가진 핸들이 여전히 close 되지 않고 버티고 있는 것을 볼 수 있다. 이 프로그램은 파일을 열고 닫지를 않으니 모든 핸들이 줄줄줄 새는 것이 당연하다. 그리고 콜 스택을 살펴보면 tmain 함수에서 호출하는 fopen을 따라 핸들을 생성하는 것을 알 수 있다.

4. 관련 문서


이상 핸들 누수를 어떻게 감지하는지 알아 보았습니다. 글을 적고 있는 필자도 그렇게 위의 툴들에 해박한 지식을 가지고 있는 것이 아니라 공부하고 있는 처지이기에 많이 부족한 글이라 생각합니다만 이 정도 만으로도 충분히 어느정도의 핸들 누수는 잡아 내는데 도움을 줄 수 있을 것이라 생각하며 이만 포스트를 접도록 하겠습니다..

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!