Tutorial 28: Win32 Debug API Part 1
이번 장에서는, Win32에서 제공되는 디버그에 관련된 기초지식을 소개한다. 이 장을 읽게되면, 어떻게 프로그램을 디버그 할 수 있을지를 알게 될 것이다.
메인 소스 | 실행 결과 1 | 실행 결과 2 | 실행 결과 3 | 실행 결과 4 |
|
Win32에는 프로그램을 디버그 하기 위해 필요한 API가 몇가지 존재하며, 이들은, Win32 디버그 API라고 불려진다. 이런 API를 사용하면, 다음과 같은 일이 가능해 진다.
- 프로그램을 실행하고, 디버그 대상이 실행되는중에 프로그램에서 포함시킬 수 있다.(Attatch)
- 디버그 하고 있는 프로그램에 대해, 프로세스 ID나 엔트리 포인트의 주소, 이미지 베이스에 대한 정보등의 상세 정보를 알아 낼 수 있다.
- 프로세스나 thread가 실행, 종료했을 때나, DLL이 로드, 언로드되었을 때 등의 디버그에 관련한 이벤트가 발생한 것을 알아 낼 수 있다.
- 디버그 대상의 thread나 프로세스에 대한 변경을 수행할 수 있다
즉, 이런 API를 사용하면 간단한 디버거를 만들 수도 있는 것이다.
이번 주제는 매우 방대한 내용이므로, 3개의 부분으로 나눴으며. 이 번장은 첫번째로, 기본적인 컨셉과 Win32 디버그 API의 사용 방법을 설명한다.Win32 디버그 API의 사용 방법은 다음과 같은 순서로 진행된다.
- 프로세스를 실행하고, 실행중인 프로그램에 추가 한다
이것이 첫번째로 수행할 작업이다. 이번 프로그램은 디버거로 실행되므로, 디버그할 대상프로그램이 필요하다. 그런 프로그램을 디버그 대상 프로그램이라 부른다(디버기 : Debugee). 디버그 대상 프로그램을 얻는 방법은 다음과 같이 두가지가 있다.
CreateProcess 함수를 사용해서 디버그 대상 프로그램을 실행한다. 이때, 디버그 작업을 표시하는 DEBUG_PROCESS플래그를 설정해야 한다. 이것에 의해 Windows에 디버그 하는 것을 선언할 수 있고, Windows는 이 프로그램에서 발생한 디버그에 관한 이벤트를 통지해 주게 된다.
디버그 대상 프로세스는 디버그 프로그램의 준비가 완료될 때까지 정지된 채로 있게된다. 만약 디버그 대상 프로세스가 child process를 실행하는 경우는, 그런 child process에 대해서 발생한 디버그 이벤트도 모두 전송해 준다. 이런 동작이 마음에 들지 않는다면, DEBUG_ONLY_THIS_PROCESS플래그를 지정하면 된다.- DebugActiveProcess로 실행중인 프로그램에 추가 할 수 있다.
- 디버그 이벤트의 감시
디버그 대상 프로그램이 정해진 후, 디버그 대상 프로그램의 프라이머리 thread는 정지한 상태가 되고, WaitForDebugEvent함수를 호출 할 때까지 계속 정지된 상태로 있게 된다. 이 함수는 다른 WaitForXXX 함수와 같이 작동해서, 어떤 이벤트가 발생할 때까지 중지하고 있지만, 이경우는 Windows로부터 디버그 이벤트가 전송되는 것을 기다리고 있는 것이다.
그럼 정의를 보자.
WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD
- lpDebugEvent는DEBUG_EVENT구조체의 포인터로, 디버그 대상 프로그램으로 부터 발생한 이벤트에 관한 정보가 저장되어 있다.
- dwMilliseconds로 지정되는 밀리 세컨드단위로 설정한 디버그 이벤트가 발생할 때까지 대기하게 되어 있다. 만약에 이 기간중에 디버그 이벤트가 발생하지 않는다면, WaitForDebugEvent가 호출되서 작동하게 되어 있다. 한편,INFINITE를 지정하면, 디버그 이벤트가 발생할 때까지 제어가 반환지 않게 된다.
그럼 DEBUG_EVENT 구조체에 대해 자세히 알아 보자
DEBUG_EVENT STRUCT dwDebugEventCode dd ? dwProcessId dd ? dwThreadId dd ? u DEBUGSTRUCT <> DEBUG_EVENT ENDS
- dwDebugEventCode에는 발생한 디버그 이벤트의 타입이 저장되어 있다. 즉, 이벤트 타입에는 매우 많은것이 존재하므로, 이 필드를 체크해서 어떤 이벤트가 발생했는지를 알 수 있다. 이벤트에는 다음과 같은 것이 있다.
이벤트 의미 CREATE_PROCESS_DEBUG_EVENT 프로세스가 생성된 것을 나타낸다. 이벤트는, 디버그 대상 프로세스가 생성되는 순간( 아직 실행되고 있지는 않다), 혹은DebugActiveProcess함수로 실행중인 프로그램에 추가되는 순간에 발생한다. 이 이벤트는 가장 먼저 발생하는 이벤트이다. EXIT_PROCESS_DEBUG_EVENT 프로세스가 종료된 것을 나타낸다. CREATE_THREAD_DEBUG_EVENT 디버그 대상 프로세스로 새로운 thread가 생성되거나, 또는 실행중인 프로세스에 처음으로 추가될 경우에 발생한다. 다만, 디버그 대상 프로그램의 메인 thread가 생성될때 이 이벤트가 발생하는것이 아님을 주의하길 바란다. EXIT_THREAD_DEBUG_EVENT 디버그 대상 프로그램의 thread가 종료된 것을 의미한다. 다만, 이 이벤트는 메인 thread가 종료했을 때에 받는것이 아니다. 즉, 메인 thread는 디버그 대상 프로세스 자신과 동등하게 취급이 되므로,EXIT_PROCESS_DEBUG_EVENT가 발생했다고 하는 것은, 메인 thread에서 EXIT_THREAD_DEBUG_EVENT가 발생했다고 인식 하는 것이다. LOAD_DLL_DEBUG_EVENT 디버그 대상 프로그램이 DLL를 로드한 것을 나타낸다. 이 이벤트를 받는 것은, PE로더가 DLL에의 링크를 해결했을 때(디버그 대상 프로그램이CreateProcess함수를 호출 했을 때)와 디버그 대상 프로그램이 LoadLibrary 함수를 호출 했을 때이다. UNLOAD_DLL_DEBUG_EVENT 디버그 대상 프로그램으로부터 DLL가 언로드된 것을 나타낸다. EXCEPTION_DEBUG_EVENT 디버그 대상 프로그램으로 예외가 발생한 것을 나타낸다.
※중요 이 이벤트는 디버그 대상 프로그램이 제일 처음의 명령을 실행할 때 한 번만 발생한다. 그 예외는 디버그 브레이크(int 3h)이다. 디버그 대상 프로그램이 계속 실행하는 경우,DBG_CONTINUE를 지정해서, ContinueDebugEvent함수를 호출하면 된다. 절대로 DBG_EXCEPTION_NOT_HANDLED플래그를 지정해서는 안되며, 만약 지정한다면, WindowsNT에서는 디버그 대상 프로그램이 오작동 하게된다(Win98에서는 정상작동함). OUTPUT_DEBUG_STRING_EVENT DebugOutputString함수를 호출해서 메세지를 전송하는 것을 나타낸다. RIP_EVENT 시스템 디버그 에러가 발생한 것을 나타낸다. - dwProcessId 와 dwThreadId는 각각, 디버그 이벤트가 발생한 프로세스 ID와 thread ID이다. 이 ID를 이용해서 디버그 대상 프로그램을 지정한다. CreateProcess함수를 사용해서 디버그 대상 프로그램을 실행 했을 경우,PROCESS_INFO 구조체로부터 디버그 대상 프로그램의 프로세스 ID와 thread ID를 구할 수 있다는 것을 기억하자.
이러한 ID는 디버그 대상 프로그램과 그 프로그램의 child process를 판별하기 위해서(DEBUG_ONLY_THIS_PROCESS플래그를 지정하지 않은 경우) 사용할 수 있다.- u에는 디버그 이벤트에 대해보다 자세한 정보가 저장되어 있다. 상기의 dwDebugEventCode가 어떤 값을 받는지에 따라, 다음의 구조체가 어떤 것인지가 판가름 난다.
dwDebugEventCode 값 u의 해석 CREATE_PROCESS_DEBUG_EVENT CREATE_PROCESS_DEBUG_INFO구조체 EXIT_PROCESS_DEBUG_EVENT EXIT_PROCESS_DEBUG_INFO구조체 CREATE_THREAD_DEBUG_EVENT CREATE_THREAD_DEBUG_INFO구조체 EXIT_THREAD_DEBUG_EVENT EXIT_THREAD_DEBUG_EVENT구조체 LOAD_DLL_DEBUG_EVENT LOAD_DLL_DEBUG_INFO구조체 UNLOAD_DLL_DEBUG_EVENT UNLOAD_DLL_DEBUG_INFO구조체 EXCEPTION_DEBUG_EVENT EXCEPTION_DEBUG_INFO구조체 OUTPUT_DEBUG_STRING_EVENT OUTPUT_DEBUG_STRING_INFO구조체 RIP_EVENT RIP_INFO구조체 이 설명서에서는, CREATE_PROCESS_DEBUG_INFO구조체 외의 어떠한 설명도 하지 않는다.
이 프로그램에서는WaitForDebugEvent함수를 호출하고, 그 후 제어가 반환된 것이라고 가정한다. 먼저 처음에, dwDebugEventCode의 값에 따라, 어떤 디버그 이벤트가 발생했는지를 판단하자. 예를 들면, CREATE_PROCESS_DEBUG_EVENT라면 멤버 변수u를 CreateProcessInfo구조체로 취급할 수 있다.- 발생한 디버그 이벤트에 대해서 어떠한 처리를 수행한다. WaitForDebugEvent로 부터 제어가 반환되면, 디버그 대상 프로그램에서 디버그 이벤트가 발생했는지, 혹은 타임 아웃 되었다는 식이다. 그 후, dwDedbugEventCode의 값을 조사해서 어떻게 처리할지를 프로그래밍 한다.
이 처리는, Windows 메세지와 같이, 자신이 처리하고 싶은 이벤트만을 대상으로 하면 좋다.- 디버그 대상 프로그램의 실행을 재개시킨다. 디버그 이벤트가 발생하면, Windows는 디버그 대상 프로그램을 정지시킨다. 디버그 이벤트에 대한 처리가 끝나면, 재개시킬 필요가 있으므로,ContinueDebugEvent 함수를 호출 해야 한다.
ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD이 함수는, 디버그 이벤트가 발생해서 정지되어 있던 프로그램을 재개 시킬 수 있다. dwProcessId 와 dwThreadId는 재개시키고 싶은 프로그램의 프로세스 핸들과 thread 핸들이 된다. 이들 2개의 값은 DEBUG_EVENT구조체로부터 구하게 된다.
dwContinueStatus 값에 의해, 어떻게 thread를 실행시키는지를 지정한다. DBG_CONTINUE 와 DBG_EXCEPTION_NOT_HANDLED라는 2개의 값을 받을 수가 있다. 디버그 이벤트 전반에 대한 것이지만, 이들 2개의 값은 모두 thread를 재개한다.
예외는 EXCEPTION_DEBUG_EVENT이다. thread는 예외 디버그 이벤트가 발생하면, 디버그 대상 thread에 예외가 발생했다고 인식하는 것이다. DBG_CONTINUE를 지정하면, thread 자신은 예외 처리를 무시하고, 계속 처리를 수행하게 된다.
이 경우, thread를 재개하기 전에 디버그 프로그램에서 예외에 대한 처리를 해야만 한다. 그렇게 하지 않으면 예외가 계속해서 발생하게 된다.
DBG_EXCEPTION_NOT_HANDLED을 지정하면, 예외를 발생하지 않도록 되어, Windows가 디폴트 예외 처리를 해 주게 된다.따라서, 디버그 이벤트가 디버그 대상 프로세스의 예외를 참조하는 경우나, 예외 발생을 억제할 수 없는 경우에는, DBG_CONTINUE를 인수로 해서 ContinueDebugEvent함수를 호출한다.
혹은, DBG_EXCEPTION_NOT_HANDLED 플래그를 지정해서, ContinueDebugEvent함수를 호출 해야 한다. 다만, 어떤처리에서도, ExceptionCode 멤버의 값 EXCEPTION_BREAKPOIN로 EXCEPTION_DEBUG_EVENT가 발생했을 경우에 대해서는 항상 DBG_CONTINUE플래그를 지정해 줘야 한다.이것은, 디버그 대상 프로그램이 처음 명령을 실행할 때, 예외 디버그 이벤트(디버그 브레이크:int 3h)가 발생하지만, DBG_EXCEPTION_NOT_HANDLED플래그를 지정해서 ContinueDebugEvent함수를 호출 하면, Windows는 디버그 대상 프로그램의 실행을 중지 시켜 버린다(예외 이벤트를 처리하는 프로그램이 없기 때문에).
그래서, Windows가 thread의 실행을 계속 수행 할 수 있도록, DBG_CONTINUE플래그를 지정해 줘야 한다.
- 디버그 대상 프로그램이 종료할 때까지 이 작업을 계속 반복적으로 수행한다. 이는 메시지 루프와 아주 유사한 작동이다. 다음과 같은 루프로 처리하게 된다.
.while TRUE invoke WaitForDebugEvent, addr DebugEvent, INFINITE .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT <Handle the debug events> invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED .endw여기서 문제는, 일단 디버그 프로그램이 실행되면, 디버그 대상 프로그램이 종료할 때까지 디버그 대상 프로그램에서 제어가 반환 되지 않는다는 것이다.
중요하므로 복습 해 보자.
- 프로세스를 생성하고 실행중인 프로그램에 추가(Attatch) 한다
- 디버그 이벤트가 발생하기를 기다린다
- 디버그 이벤트가 발생하면 처리를 한다
- 디버그 대상 프로그램을 계속 실행한다
- 디버그 대상 프로세스가 종료할 때까지 2에서 4를 반복해서 수행한다
|
이 예제는 win32 프로그램을 디버그하고, 프로세스 핸들이나 프로세스 ID, 이미지 베이스라는 중요한 정보를 표시한다
.386 .model flat, stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\user32.lib .data AppName db "Win32 Debug Example no. 1", 0 ofn OPENFILENAME <> FilterString db "Executable Files", 0,"*. exe", 0 db "All Files", 0,"*. *", 0,0 ExitProc db "The debuggee exits", 0 NewThread db "A new thread is created", 0 EndThread db "A thread is destroyed", 0 ProcessInfo db "File Handle: %lx ", 0dh, 0Ah db "Process Handle: %lx", 0Dh, 0Ah db "Thread Handle: %lx", 0Dh, 0Ah db "Image Base: %lx", 0Dh, 0Ah db "Start Address: %lx", 0 .data? buffer db 512 dup(? ) startinfo STARTUPINFO <> pi PROCESS_INFORMATION <> DBEvent DEBUG_EVENT <> .code start: mov ofn.lStructSize, sizeof ofn mov ofn.lpstrFilter, offset FilterString mov ofn.lpstrFile, offset buffer mov ofn.nMaxFile, 512 mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY invoke GetOpenFileName, ADDR ofn .if eax==TRUE invoke GetStartupInfo, addr startinfo invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi\ .while TRUE invoke WaitForDebugEvent, addr DBEvent, INFINITE .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION .break .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile,\ DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread,\ DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE .continue .endif .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT invoke MessageBox, 0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT invoke MessageBox, 0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION .endif invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED .endw invoke CloseHandle, pi.hProcess invoke CloseHandle, pi.hThread .endif invoke ExitProcess, 0 end start
|
상기 프로그램에서는, OPENFILENAME 구조체에 값을 설정해서, 유저에게 디버그 하는 프로그램을 선택하기 위해서 GetOpenFileName 함수를 호출하고 있다.
invoke GetStartupInfo, addr startinfo invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE,\ DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi유저가 프로그램을 선택하면,CreateProcess함수를 호출해서 프로그램을 로드한다. 호출할 때의 인수로서 STARTUPINFO구조체가 필요하므로, 사전에 GetStartupInfo함수로 초기화 해 둔다.
.while TRUE invoke WaitForDebugEvent, addr DBEvent, INFINITE디버그 대상 프로그램이 로드 되면, WaitForDebugEvent함수를 호출해서 디버그 루프에 들어간다. WaitForDebugEvent함수의 두번째 인수에 INFINITE를 지정했으므로, 디버그 이벤트가 발생할 때까지 제어가 반환 되지 않는다.
디버그 이벤트가 발생하면 WaitForDebugEvent함수는 제어를 반환하고, DBEvent 변수에 디버그 이벤트에 관한 데이터가 들어가 있게 된다.
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION .break가장 먼저 dwDebugEventCode의 값을 검사한다. 만약 EXIT_PROCESS_DEBUG_EVENT 라면 "The debuggee exits"라는 메시지 박스를 표시하고, 디버그 루프를 빠져 나간다.
.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile,\ DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread,\ DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATIONdwDebugEventCode의 값이 CREATE_PROCESS_DEBUG_EVENT라면 디버그 대상 프로그램에 대한 정보를 메시지 박스로 표시한다. 그런 정보는 u.CreateProcessInfo의 값을 참조한다. CreateProcessInfo는 CREATE_PROCESS_DEBUG_INFO구조체의 변수로서, Win32API 레퍼런스에 이 구조체에 대한 자세한 설명이 있으므로 참조하기 바란다.
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE .continue .endifdwDebugEventCode가 EXCEPTION_DEBUG_EVENT라면, 보다 상세하게 예외 형태를 검사해줘야 한다. 이 코드를 보면 알수 있듯이, 구조체의 구조체로 아주 복잡한 면은 있지만, 결론적으로는 ExceptionCode변수를 통해서 예외의 종류를 얻을 수 있다. 만약 ExceptionCode가 EXCEPTION_BREAKPOINT라면 그것이 처음(없지는 않지만 int3h 명령이 디버그 대상 프로그램에 없는경우)인 경우는, 디버그 대상 프로그램이 가장 처음으로 명령을 수행할 경우 발생한 예외라고 가정해도 좋다.
이 예외에 대해서 어떤 처리를 수행하는 경우, 디버그 대상 프로그램을 계속 실행하기 위해, DBG_CONTINUE플래그를 지정해서, ContinueDebugEvent함수를 호출 한다. 그 후, 다음 디버그 이벤트가 발생할 때 까지 계속 기다리게 된다.
.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT invoke MessageBox, 0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT invoke MessageBox, 0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION .endifdwDebugEventCode가 CREATE_THREAD_DEBUG_EVENT 나 EXIT_THREAD_DEBUG_EVENT인 경우는, 메세지 그대로 출력한다.
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED .endw상기의 EXCEPTION_DEBUG_EVENT인 경우를 제외하고, 디버그 대상 프로그램을 계속 실행하기 위해, DBG_EXCEPTION_NOT_HANDLED를 인수로 지정해서, ContinueDebugEvent함수를 호출하고 있다.
invoke CloseHandle, pi.hProcess invoke CloseHandle, pi.hThread디버그 대상 프로그램이 종료하면, 디버그 루프를 빠져 나가게 되고, 그 후 디버그 대상 프로그램의 프로세스 핸들과 thread 핸들을 닫아준다. 핸들을 닫는다는 것은 프로세스나 thread를 kill 하는 것은 아니고, 단지 이 프로그램에서 더이상 해당 핸들을 사용하지 않는다는 의미이다.