"이 문서는 http://blogs.msdn.com/ntdebugging blog 의 번역이며 원래의 자료가 통보 없이 변경될 수 있습니다. 이 자료는 법률적 보증이 없으며 의견을 주시기 위해 원래의 blog 를 방문하실 수 있습니다. (http://blogs.msdn.com/ntdebugging/archive/2007/05/29/detecting-and-automatically-dumping-hung-gui-based-windows-applications.aspx)"

GUI hang 이 발생한 Windows application 찾고 dump 하기

Jeff Dailey 작성

제 이름은 Jeff 이고 CPR Platform 팀의 Escalation Engineer 입니다. Tate 의 Hang 에 대한 블로그에 이어 hang 을 분석하는 방법에 대해서 이야기 하고자 합니다. 여러분의 machine 에서 debug 할 수 있게 몇 가지 lab 을 제공할 것 이고 어떻게 hang 을 찾고 dump 를 생성할 수 있는지 확인할 수 있을 것 입니다. 추가적으로 몇 hang 이 발생하는 경우에 대해서 badwindows.zpi 이라는 파일에 첨부해 넣고 몇 가지 내용을 더 알려드릴 것 입니다.

GUI Hangs

가끔 windows, button, scroll bar 등과 같은 GUI 로 만들어진 Windows application 이 멈추는 경우(작업관리자에 응답 없음) 가 됩니다. Application이 응답 없음이 되어도 운영체제는 정상적으로 동작합니다. 그러나 Application의 Window 는 다시 그려지지도 않고 마우스나 키보드 입력에도 응답하지 않습니다. 그리고 계속되지 않고 일시적으로 발생하기도 합니다. 여러분의 Application은 하루에 10 – 30초 동안 한 번 또는 두 번 정도 멈출 수 있습니다. 그리고 아주 긴 시간 동안 응답이 없거나 영원히 멈출 수도 있습니다.

이 문제를 더 잘 이해하기 위해 모든 GUI 로 만들어진 Windows Application은 다른 프로그램의 Message queue 에 메시지를 전달하는 방식으로 동작하는 것을 알아야 합니다. Windows Application은 보통 하나의 main thread 에서 message 처리를 담당 합니다. 그리고 보통 WinMain 에 이 기능이 구현되어 있습니다. 이 Thread 는 전달받은 Message 에 따라 diaglog 를 열거나 다른 thread 를 만들거나 mouse click 동작에 따라 다른 Window 로 메시지를 전달하는 등 다른 동작을 수행하게 됩니다.

Application 이 응답 없음 상태로 가는 일반적인 이유는 실행이 오래 걸리는 동적인 call 을 호출하는 경우 입니다. Thread 가 전달받은 message 를 처리하지 못한다면 hang 으로 표시될 것 입니다. Process 의 dump 를 가지고 있다면 Cdb 나 Windbg 의 ~0s 명령을 사용하한 후 KB 명령을 사용하면 message 처리를 위해 block 되어 있거나 실행중인 정보를 확인할 수 있을 것입니다. 만약 Thread 0 번이 message 를 처리하는 thread 가 아니라면 ~*kb 명령을 사용하여 모든 Thread 의 stack 정보를 확인하여 찾을 수 있습니다.

Cdb나 windbg 를 사용하여 적절한 시간에 dump 를 만들 수 없거나 debugger를 사용하여 dump 를 만들 기술적인 이해가 없을 경우 아래 tool 을 사용할 수 있습니다.

여러분의 tool 만들기

앞으로 hang 시나리오와 간단한 해결방법을 보도록 하겠습니다.

Visual Studio (The Express edition 은 무료), Windows SDK (무료), debugger SDK (Debugging tools 를 설치), 그리고 Windows 동작에 대한 약간의 이해가 필요 합니다.

우리의 debug application 이 하는 것과 하지 않을 것에 대해 생각해 봅시다.

1. 사용과 설정하기가 쉽습니다.

2. 운영체제를 멈추거나 영향을 주지 않습니다. 이것은 많은 CPU 나 Resource 를 사용하지 않는다는 것입니다.

3. Hang 상태에 빠져있는지 판단하기 위해 약간의 대기 시간이 있습니다.

