I'll continue to talk about the way how a debugger works after investing the memory management.
Reading and Writing Memory
Reading from a debuggee's memory is simple. ReadProcessMemory takes care of it for you. A debugger has full access to the debuggee if the debugger started it because the handle to the process returned by the CREATE_PROCESS_DEBUG_EVENT debug event has PROCESS_VM_READ and PROCESS_VM_WRITE access. If the debugger attaches to the process with DebugActiveProcess, OpenProcess must be used to get a handle to the debuggee, and it's needed to specify both read and write access.
Before I can talk about writing to the debuggee's memory, I need to briefly explain an important concept: copy-on-write. When Windows loads an executable file, Windows shares as many mapped memory pages of that binary as possible with the different processes using it. If one of those processes is running under a debugger and one of those pages has a breakpoint written to it, the breakpoint obviously can't be present in all the processes sharing that page. As soon as any process running outside the debugger executed that code, it would crash with a breakpoint exception. To avoid that situation, the operating system sees that the page changed for a particular process and makes a copy of that page that is private to the process that had the breakpoint written to it. Thus, as soon as a process writes to a page, the operating system copies the page.
An interesting detail about the Win32 Debugging API is that the debugger is responsible for getting the string to output when an OUTPUT_DEBUG_STRING_EVENT comes through. The information passed to the debugger includes the location and the length of the string. When it receives this message, the debugger goes and reads the memory out of the debuggee. There're multiple trace statements could easily change an application's behavior when running under a debugger. Because all threads in the application stop when the debug loop is processing an event, calling OutputDebugString in the debuggee means that all your threads stop.Take a look at the following code to see how a debugger (WDBG) handles the OUTPUT_DEBUG_STRING_EVENT. Notice that the DBG_ReadProcessMemory function is the wrapper function around ReadProcessMemory from LOCALASSIST.DLL.
static DWORD OutputDebugStringEvent(CDebugBaseUser * pUserClass ,LPDEBUGGEEINFO pData ,DWORD dwProcessId ,OUTPUT_DEBUG_STRING_INFO & stODDWORD dwThreadId , SI) { TCHAR szBuff[512]; HANDLE hProc = pData->GetProcessHandle(); DWORD dwRead ;// Read the memory.BOOL bRet = DBG_ReadProcessMemory(hProc,
stODSI.lpDebugStringData,szBuff ,sizeof(szBuff),min (stODSI.nDebugStringLength),&dwRead);ASSERT ( TRUE == bRet ) ; if ( TRUE == bRet ) {// Always NULL terminate the string.szBuff [ dwRead + 1 ] = _T ( '\0' ) ;
// Convert CR/LFs if I’m supposed to.pUserClass->ConvertCRLF(szBuff, sizeof(szBuff));// Send the converted string on to the user class.pUserClass->OutputDebugStringEvent(dwProcessId, dwThreadId, szBuff);
}return ( DBG_CONTINUE ) ;}
Most engineers don't realize that debuggers use breakpoints extensively behind the scenes to allow the debugger to control the debuggee. Although you might not directly set any breakpoints, the debugger will set many to allow you to handle tasks such as stepping over a function call. The debugger also uses breakpoints when you choose to run to a specific source file line and stop. Finally, the debugger uses breakpoints to break into the debuggee on command (via the Debug Break menu option in WDBG, for example).
The concept of setting a breakpoint is simple. All you need to do is have a memory address where you want to set a breakpoint, save the opcode (the value) at that location, and write the breakpoint instruction into the address. On the Intel Pentium family, the breakpoint instruction mnemonic is "INT 3" or an opcode of 0xCC, so you need to save only a single byte at the address you're setting the breakpoint. Other CPUs, such as the Intel Merced, have different opcode sizes, so you would need to save more data at the address.
Take a look at the SetBreakPoint function in the following code:
int CPUHELP_DLLINTERFACE __stdcall SetBreakpoint ( PDEBUGPACKET dp, ULONG ulAddr ,OPCODE * pOpCode ) {
DWORD dwReadWrite = 0 ;BYTE bTempOp = BREAK_OPCODE ;BOOL bReadMem ;BOOL bFlush ; BOOL bWriteMem ;DWORD dwOldProtect ;
MEMORY_BASIC_INFORMATION mbi ;ASSERT ( FALSE == IsBadReadPtr ( dp , sizeof ( DEBUGPACKET ) ) ) ;ASSERT ( FALSE == IsBadWritePtr ( pOpCode , sizeof ( OPCODE ) ) ) ;if ( ( TRUE == IsBadReadPtr ( dp , sizeof ( DEBUGPACKET ) ) ) || ( TRUE == IsBadWritePtr ( pOpCode , sizeof ( OPCODE ) ) ) ) {}
TRACE0 ( "SetBreakpoint : invalid parameters\n!" ) ;
return ( FALSE ) ;// If the operating system is Windows 98 and the address is above 2 GB, just leave quietly.if ( ( FALSE == IsNT ( ) ) && ( ulAddr >= 0x80000000 ) ) {return ( FALSE ) ;}// Read the opcode at the location.bReadMem = DBG_ReadProcessMemory ( dp->hProcess, (LPCVOID)ulAddr, &bTempOp, sizeof ( BYTE ) , &dwReadWrite) ;
ASSERT ( FALSE != bReadMem ) ;sizeof ( BYTE ) == dwReadWrite ) ;ASSERT ( if ( ( FALSE == bReadMem) || ( sizeof ( BYTE ) != dwReadWrite ) ) {return ( FALSE ) ;}// Is this new breakpoint about to overwrite an existing breakpoint opcode?if ( BREAK_OPCODE == bTempOp ) {return ( -1 ) ;}// Get the page attributes for the debuggee.DBG_VirtualQueryEx ( dp->hProcess ,(LPCVOID)ulAddr ,&mbi ,
sizeof ( MEMORY_BASIC_INFORMATION ) ) ;// Force the page to copy-on-write in the debuggee.if ( FALSE == DBG_VirtualProtectEx ( dp->hProcess ,mbi.BaseAddress ,mbi.RegionSize ,&mbi.ProtectPAGE_EXECUTE_READWRITE ,) ){ASSERT ( !"VirtualProtectEx failed!!" ) ;return ( FALSE ) ;}// Save the opcode I’m about to whack.*pOpCode = (void*)bTempOp ;bTempOp = BREAK_OPCODE ; dwReadWrite = 0 ; // The opcode was saved, so now set the breakpoint.bWriteMem = DBG_WriteProcessMemory ( dp->hProcess, (LPVOID)ulAddr , (LPVOID)&bTempOp ,
sizeof ( BYTE ) ,&dwReadWrite ) ;ASSERT ( FALSE != bWriteMem ) ;sizeof ( BYTE ) == dwReadWrite ) ;ASSERT (if ( ( FALSE == bWriteMem ) ||( sizeof ( BYTE ) != dwReadWrite ) ){return ( FALSE ) ;}// Change the protection back to what it was before I blasted the// breakpoint in.VERIFY ( DBG_VirtualProtectEx ( dp->hProcess ,mbi.BaseAddress ,mbi.Protect ,mbi.RegionSize ,) ) ;&dwOldProtect// Flush the instruction cache in case this memory was in the CPU// cache.bFlush = DBG_FlushInstructionCache ( dp->hProcess ,(LPCVOID)ulAddr ,sizeof ( BYTE ) ) ;ASSERT ( TRUE == bFlush ) ;return ( TRUE ) ;}
After you set the breakpoint, the CPU will execute it and will tell the debugger that an EXCEPTION_BREAKPOINT (0x80000003) occurred—that's where the fun begins. If it's a regular breakpoint, the debugger will locate and display the breakpoint location to the user. After the user decides to continue execution, the debugger has to do some work to restore the state of the program. Because the breakpoint overwrote a portion of memory, if you, as the debugger writer, were to just let the process continue, you would be executing code out of sequence and the debuggee would probably crash. What you need to do is to move the current instruction pointer back to the breakpoint address and replace the breakpoint with the opcode you saved when you set the breakpoint. After restoring the opcode, you can continue executing.
There's only one small problem: How do you reset the breakpoint so that you can stop at that location again? If the CPU you're working on supports single-step execution, resetting the breakpoint is trivial. In single-step execution, the CPU executes a single instruction and generates another type of exception, EXCEPTION_SINGLE_STEP (0x80000004). Fortunately, all CPUs that Win32 runs on support single-step execution. For the Intel Pentium family, setting single-step execution requires that you set bit 8 on the flags register. The Intel reference manual calls this bit the TF, or Trap Flag. The followingcode shows the SetSingleStep function and the work needed to set the TF. After replacing the breakpoint with the original opcode, the debugger marks its internal state to reflect that it's expecting a single-step exception, sets the CPU into single-step execution, and then continues the process.
BOOL CPUHELP_DLLIMNTERFACE __stdcall SetSingleStep(PDEBUGPACKET dp){BOOL bSetContext ;ASSERT ( FALSE == IsBadReadPtr(dp, sizeof(DEBUGPACKET)));if(TRUE == IsBadReadPtr(dp, sizeof(DEBUGPACKET))){TRACE0("SetSingleStep : invalid parameters\n!");return (FALSE);}// For the i386, just set the TF bit.dp->context.EFlags |= TF_BIT ;bSetContext = DBG_SetThreadContext(dp->hThread, &dp->context ) ;ASSERT(FALSE != bSetContext);
return (bSetContext);}
After the debugger releases the process by calling ContinueDebugEvent, the process immediately generates a single-step exception after the single instruction executes. The debugger checks its internal state to verify that it was expecting a single-step exception. Because the debugger was expecting a single-step exception, it knows that a breakpoint needs to be reset. The single step caused the instruction pointer to move past the original breakpoint location. Due to that, the debugger can set the breakpoint opcode back at the original breakpoint location without any impact to the execution process. The operating system automatically clears the TF each time the EXCEPTION_SINGLE_STEP exception occurs, so there's no need for the debugger to clear it. After setting the breakpoint, the debugger releases the debuggee to continue running.
One more thing when writing a debugger is considered is concerned with thread, especially in multithread environment. But it seems that this entry is long enough, so I decide to leave it in the third part.
Greatz thanks to:
- John Robbins, Debugging Applications, 2000.