MinDBG: A Simple Win32 Debugger

[Previous] [Next]

From a distance, a Win32 debugger is a simple program, with only a couple requirements. The first requirement is that the debugger must pass a special flag in the dwCreationFlags parameter to CreateProcess: DEBUG_ONLY_THIS_PROCESS. This flag tells the operating system that the calling thread will enter a debug loop to control the process it's starting. If the debugger can handle multiple processes spawned by the initial debuggee, it will pass DEBUG_PROCESS as the creation flag.

As you can see from the call to CreateProcess above, in the Win32 Debugging API, the debugger and debuggee are in separate processes, making the Win32 operating systems much more robust when debugging. Even if the debuggee has wild memory writes, the debuggee won't crash the debugger. (Debuggers in the 16-bit Windows and Macintosh operating systems are susceptible to debuggee mischief because the debugger and the debuggee run in the same process context.)

The second requirement is that after the debuggee starts, the debugger must enter into a loop calling the WaitForDebugEvent API function to receive debugging notifications. When it has finished processing a particular debugging event, it calls ContinueDebugEvent. Be aware that only the thread that called CreateProcess with the special debug creation flags can call the Debugging API functions. The following pseudocode shows just how little code is required to create a Win32 debugger:

 void main ( void ) {     CreateProcess ( ..., DEBUG_ONLY_THIS_PROCESS ,... ) ;     while ( 1 == WaitForDebugEvent ( ... ) )     {         if ( EXIT_PROCESS )         {             break ;         }         ContinueDebugEvent ( ... ) ;  } }

As you can see, a minimal Win32 debugger doesn't require multithreading, a user interface, or much of anything else. Nevertheless, as with most applications in Windows, the difference between minimal and reasonable is considerable. In reality, the Win32 Debugging API almost dictates that the actual debug loop needs to sit in a separate thread. As the name implies, WaitForDebugEvent blocks on an internal operating system event until the debuggee performs some operation that makes the operating system stop the debuggee so that it can tell the debugger about the event. If your debugger had a single thread, your user interface would totally hang until the debuggee triggered a debug event.

During the time a debugger sits in the debug loop, it receives various notifications that certain events took place in the debuggee. The following DEBUG_EVENT structure, which is filled in by the WaitForDebugEvent function, contains all the interesting information about a debug event. Table 4-1 describes each of the individual events.

 typedef struct _DEBUG_EVENT {     DWORD dwDebugEventCode;     DWORD dwProcessId;     DWORD dwThreadId;     union {         EXCEPTION_DEBUG_INFO Exception;         CREATE_THREAD_DEBUG_INFO CreateThread;         CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;         EXIT_THREAD_DEBUG_INFO ExitThread;         EXIT_PROCESS_DEBUG_INFO ExitProcess;         LOAD_DLL_DEBUG_INFO LoadDll;         UNLOAD_DLL_DEBUG_INFO UnloadDll;         OUTPUT_DEBUG_STRING_INFO DebugString;         RIP_INFO RipInfo;     } u; } DEBUG_EVENT

Table 4-1 Debugging Events

Debugging EventDescription
CREATE_PROCESS_DEBUG_EVENT This debugging event is generated whenever a new process is created in a process being debugged or whenever the debugger begins debugging an already active process. The kernel generates this debugging event before the process begins to execute in user mode and before the kernel generates any other debugging events for the new process.

The DEBUG_EVENT structure contains a CREATE_PROCESS_DEBUG_INFO structure. This structure includes a handle to the new process, a handle to the process's image file, a handle to the process's initial thread, and other information that describes the new process.

The handle to the process has PROCESS_VM- _READ and PROCESS_VM_WRITE access. If a debugger has these types of access to a thread, it can read and write to the process's memory by using the ReadProcessMemory and WriteProcessMemory functions.

The handle to the process's image file has GENERIC_READ access and is opened for read-sharing.

The handle to the process's initial thread has THREAD_GET_CONTEXT, THREAD_SET- _CONTEXT, and THREAD_SUSPEND_RESUME access to the thread. If a debugger has these types of access to a thread, it can read from and write to the thread's registers by using the GetThreadContext and SetThreadContext functions and can suspend and resume the thread by using the SuspendThread and ResumeThread functions.

CREATE_THREAD_DEBUG_EVENT This debugging event is generated whenever a new thread is created in a process being debugged or whenever the debugger begins debugging an already active process. This debugging event is generated before the new thread begins to execute in user mode.

