Until now, although having more than 5 years working as a programmer, I actually have not understood what the hell the concept "debug" is and how it works. And hope that it's not too late for a investigation about the concept. Greatz thanks Alexander Sandler for ur article here
First of all, actual debugging requires operating system kernel support and here’s
why. Think about it. We’re living in a world where one process reading
memory belonging to another process is a serious security vulnerability.
Yet, when debugging a program, we would like to access a memory that is
part of debugged process’s (debuggie) memory space, from debugger
process. It is a bit of a problem, isn’t it? We could, of course, try
somehow to use same memory space for both debugger and debuggie, but
then what if debuggie itself creates processes. This really complicates
things.
Debugger support has to be part of the operating system kernel.
Kernel able to read and write memory that belongs to each and every
process in the system. Furthermore, as long as process is not running,
kernel can see value of its registers and debugger have to be able to
know values of the debuggie registers. Otherwise it won’t be able to
tell you where the debuggie has stopped (when we pressed CTRL-C in gdb
for instance).
As we spoke about where debugger support starts we already mentioned
several of the features that we need in order to have debugging support
in operating system. We don’t want just any process to be able to debug
other processes. Someone has to monitor debuggers and debuggies. Hence
the debugger has to tell the kernel that it is going to debug certain
process and kernel has to either permit or deny this request. Therefore,
we need an ability to tell the kernel that certain process is a
debugger and it is about to debug other process. Also we need an ability
to query and set values from debuggie’s memory space. And we need an
ability to query and set values of the debuggie’s registers, when it
stops.
And operating system lets us to do all this. Each operating system
does it in it’s manner of course. Windows provides single system call
named CreateProcess() (with the argument dwCreationFlags begins with DEBUG_...) (And ptrace() in Linux) , which allows to do all these operations and much more.
- Types of Windows Debuggers
- User-Mode Debugger
User-mode debuggers are used to debug any application that runs in user mode,
which includes any GUI programs as well as applications you wouldn't expect,
such as Windows 2000 services. Generally, user-mode debuggers use GUIs. The main
hallmark of user-mode debuggers is that they use the Win32 Debugging application
programming interface (API). Because the operating system marks the debuggee as
running in a special mode, you can use the IsDebuggerPresent API function
to find out whether your process is running under a debugger.
The Win32 Debugging API comes with an implied contract: once a process is
running under the Debugging API, thus making it a debuggee, the debugger can't
detach from that process. This symbiotic relationship means that if the debugger
ends, the debuggee will end as well. The debugger is also limited to debugging
only the debuggee and any processes spawned by the debuggee (if the debugger
supports descendant processes).
For interpreted languages and run times that use a virtual machine approach,
the virtual machines themselves provide the complete debugging environment and
don't use the Win32 Debugging API. Some examples of those types of environments
are the Microsoft or Sun Java Virtual Machines (VMs), the Microsoft scripting
environment for Web applications, and the Microsoft Visual Basic p-code
interpreter.
A surprising number of programs use the Win32 Debugging API. These include the Visual C++ debugger, the Windows Debugger (WinDBG), which I discuss in the kernel-mode debugger section that follows; Compuware NuMega's BoundsChecker; the Platform SDK HeapWalker program; the Platform SDK Depends program; the Borland Delphi and C++ Builder debuggers; and the NT Symbolic Debugger (NTSD). I'm sure there are many more.
- Kernel-Mode Debugger
Kernel-mode debuggers sit between the CPU and the operating system. That
means that when you stop in a kernel-mode debugger, the operating system also
stops completely. As you can imagine, bringing the operating system to an abrupt
halt is helpful when you're working on timing and synchronization problems.
Except for one kernel-mode debugger, however, you can't debug user-mode code
with kernel-mode debuggers.
There aren't that many kernel-mode debuggers. A few are the Windows 80386
Debugger (WDEB386), the Kernel Debugger (i386KD), WinDBG, and SoftICE.
- Simple Win32 Debugger
As said already, we can use CreateProcess and pass a special flag in the dwCreationFlags parameter: DEBUG_ONLY_THIS_PROCESS (or DEBUG_PROCESS if can handle multiple processes spawned by the initial debuggee).
Another 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 ( ... ) ; } }
During the time a debugger sits in the debug loopit 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.
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;
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. (When this exception occurs in the first time, just check if the value in ExceptionCode field is EXCEPTION_BREAKPOINT, we can now safety ensure that this exception happens when the debuggee was going to execute its very first instruction). 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.
Here is the code for a small sample debugger from Debugging Applications (John Robbins):
/*---------------------------------------------------------------------- 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" " void\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 ) ; } 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 ) ; }
Greatz thanks to:
- Iczelion for your tutorials at here
- MSDN, Debugging Application Programming Interface, http://msdn.microsoft.com/en-us/library/ms809754.aspx
- John Robbins, Debugging Applications, 2000.