4. 잘못 동작하고 있는 Application 의 dump 파일을 생성할 수 있습니다.

5. 다수의 사용자를 처리할 수 있으며 안전하지 않은 곳에 dump 를 생성하지 않습니다. 이것은 user 의 temp 폴더에 생성됨을 이야기 합니다.

6. 제한된 숫자의 dump 파일을 생성하여 harddisk 를 다 사용하게 하지 않습니다.

7. Event log 에 hang 과 dump 관련된 event 를 기록 합니다.

8. Hang 을 찾았을 때 별도의 binary 를 실행할 수 있습니다.

9.

어떻게 동작하는가?

간단히 만들기 위해 dumphungwindow.exe 라는 consol application 을 만들었습니다. Dump 파일들을 수집할 때 까지 loop 를 돌 것이며 많은 시간 동안 깨어나서 각각의 window 는 SendMessageTimeout 을 설정하여 Message 를 전달할 것입니다. Timeout 때까지 어떤 Application 이 응답하지 않는다면 dump 를 생성하고 event log 에 기록할 것입니다.

Dumphungwindow.zip 파일과 badwindow.zip 파일을 아래에서 다운 받을 수 있습니다. 여기에는 Exe 파일과 Visual studio 2005 의 project 와 모든 소스가 들어 있습니다. 이 project 는 dumphungwindow 라고 불리며 badwindow 로 test 할 수 있습니다. 이 project 에는 3가지 다른 hang 경우가 있어 Test 할 수 있습니다.

The command line options are as follows.

C:\source\dumphungwindow\debug>dumphungwindow.exe /?
This sample application shows you how to use the debugger
help api to dump an application if it stop responding.

This tool depends on dbghelp.dll, this comes with the Microsoft debugger tools on www.microsoft.com

Please make sure you have the debugger tools installed before running this tool.
This tool is based on sample source code and is provided as is without warranty.

feel free to contact jeffda@microsoft.com to provide feedback on this sample application

/m[Number] Default is 5 dumps

The max number of dumps to take of hung windows before exiting.

/t[Seconds] Default is 5 seconds

The number of seconds a window must hang before dumping it.

/p[Seconds] Default is 0 seconds

The number of seconds to pause when dumping before continuing scan.

/s[Seconds] Default is 5 seconds.

The scan interval in seconds to wait before rescanning all windows.

/d[DUMP_FILE_PATH] The default is the SystemRoot folder

The path or location to place the dump files.

/e[EXECUTABLE NAME] This allows you to start another program if an application hangs

Dumphungwindow.exe 를 실행하기 위해 아래와 같이 하면 됩니다.

C:\source\dumphungwindow\debug>dumphungwindow.exe
Dumps will be saved in C:\Users\jeff\AppData\Local\Temp\
scanning for hung windows

****

bad application 을 실행하기 위해서는 dumphungwindows.zip 안의 Badwindowapp.zip 을 압축해제 하면 됩니다. 다음 badwindow.exe 의 menu 에서 hang / hang type 2 와 같이 선택하면 됩니다. 몇 초가 지난 후 findhungwindow 가 응답하지 않는 badwindow.exe 를 찾고 dump 를 생성할 것입니다.

Hung Window found dumping process (7064) badwindow.exe

Dumping unresponsive process

C:\Users\jeffda\AppData\Local\Temp\HWNDDump_Day5_29_2007_Time10_36_38_Pid7064_badwindow.exe.dmp

어떻게 hung window 를 찾는지 주석을 달아 놓았고 어떻게 dump 를 생성하는지 기술해 놓았으니 약간의 시간을 들여 Source 를 확인해 주십시오.

Dumphungwindow 와 badwindow 를 다운 받아 사용하십시오. Hang type 1 을 먼저 봐 주십시오. 먼저 Hang type 1 에 대해서 다음 blog 에서 다루고 몇 주가 흐른 후 나머지에 대해서 blog 하도록 하겠습니다. Dump 파일이 생성되었다면 windbg 의 file \ open crash dump 를 사용하여 확인할 수 있습니다.

여러분이 이 tool 을 손쉽게 찾고 사용하기를 바랍니다.

감사합니다. Jeff