The DEBUG_EVENT structure contains a CREATE_THREAD_DEBUG_INFO structure. This structure includes a handle to the new thread and the thread's starting address. The handle has THREAD_GET_CONTEXT, THREAD_SET- _CONTEXT, and THREAD_SUSPEND_RESUME access to the thread. If a debugger has these types of access to a thread, it can read from and write to the thread's registers by using the GetThreadContext and SetThreadContext functions and can suspend and resume the thread by using the SuspendThread and ResumeThread functions.

EXCEPTION_DEBUG_EVENT This debugging event is generated whenever an exception occurs in the process being debugged. Possible exceptions include attempting to access inaccessible memory, executing breakpoint instructions, attempting to divide by 0, or any other exception noted in the Platform SDK topic, "Structured Exception Handling."

The DEBUG_EVENT structure contains an EXCEPTION_DEBUG_INFO structure. This structure describes the exception that caused the debugging event.

Besides the standard exception conditions, an additional exception code can occur during console process debugging. The kernel generates a DBG_CONTROL_C exception code when Ctrl+C is input to a console process that handles Ctrl+C signals and is being debugged. This exception code isn't meant to be handled by applications. An application should never use an exception handler to deal with it. It is raised only for the benefit of the debugger and is used only when a debugger is attached to the console process.

If a process isn't being debugged or if the debugger passes on the DBG_CONTROL_C exception unhandled, the application's list of handler functions is searched. (For more information about console process handler functions, see the MSDN documentation for the SetConsoleCtrlHandler function.)

EXIT_PROCESS_DEBUG_EVENT This debugging event is generated whenever the last thread in a process being debugged exits. It occurs immediately after the kernel unloads the process's DLLs and updates the process's exit code.

The DEBUG_EVENT structure contains an EXIT_PROCESS_DEBUG_INFO structure that specifies the exit code.

The debugger deallocates any internal structures associated with the process on receipt of this debugging event. The kernel closes the debugger's handle to the exiting process and all the process's threads.

EXIT_THREAD_DEBUG_EVENT This debugging event is generated whenever a thread that is part of a process being debugged exits. The kernel generates this debugging event immediately after it updates the thread's exit code.

The DEBUG_EVENT structure contains an EXIT_THREAD_DEBUG_INFO structure that specifies the exit code.

The debugger deallocates any internal structures associated with the thread on receipt of this debugging event. The system closes the debugger's handle to the exiting thread.

This debugging event doesn't occur if the exiting thread is the last thread of a process. In this case, the EXIT_PROCESS_DEBUG_EVENT debugging event occurs instead.

LOAD_DLL_DEBUG_EVENT This debugging event is generated whenever a process being debugged loads a DLL. This debugging event occurs when the system loader resolves links to a DLL or when the debugged process uses the LoadLibrary function. This debugging event is called each time the DLL loads into the address space. If the DLL's reference count falls to 0, the DLL is unloaded. The next time the DLL is loaded, this event will be generated again.

The DEBUG_EVENT structure contains a LOAD_DLL_DEBUG_INFO structure. This structure includes a handle to the newly loaded DLL, the base address of the DLL, and other information that describes the DLL.

Typically, a debugger loads a symbol table associated with the DLL on receipt of this debugging event.

OUTPUT_DEBUG_STRING_EVENT This debugging event is generated when a process being debugged uses the OutputDebugString function.

The DEBUG_EVENT structure contains an OUTPUT_DEBUG_STRING_INFO structure. This structure specifies the address, length, and format of the debugging string.

UNLOAD_DLL_DEBUG_EVENT This debugging event is generated whenever a process being debugged unloads a DLL by using the FreeLibrary function. This debugging event occurs only the last time a DLL is unloaded from a process's address space (that is, when the DLL's usage count is 0).

The DEBUG_EVENT structure contains an UNLOAD_DLL_DEBUG_INFO structure. This structure specifies the base address of the DLL in the address space of the process that unloads the DLL.

Typically, a debugger unloads a symbol table associated with the DLL upon receiving this debugging event.

When a process exits, the kernel automatically unloads the process's DLLs but doesn't generate an UNLOAD_DLL_DEBUG_EVENT debugging event.

RIP_INFO This debugging event is generated only by the Windows 98 checked build and is used to report error conditions such as closing invalid handles.

