Windows user mode debugger principle Windows operating system provides a set of APIs to support the debugger. These APIs can be divided into three types: APIs for creating debugging targets and APIs for handling debugging events in the debugging cycle. View and modify the API of the debugging target. Next, we will introduce these three APIs separately. Before the debugger works, you must create a debugging target. The user-mode debugger has two methods to create debugging targets: one is to create a new process, and the other is to attach it to a running process. After either of the two methods is used, the process becomes the debugging target. The operating system associates the debugger with the debugging target. The debugging goal of the debugger is to call CreateProcess and pass in the DEBUG_PROCESS flag. For example: [cpp] STARTUPINFO si = {0}; si. cb = sizeof (si); PROCESS_INFORMATION pi = {0}; bool ret = CreateProcesss (NULL, argv [1], NULL, NULL, false, DEBUG_PROCESS, NULL, NULL, & si, & pi); attaching a debugger to a running process is implemented by calling DebugActiveProcess. DebugActiveProcess This function allows you to bind a debugger to a running process. [Cpp] BOOL DebugActiveProcess (DWORD dwProcessId) dwProcessId: process identifier of the process to be bound if the function is successful, a non-zero value is returned; if the process fails, zero is returned no matter which method is used, the interaction between the debugger and the operating system is the same. This type of debugger is called the active debugger (living debuger ). Each debugger can have only one debugging target. Debugging cycle we must have been exposed to message loops when we were new to Windows. The debugging cycle is similar to this. While (when debugging does not end) {// wait for the operating system to send the debugging event. // Handle debugging events. // Notify the debugging target to perform corresponding operations .} When the debugging target is debugged, some operations executed by the process will notify the debugger in Event Mode. For example, the debugger is notified when a dynamic library is loaded and detached, new threads are created and destroyed, and exceptions thrown by code or processors are thrown. When an event needs to notify the debugger, the operating system first suspends all threads of the debugging target, and then notifies the debugger of the event. Wait for the debugger to notify it to continue execution. The debugger calls WaitForDebugEvent to wait for the arrival of Event Notifications. When an Event Notification arrives, the returned event information is encapsulated in the DEBUG_EVENT structure. This structure contains other information such as the event type. There are several types of events: WaitForDebugEvent. This function is used to wait for a debug event to occur in the debugged process. [Cpp] BOOL WaitForDebugEvent, if no debugging event occurs during this period, the function returns the caller. If this parameter is set to INFINITE, the function waits until the debugging event occurs. If the function succeeds, the system returns a non-zero value; if it fails, zero is returned. After the debugger calls WaitForDebugEvent to return the event notification, the system parses the DEBUG_EVENT structure and responds to the event. After processing, the debugger calls ContinueDebugEvent, the debugging target is notified to perform corresponding operations based on the parameters. ContinueDebugEvent function this function allows the debugger to restore threads that were previously suspended due to debugging events. [Cpp] BOOL ContinueDebugEvent (DWORD dwProcessId, DWORD dwThreadId, DWORD dwContinueStatus) dwProcessId is the process identifier of the debugged process. dwThreadId is the thread identifier of the thread to be restored. dwContinueStatus specifies the method in which the thread will continue. It contains two defined values: DBG_CONTINUE and DBG_EXCEPTION_NOT_HANDLED. If the function is successful, A non-zero value is returned. If the operation fails, zero is returned. Specific implementation: [cpp] DWORD Condition = DBG_CONTINUE; while (Condition) {DEBUG_EVENT DebugEvent = {0}; WaitForDebugEvent (& DebugEvent, INFINITE ); // wait for the debug event ProcessEvenet (DebugEvent) // process the debug event. ContinueDebugEvent (DebugEvent. dwProcessId, DebugEvent. dwThreadId, Condition); // notifies the debugging target to continue execution.} ProcessEvent is used to process debugging events. It is a user-defined function. This function parses the DEBUG_EVENT structure. The DEBUG_EVENT structure is: [cpp] typedef struct _ DEBUG_EVENT {DWORD dwDebugEventCode; DWORD dwProcessId; DWORD dwThreadId; union {except Exception; descricreatethread; inclucreateprocessinfo; incluexitthread; incluexitprocess; LOAD_DLL_DEBUG_INFO LoadDll; UNLOAD_DLL_DEBUG_INFO UnloadDll; OUTPUT_DEBUG_STRING_INFO Debu GString; RIP_INFO RipInfo;} u;} DEBUG_EVENT, * LPDEBUG_EVENT; the Code for Processing notifications is as follows: [cpp] DWORD ProcessEvent (DEBUG_EVENT de) {switch (de. dwDebugEvent. code) {case EXCEPTION_DEBUG_EVENT :{} break; case CREATE_THREAD_DEBUG_EVENT :{} break; case CREATE_PROCESS_DEBUG_EVENT :{} break; case when :{} break; case LOAD_DLL_DEBUG_EVENT: {} break; case O UTPUT_DEBUG_STRING_EVENT: {} break ;......} return DBG_CONTINUE;} introduction to debugging events OUTPUT_DEBUG_STRING_EVENT many programmers prefer to output execution results or intermediate steps to check whether the program is correctly executed. This is inconvenient in many systems. However, you can use the debug output command to output some results to the output window. Such as the TRACE macro of vc. In fact, the TRACE macro is implemented by calling OutputDebugString. The debugger displays the output string of the debugging target through the event processing code. There is a DebugString member in the DEBUG_EVENT structure. This structure is defined as: [cpp] typedef struct _ OUTPUT_DEBUG_STRING_INFO {LPSTR lpDebugStringData; WORD fUnicode; WORD nDebugStringLength;} bytes, * bytes; there is a lpDebugStringData Member in this structure, it stores the address of the output string. NDebugStringLength is the string length. FUnicode indicates whether it is an ANSI or UNICODE character. The following code processes the OUTPUT_DEBUG_STRING_EVENT event: [cpp] case OUTPUT_DEBUG_STRING_EVENT: {OUTPUT_DEBUG_STRING_INFO oi = de. u. debugString; WCHAR * msg = ReadRemoteString (debug target handle, oi. lpDebugStringData, oi. nDebugStringLength, oi. fUnicode); std: wcout <msg; break;} ReadRemoteString is a user-defined function. In this function, ReadProcessMemory is called to read strings from the debugging target process. We will not discuss it further. ReadProcessMemory reads data from a region of a specified process. [Cpp] BOOL ReadProcessMemory (HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * HANDLE) hProcess: Process HANDLE lpBassAddress: base address of the region to be read lpBuffer: nSize: number of bytes to read lpNumberOfBytesRead: The address pointer that stores the number of bytes read. If the function is successful, a non-zero value is returned. If the function fails, zero processing of prediction_debug_event is returned. When the debugging target encounters an exception during debugging, the operating system will send the prediction_debug_event event notification to the debugger. When this event occurs, the DEBUG_EVENT structure contains a prediction_debug_info structure. [Cpp] typedef struct _ EXCEPTION_DEBUG_INFO {EXCEPTION_RECORD ExceptionRecord; DWORD dwFirstChance;} prediction_debug_info, * LPEXCEPTION_DEBUG_INFO; The ExceptionRecord member contains a copy of the exception information. Such as the Exception Code, the address caused by the exception, and the exception parameters. Definition: [cpp] typedef struct _ EXCEPTION_RECORD {DWORD ExceptionCode; DWORD ExceptionFlags; struct _ EXCEPTION_RECORD * ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation [encoding];} encoding; dwFirstChance tells the debugger whether the exception is notified in the first round. From the operating system perspective, the debugger must parse the exception and pass DBG_CONTINUE or DBG_EXECPTION_NOT_HANDLED as parameters to ContinueDebugEvent. If DBG_CONTINUE is executed, the operating system considers that the exception has been properly handled. Therefore, the program execution starts from the address where an exception is generated. If DBG_EXCEPTION_NOT_HANDLED is input, the system is notified that the exception is not handled and the operating system continues to distribute the exception. [Cpp] case EXCEPTION_DBUG_EVENT: {std: cout <"Exception Code:" <std: hex <debugEvent. u. exception. predictionrecord. exceptionCode <std: endl; // identify the exception type in the switch and perform the corresponding operation. Switch (debugEvent. u. exception. predictionrecord. predictioncode) {case EXCEPTION_BREAKPOINT: break; case EXCEPTION_SINGLE_STEP: beak; return DBG_CONTINUE;} break;} in the debugging cycle, return from WaitForDebugEvent and call the time between ContinueDebugEvent, the debugging target will not be executed, so its status will remain unchanged. When the debugging target is suspended, the debugger enters the interaction mode, receives various user commands, and performs different operations according to different commands. The sequence of debugging events. When we start the debugging target, the first event received by the debugger is CREATE_THREAD_DEBUG_EVENT. Next is the dll loading event. Each time a file is loaded, such an event is generated. After all modules are loaded into the process address space, the debugging target is ready to run and the debugger is ready to receive notifications. This is the best time to set breakpoints. The debugger will receive the EXIT_DEBUG_PROCESS_EVENT notification before exiting the debugging target. After that, the debugger cannot receive the UNLOAD_DLL_DEBUG_EVENT notification of the dll loaded to the process address space from the process. The debugging events described above are all sent by the Windows operating system to notify the debugger. However, the debugging target will also issue its own exceptions. The debugger can handle these exceptions in the same way as other debugging events. The Windows operating system uses the structured exception handling (SEH) mechanism to pass the exception caused by the processor to the kernel and user State programs. Each SEH exception is uniquely identified by an unsigned integer Exception Code. This exception code is specified by the system when an exception occurs. These error codes use public error codes defined by operating system developers. For example, the access violation exception code is 0xC0000005 and the breakpoint exception is 0xC80000003. To facilitate memory, these exception codes are defined as constants. Its name is like STATUS_XXX. For example, # define STATUS_BREAKPOINT (NTSTATUS) 0x80000003L) is difficult to remember because of the Exception Code. Therefore, the Windows debugger contains aliases that are easier to remember to control the behavior of the debugger. For example, the breakpoint exception 0x80000003 alias is bpe. The alias for the C ++ Exception Code 0xE06D7363 is eh.