/********************************************************************************************************************

Warranty Disclaimer

--------------------------

This sample code, utilities, and documentation are provided as is, without warranty of any kind. Microsoft further disclaims all

implied warranties including without limitation any implied warranties of merchantability or of fitness for a particular purpose.

The entire risk arising out of the use or performance of the product and documentation remains with you.

In no event shall Microsoft be liable for any damages whatsoever (including, without limitation, damages for loss of business

profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to

use the sample code, utilities, or documentation, even if Microsoft has been advised of the possibility of such damages.

Because some states do not allow the exclusion or limitation of liability for consequential or incidental damages, the above

limitation may not apply to you.

********************************************************************************************************************/

#include <stdio.h>

#include <windows.h>

#include <dbghelp.h>

#include <psapi.h>

// don't warn about old school strcpy etc.

#pragma warning( disable : 4996 )

int iMaxDump=5;

int iDumpsTaken=0;

int iHangTime=5000;

int iDumpPause=1;

int iScanRate=5000;

HANDLE hEventLog;

char * szDumpLocation;

int FindHungWindows(void);

char * szDumpFileName = 0;

char * szEventInfo = 0;

char * szDumpFinalTarget = 0;

char * szModName = 0;

char * szAppname = 0;

DWORD dwExecOnHang = 0;

#define MAXDUMPFILENAME 1000

#define MAXEVENTINFO 5000

#define MAXDUMPFINALTARGET 2000

#define MAXDUMPLOCATION 1000

#define MAXAPPPATH 1000

#define MAXMODFILENAME 500

#define HMODSIZE 255

int main(int argc, char * argv[])

{

int i;

int z;

size_t j;

char scan;

// check to make sure we have dbghelp.dll on the machine.

if(!LoadLibrary("dbghelp.dll"))

{

printf("dbghelp.dll not found please install the debugger tools and place this tool in \r\nthe debugging tools directory or a copy of dbghelp.dll in this tools directory\r\n");

return 0;

}

// Allocate a buffer for our dump location

szDumpLocation = (char *)malloc(MAXDUMPLOCATION);

{

if(!szDumpLocation)

{

printf("Failed to alloc buffer for szdumplocation %d",GetLastError());

return 0;

}

}

szAppname = (char *)malloc(MAXAPPPATH);

{

if(!szAppname)

{

printf("Failed to alloc buffer for szAppname %d",GetLastError());

return 0;

}

}

// We use temp path because if we are running under terminal server sessions we want the dump to go to each

// users secure location, ie. there private temp dir.

GetTempPath(MAXDUMPLOCATION, szDumpLocation );

for (z=0;z<argc;z++)

{

switch(argv[z][1])

{

case '?':

{

printf("\n This sample application shows you how to use the debugger \r\n help api to dump an application if it stop responding.\r\n\r\n");

printf("\n This tool depends on dbghelp.dll, this comes with the Microsoft debugger tools on www.microsoft.com");

printf("\n Please make sure you have the debugger tools installed before running this tool.");

printf("\n This tool is based on sample source code and is provided as is without warranty.");

printf("\n feel free to contact jeffda@microsoft.com to provide feedback on this sample application\r\n\r\n");

printf(" /m[Number] Default is 5 dumps\r\n The max number of dumps to take of hung windows before exiting.\r\n\r\n");

printf(" /t[Seconds] Default is 5 seconds\r\n The number of seconds a window must hang before dumping it. \r\n\r\n");

printf(" /p[Seconds] Default is 0 seconds\r\n The number of seconds to pause when dumping before continuing scan. \r\n\r\n");

printf(" /s[Seconds] Default is 5 seconds.\r\n The scan interval in seconds to wait before rescanning all windows.\r\n\r\n");

printf(" /d[DUMP_FILE_PATH] The default is the SystemRoot folder\r\n The path or location to place the dump files. \r\n\r\n");

printf(" /e[EXECUTABLE NAME] This allows you to start another program if an application hangs\r\n\r\n");

return 0;

}

case 'm':

case 'M':

{

iMaxDump = atoi(&argv[z][2]);

break;

}

case 't':

case 'T':

{

iHangTime= atoi(&argv[z][2]);

iHangTime*=1000;

break;

}

case 'p':

case 'P':

{

iDumpPause= atoi(&argv[z][2]);

iDumpPause*=1000;

break;

}

case 's':

case 'S':

{

iScanRate = atoi(&argv[z][2]);

iScanRate*=1000;

break;

}

case 'd':

case 'D':

{ // Dump file directory path

strcpy(szDumpLocation,&argv[z][2]);

j = strlen(szDumpLocation);

if (szDumpLocation[j-1]!='\\')

{

szDumpLocation[j]='\\';

szDumpLocation[j+1]=NULL;

}

break;

}

case 'e':

case 'E':

{ // applicaiton path to exec if hang happens

strcpy(szAppname,&argv[z][2]);

dwExecOnHang = 1;

break;

}

}

}

printf("Dumps will be saved in %s\r\n",szDumpLocation);

puts("scanning for hung windows\n");

hEventLog = OpenEventLog(NULL, "HungWindowDump");

i=0;

scan='*';

while(1)

{

if(i>20)

{

if ('*'==scan)

{

scan='.';

}

else

{

scan='*';

}

printf("\r");

i=0;

}

i++;

putchar(scan);

if(!FindHungWindows())

{

return 0;

}

if (iMaxDump == iDumpsTaken)

{

printf("\r\n%d Dumps taken, exiting\r\n",iDumpsTaken);

return 0;

}

Sleep(iScanRate);

}

free(szDumpLocation);

return 0;

}