When the debugger is processing the debug events returned by WaitForDebugEvent, it has full control over the debuggee because the operating system stops all the threads in the debuggee and won't reschedule them until ContinueDebugEvent is called. If the debugger needs to read from or write to the debuggee's address space, it can use ReadProcessMemory and WriteProcessMemory. If the memory is marked as read-only, you can use the VirtualProtect function to reset the protection levels. If the debugger patches the debuggee's code via a call to WriteProcessMemory, it must call FlushInstructionCache to clear out the instruction cache for the memory. If you forget to call FlushInstructionCache, your changes might work, but if the memory you changed is currently in the CPU cache, it might not. Calling FlushInstructionCache is especially important on multiprocessor machines. If the debugger needs to get or set the debuggee's current context or CPU registers, it can call GetThreadContext or SetThreadContext.

The only Win32 debug event that needs special handling is the loader breakpoint. After the operating system sends initial CREATE_PROCESS_DEBUG_EVENT and LOAD_DLL_DEBUG_EVENT notifications for the implicitly loaded modules, the debugger receives an EXCEPTION_DEBUG_EVENT. This debug event is the loader breakpoint. The debuggee executes this breakpoint because the CREATE_PROCESS_DEBUG_EVENT indicates only that the process was loaded, not that it was executed. The loader breakpoint, which the operating system forces each debuggee to execute, is the first time the debugger knows when the debuggee is truly running. In real-world debuggers, the main data structure initialization, such as for symbol tables, is handled during process creation, and the debugger starts showing code disassembly or doing necessary debuggee patching in the loader breakpoint.

When the loader breakpoint occurs, the debugger should record that it saw the breakpoint so that the debugger can handle subsequent breakpoints accordingly. The only other processing needed for the first breakpoint (and for all breakpoints in general) depends on the CPU. For the Intel Pentium family, the debugger has to continue processing by calling ContinueDebugEvent and passing it the DBG_CONTINUE flag so that the debuggee resumes execution.

Listing 4-2 shows MinDBG, a minimal debugger. MinDBG processes all the debug events and properly runs a debuggee process. If you run MinDBG, notice that the debug event handlers don't really display any interesting information, such as the executable or DLL names. Taking a minimal debugger and turning it into a real debugger involves quite a bit of work.