int FindHungWindows(void)

{

DWORD dwResult = 0;

DWORD ProcessId = 0;

DWORD tid = 0;

DWORD dwEventInfoSize = 0;

// Handles

HWND hwnd = 0;

HANDLE hDumpFile = 0;

HANDLE hProcess = 0;

HRESULT hdDump = 0;

SYSTEMTIME SystemTime;

MINIDUMP_TYPE dumptype = (MINIDUMP_TYPE) (MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithProcessThreadData);

// These buffers are presistant.

// security stuff to report the SID of the dumper to the event log.

PTOKEN_USER pInstTokenUser;

HANDLE ProcessToken;

TOKEN_INFORMATION_CLASS TokenInformationClass = TokenUser;

DWORD ReturnLength =0;

// This allows us to get the first window in the chain of top windows.

hwnd = GetTopWindow(NULL);

if(!hwnd)

{

printf("Could not GetTopWindow\r\n");

return 0;

}

// We will iterate through all windows until we get to the end of the list.

while(hwnd)

{

// Get the process ID for the current window

tid = GetWindowThreadProcessId(hwnd, &ProcessId);

// Sent a message to this window with our timeout.

// If it times out we consider the window hung

if (!SendMessageTimeout(hwnd, WM_NULL, 0, 0, SMTO_BLOCK, iHangTime, &dwResult))

{

// SentMessageTimeout can fail for other reasons,

// if it's not a timeout we exit try again later

if(ERROR_TIMEOUT != GetLastError())

{

printf("SendMessageTimeout has failed with error %d\r\n",GetLastError());

return 1;

}

// Iint our static buffers points.

// On our first trip through if we have not

// malloced memory for our buffers do so now.

if(!szModName)

{

szModName = (char *)malloc(MAXMODFILENAME);

{

if(!szModName)

{

printf("Failed to alloc buffer for szModName %d",GetLastError());

return 0;

}

}

}

if(!szDumpFileName)// first time through malloc a buffer.

{

szDumpFileName = (char *)malloc(MAXDUMPFINALTARGET);

{

if(!szDumpFileName)

{

printf("Failed to alloc buffer for dumpfilename %d",GetLastError());

return 0;

}

}

}

if(!szDumpFinalTarget)// first time through malloc a buffer.

{

szDumpFinalTarget= (char *)malloc(MAXDUMPFINALTARGET);

{

if(!szDumpFinalTarget)

{

printf("Failed to alloc buffer for dumpfiledirectory %d",GetLastError());

return 0;

}

}

}

if(!szEventInfo)

{

szEventInfo= (char *)malloc(MAXEVENTINFO);

{

if(!szEventInfo)

{

printf("Failed to alloc buffer for szEventInfo %d",GetLastError());

return 0;

}

}

}

// End of initial buffer allocations.

GetLocalTime (&SystemTime);

// Using the process id we open the process for various tasks.

hProcess = OpenProcess(PROCESS_ALL_ACCESS,NULL,ProcessId);

if(!hProcess )

{

printf("Open process of hung window failed with error %d\r\n",GetLastError());

return 1;

}

// What is the name of the executable?

GetModuleBaseName( hProcess, NULL, szModName,MAXMODFILENAME);

printf("\r\n\r\nHung Window found dumping process (%d) %s\n",ProcessId,szModName);

// Here we build the dump file name time, date, pid and binary name

sprintf(szDumpFileName,"HWNDDump_Day%d_%d_%d_Time%d_%d_%d_Pid%d_%s.dmp",SystemTime.wMonth,SystemTime.wDay,SystemTime.wYear,SystemTime.wHour,SystemTime.wMinute,SystemTime.wSecond,ProcessId,szModName);

strcpy(szDumpFinalTarget,szDumpLocation);

strcat(szDumpFinalTarget,szDumpFileName);

// We have to create the file and then pass it's handle to the dump api

hDumpFile = CreateFile(szDumpFinalTarget,FILE_ALL_ACCESS,0,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

if(!hDumpFile)

{

printf("CreateFile failed to open dump file at location %s, with error %d\r\n",szDumpLocation,GetLastError());

return 0;

}

printf("Dumping unresponsive process\r\n%s",szDumpFinalTarget);

// This dump api will halt the target process while it writes it's

// image to disk in the form a dump file.

// this can be opened later by windbg or cdb for debugging.

if(!MiniDumpWriteDump(hProcess,ProcessId,hDumpFile,dumptype ,NULL,NULL,NULL))

{

// We do this on failure

hdDump = HRESULT_FROM_WIN32(GetLastError());

printf("MiniDumpWriteDump failed with a hresult of %d last error %d\r\n",hdDump,GetLastError());

CloseHandle (hDumpFile);

return 0;

}

else

{

// If we are here the dump worked. Now we need to notify the machine admin by putting a event in

// the application event log so someone knows a dump was taken and where it is stored.

sprintf(szEventInfo,"An application hang was caught by findhungwind.exe, the process was dumped to %s",szDumpFinalTarget);

// We need to get the process token so we can get the user sit so ReportEvent will have the

// User name / account in the event log.

if (OpenProcessToken(hProcess, TOKEN_QUERY,&ProcessToken ) )

{

// Make the firt call to findout how big the sid needs to be.

GetTokenInformation(ProcessToken,TokenInformationClass, NULL,NULL,&ReturnLength);

pInstTokenUser = (PTOKEN_USER) malloc(ReturnLength);

if(!pInstTokenUser)

{

printf("Failed to malloc buffer for InstTokenUser exiting error %d\r\n",GetLastError());

return 0;

}

if(!GetTokenInformation(ProcessToken,TokenInformationClass, (VOID *)pInstTokenUser,ReturnLength,&ReturnLength))

{

printf("GetTokenInformation failed with error %d\r\n",GetLastError());

return 0;

}

}

// write the application event log message.

// This will show up as source DumpHungWindow

dwEventInfoSize=(DWORD)strlen(szEventInfo);

ReportEvent(hEventLog,EVENTLOG_WARNING_TYPE,1,1,pInstTokenUser->User.Sid,NULL,dwEventInfoSize,NULL,szEventInfo);

// Free to token buffer, we don't want to leak anything.

free(pInstTokenUser);

// In additon to leaking a handle if you don't close the handle

// you may not get the dump to flush to the hard drive.

CloseHandle (hDumpFile);

printf("\r\nDump complete");

// This allows you to execute something if you get a hang like crash.exe

if (dwExecOnHang)

{

system(szAppname);

}

// The Sleep is here so in the event you want to wait N seconds

// before collecting another dump

// you can pause. This is helpful if you want to see if any

// forward progress is happening over time

Sleep(iDumpPause);

}

// Once we are at our threadshold for max dumps

// we exit so we do not fill up the hard drive.

iDumpsTaken++;

if (iMaxDump == iDumpsTaken)

{

return 0;

}

}

// This is where we traverse to the next window.

hwnd = GetNextWindow(hwnd, GW_HWNDNEXT);

}

return 1;

}