Listing 4-2 MINDBG.CPP

 /*---------------------------------------------------------------------- The world s simplest debugger for Win32 programs ----------------------------------------------------------------------*/ /*////////////////////////////////////////////////////////////////////// The Usual Includes //////////////////////////////////////////////////////////////////////*/ #include "stdafx.h" /*////////////////////////////////////////////////////////////////////// Prototypes //////////////////////////////////////////////////////////////////////*/ // Shows the minimal help. void ShowHelp ( void ) ; // Display functions void DisplayCreateProcessEvent ( CREATE_PROCESS_DEBUG_INFO & stCPDI ) ; void DisplayCreateThreadEvent ( CREATE_THREAD_DEBUG_INFO & stCTDI ) ; void DisplayExitThreadEvent ( EXIT_THREAD_DEBUG_INFO & stETDI ) ; void DisplayExitProcessEvent ( EXIT_PROCESS_DEBUG_INFO & stEPDI ) ; void DisplayDllLoadEvent ( LOAD_DLL_DEBUG_INFO & stLDDI ) ; void DisplayDllUnLoadEvent ( UNLOAD_DLL_DEBUG_INFO & stULDDI ) ; void DisplayODSEvent ( HANDLE hProcess , OUTPUT_DEBUG_STRING_INFO & stODSI ) ; void DisplayExceptionEvent ( EXCEPTION_DEBUG_INFO & stEDI ) ; /*////////////////////////////////////////////////////////////////////// Entry Point! //////////////////////////////////////////////////////////////////////*/ void main ( int argc , char * argv[ ] ) { // Check that there is a command-line argument. if ( 1 == argc ) { ShowHelp ( ) ; return ; } // Concatenate the command-line parameters. TCHAR szCmdLine[ MAX_PATH ] ; szCmdLine[ 0 ] = '\0' ; for ( int i = 1 ; i < argc ; i++ ) { strcat ( szCmdLine , argv[ i ] ) ; if ( i < argc ) { strcat ( szCmdLine , " " ) ; } } // Try to start the debuggee process. The function call looks // like a normal CreateProcess call except for the special start // option flag DEBUG_ONLY_THIS_PROCESS. STARTUPINFO stStartInfo ; PROCESS_INFORMATION stProcessInfo ; memset ( &stStartInfo , NULL , sizeof ( STARTUPINFO ) ) ; memset ( &stProcessInfo , NULL , sizeof ( PROCESS_INFORMATION ) ) ; stStartInfo.cb = sizeof ( STARTUPINFO ) ; BOOL bRet = CreateProcess ( NULL , szCmdLine , NULL , NULL , FALSE , CREATE_NEW_CONSOLE | DEBUG_ONLY_THIS_PROCESS , NULL , NULL , &stStartInfo , &stProcessInfo ) ; // See whether the debuggee process started. if ( FALSE == bRet ) { printf ( "Unable to start %s\n" , szCmdLine ) ; return ; } // The debuggee started, so let s enter the debug loop. DEBUG_EVENT stDE ; BOOL bSeenInitialBP = FALSE ; BOOL bContinue = TRUE ; HANDLE hProcess = INVALID_HANDLE_VALUE ; DWORD dwContinueStatus ; // Loop until told to stop. while ( TRUE == bContinue ) { // Pause until a debug event notification happens. bContinue = WaitForDebugEvent ( &stDE , INFINITE ) ; // Handle the particular debug events. Because MinDBG is only a // minimal debugger, it handles only a few events. switch ( stDE.dwDebugEventCode ) { case CREATE_PROCESS_DEBUG_EVENT : { DisplayCreateProcessEvent ( stDE.u.CreateProcessInfo ) ; // Save the handle information needed for later. hProcess = stDE.u.CreateProcessInfo.hProcess ; dwContinueStatus = DBG_CONTINUE ; } break ; case EXIT_PROCESS_DEBUG_EVENT : { DisplayExitProcessEvent ( stDE.u.ExitProcess ) ; bContinue = FALSE ; dwContinueStatus = DBG_CONTINUE ; } break ; case LOAD_DLL_DEBUG_EVENT : { DisplayDllLoadEvent ( stDE.u.LoadDll ) ; dwContinueStatus = DBG_CONTINUE ; } break ; case UNLOAD_DLL_DEBUG_EVENT : { DisplayDllUnLoadEvent ( stDE.u.UnloadDll ) ; dwContinueStatus = DBG_CONTINUE ; } break ; case CREATE_THREAD_DEBUG_EVENT : { DisplayCreateThreadEvent ( stDE.u.CreateThread ) ; dwContinueStatus = DBG_CONTINUE ; } break ; case EXIT_THREAD_DEBUG_EVENT : { DisplayExitThreadEvent ( stDE.u.ExitThread ) ; dwContinueStatus = DBG_CONTINUE ; } break ; case OUTPUT_DEBUG_STRING_EVENT : { DisplayODSEvent ( hProcess , stDE.u.DebugString ) ; dwContinueStatus = DBG_CONTINUE ; } break ; case RIP_EVENT : { dwContinueStatus = DBG_CONTINUE ; } break ; case EXCEPTION_DEBUG_EVENT : { DisplayExceptionEvent ( stDE.u.Exception ) ; // The only exception that I have to treat specially is // the initial breakpoint the loader provides. switch ( stDE.u.Exception.ExceptionRecord.ExceptionCode ) { case EXCEPTION_BREAKPOINT : { // If a breakpoint exception occurs and it s the // first seen, I continue on my merry way; // otherwise, I pass the exception on to the // debuggee. if ( FALSE == bSeenInitialBP ) { bSeenInitialBP = TRUE ; dwContinueStatus = DBG_CONTINUE ; } else { // Houston, we have a problem! dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED ; } } break ; // Just pass on any other exceptions to the // debuggee. default : { dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED ; } break ; } } break ; // For any other events, just continue on. default : { dwContinueStatus = DBG_CONTINUE ; } break ; } // Pass on to the operating system. ContinueDebugEvent ( stDE.dwProcessId , stDE.dwThreadId , dwContinueStatus ) ; } } /*////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////*/ void ShowHelp ( void ) { printf ( "MinDBG <program to debug> " "<program s command-line options>\n" ) ; } void DisplayCreateProcessEvent ( CREATE_PROCESS_DEBUG_INFO & stCPDI ) { printf ( "Create Process Event :\n" ) ; printf ( " hFile : 0x%08X\n" , stCPDI.hFile ) ; printf ( " hProcess : 0x%08X\n" , stCPDI.hProcess ) ; printf ( " hThread : 0x%08X\n" , stCPDI.hThread ) ; printf ( " lpBaseOfImage : 0x%08X\n" , stCPDI.lpBaseOfImage ) ; printf ( " dwDebugInfoFileOffset : 0x%08X\n" , stCPDI.dwDebugInfoFileOffset ) ; printf ( " nDebugInfoSize : 0x%08X\n" , stCPDI.nDebugInfoSize ) ; printf ( " lpThreadLocalBase : 0x%08X\n" , stCPDI.lpThreadLocalBase ) ; printf ( " lpStartAddress : 0x%08X\n" , stCPDI.lpStartAddress ) ; printf ( " lpImageName : 0x%08X\n" , stCPDI.lpImageName ) ; printf ( " fUnicode : 0x%08X\n" , stCPDI.fUnicode ) ; } void DisplayCreateThreadEvent ( CREATE_THREAD_DEBUG_INFO & stCTDI ) { printf ( "Create Thread Event :\n" ) ; printf ( " hThread : 0x%08X\n" , stCTDI.hThread ) ; printf ( " lpThreadLocalBase : 0x%08X\n" , stCTDI.lpThreadLocalBase ) ; printf ( " lpStartAddress : 0x%08X\n" , stCTDI.lpStartAddress ) ; } void DisplayExitThreadEvent ( EXIT_THREAD_DEBUG_INFO & stETDI ) { printf ( "Exit Thread Event :\n" ) ; printf ( " dwExitCode : 0x%08X\n" , stETDI.dwExitCode ) ; } void DisplayExitProcessEvent ( EXIT_PROCESS_DEBUG_INFO & stEPDI ) { printf ( "Exit Process Event :\n" ) ; printf ( " dwExitCode : 0x%08X\n" , stEPDI.dwExitCode ) ; } void DisplayDllLoadEvent ( LOAD_DLL_DEBUG_INFO & stLDDI ) { printf ( "DLL Load Event :\n" ) ; printf ( " hFile : 0x%08X\n" , stLDDI.hFile ) ; printf ( " lpBaseOfDll : 0x%08X\n" , stLDDI.lpBaseOfDll ) ; printf ( " dwDebugInfoFileOffset : 0x%08X\n" , stLDDI.dwDebugInfoFileOffset ) ; printf ( " nDebugInfoSize : 0x%08X\n" , stLDDI.nDebugInfoSize ) ; printf ( " lpImageName : 0x%08X\n" , stLDDI.lpImageName ) ; printf ( " fUnicode : 0x%08X\n" , stLDDI.fUnicode ) ; } void DisplayDllUnLoadEvent ( UNLOAD_DLL_DEBUG_INFO & stULDDI ) { printf ( "DLL Unload Event :\n" ) ; printf ( " lpBaseOfDll : 0x%08X\n" , stULDDI.lpBaseOfDll ) ; } void DisplayODSEvent ( HANDLE hProcess , OUTPUT_DEBUG_STRING_INFO & stODSI ) { printf ( "OutputDebugString Event :\n" ) ; printf ( " lpDebugStringData : 0x%08X\n" , stODSI.lpDebugStringData ) ; printf ( " fUnicode : 0x%08X\n" , stODSI.fUnicode ) ; printf ( " nDebugStringLength : 0x%08X\n" , stODSI.nDebugStringLength ) ; printf ( " String :\n" ) ; char szBuff[ 512 ] ; if ( stODSI.nDebugStringLength > 512 ) { return ; } DWORD dwRead ; BOOL bRet ; bRet = ReadProcessMemory ( hProcess , stODSI.lpDebugStringData , szBuff , stODSI.nDebugStringLength , &dwRead ) ; printf ( "%s" , szBuff ) ; } void DisplayExceptionEvent ( EXCEPTION_DEBUG_INFO & stEDI ) { printf ( "Exception Event :\n" ) ; printf ( " dwFirstChance : 0x%08X\n" , stEDI.dwFirstChance ) ; printf ( " ExceptionCode : 0x%08X\n" , stEDI.ExceptionRecord.ExceptionCode ) ; printf ( " ExceptionFlags : 0x%08X\n" , stEDI.ExceptionRecord.ExceptionFlags ) ; printf ( " ExceptionRecord : 0x%08X\n" , stEDI.ExceptionRecord.ExceptionRecord ) ; printf ( " ExceptionAddress : 0x%08X\n" , stEDI.ExceptionRecord.ExceptionAddress ) ; printf ( " NumberParameters : 0x%08X\n" , stEDI.ExceptionRecord.NumberParameters ) ; } 



Debugging Applications
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2000
Pages: 122
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net