Design and Implementation of Win32 debugging Interfaces

Source: Internet
Author: User
Tags apc
The so-called debugger is actually a very broad concept. Any program that can monitor the execution process of other programs in some form can be called a debugger. On Windows, the debugger can be divided into three types based on its implementation principles: kernel-mode debugger, user-mode debugger, and pseudo-code debugger.
The kernel-mode debugger works at the kernel level of the operating system and performs debugging between the hardware and the operating system on the system core or driver. Common examples include SoftICE, windbg, wdeb386, and iw.kd; the user-state debugger uses the debugging interface provided by the operating system to debug user-state programs between the operating system and user-state programs, there are common development environments such as the self-embedded debugger of VC/Delphi and ollydbg. the pseudo-code debugger uses the custom debugging interface of the target system, debug script languages or virtual machine code supported by user State programs, common debugging tools such as JVM/CLR, pcode debugger of VB, and active script debugger.
Because the pseudo-code debugger has a strong correlation with the implementation of specific systems and does not have the versatility at the principle level, this series of articles does not cover its content as much as possible, in the future, if you have the opportunity to discuss the debugging interfaces provided by JVM/CLR/active script, the user-mode debugger is the most widely used and has complete references. I will spend a lot of time exploring it; the core-state debugger is closely integrated with the operating system, and I am not very familiar with it. I can only do my best. Comments and suggestions
In addition, we strongly recommend John Robbins's bugslayer column on msdn and his book <debugging Applications> (Chinese Version of application debugging technology). this book provides a comprehensive explanation of the principle and application of the debugger.

[1] user-mode debugger Structure

The user-mode debugger directly uses the debugging interface provided by Win32 API and follows the Win32 event-driven design philosophy. The implementation idea is very simple. The basic framework pseudocode is as follows:

// Start the process to be debugged or hook the debugger to a running process
CreateProcess (..., debug_process,...) or debugactiveprocess (dwprocessid)

Debug_event de;
Bool bcontinue = true;
DWORD dwcontinuestatus;

While (bcontinue)
{
Bcontinue = waitfordebugevent (& de, infinite );

Switch (De. dwdebugeventcode)
{
...
Default:
{
Dwcontinuestatus = dbg_continue;
Break;
}
}

Continuedebugevent (De. dwprocessid, De. dwthreadid, dwcontinuestatus );
}

When the debugger starts debugging, it starts a new process or attach the program to a running process. At this time, the Win32 system starts the server end of the debugging interface; then, the debugger calls the waitfordebugevent function to wait for the debugging event on the debugging server to be triggered. The debugger handles the event accordingly. Finally, it calls the continuedebugevent function to request the debugging server to continue executing the debugging process, to wait for and process the next debugging event.

First, let's take a rough look at the server-side implementation idea of the debugging interface: the server-side interface of the debugging service actually exists in the debug port of the debugging process ), the implementation of this core object is similar to the completion port of Win32, and is an LPC Port implemented through a core queue. The debugging server is actually connected to the debugging subsystem of Win32 to the debugging process, and the debugging port is constructed within the debugging process. The debugger communicates with the debugging subsystem of Win32 through the debugging port. The debugging subsystem responds to debugging events caused by system operations and distributes debugging events to the core/user debugger through the debugging port.

When creating a new process of the program to be debugged, you must set the debug_only_this_process or debug_process flag in the dwcreationflags parameter of the CreateProcess function to indicate that the new process needs to be debugged. The call path of the CreateProcess function is as follows:

Createprocessa/createprocessw (kernel32.dll)
Createprocessinternalw (kernel32.dll)
Ntcreateprocessex (ntoskrnl. dll)
Pspcreateprocess (ntos/PS/create. C: 969)

Based on the incoming dwcreationflags parameter, the createprocessinternalw function determines whether to construct the port core object for debugging port and set the corresponding debugging flag of peb. pspcreateprocess uses the debugging options and port object handle of the input parameter, select whether to create a debug port for the target process. If you want to create a debug port, convert the incoming port handle to a kernel object reference and save it in the eprocess-> debugport field of the program to be debugged.
The isdebuggerpresent function provided by Win32 API is used to determine whether the current process is debugged by judging the flag set by the createprocessinternalw function in peb. The pseudo code of the isdebuggerpresent function is as follows:

Bool isdebuggerpresent (void)
{
Return ntcurrentteb ()-> processenvironmentblock-> beingdebugged;
}

The structure of Teb and peb can be found at http://www.ntinternals.net.

However, this method is easily spoofed by the debugger to directly modify the peb memory structure. Therefore, another method is to directly check whether the eprocess-> debugport field is used, to determine whether the process is being debugged. In the past, Shui Mu has also discussed the code several times, such as the Code provided in blowfish's "Detection of Debugger method Addendum. In Windows XP/2003, The checkremotedebuggerpresent function provided by Win32 API also uses the same idea. It is implemented by calling the ntqueryinformationprocess function to query the debug port. The pseudocode is as follows:

Bool checkremotedebuggerpresent (handle hprocess, pbool pbdebuggerpresent)
{
Enum process_info_class {processdebugport = 7 };

If (hprocess & pbdebuggerpresent)
{
Handle hport;

* Pbdebuggerpresent = nt_success (ntqueryinformationprocess (hprocess, processdebugport, & hport, sizeof (hport), null ))? True: false;

Return * pbdebuggerpresent;
}
Return false;
}

Unlike the new process that directly creates the program to be debugged, The debugactiveprocess function that debugs the started process first connects to the port of the Win32 system debugging server, then, activate the debug port of the currently running debugging process. The pseudocode of debugactiveprocess is as follows:

Bool debugactiveprocess (DWORD dwprocessid)
{
If (dbguiconnecttodbg ())
{
Handle hprocess = processidtohandle (dwprocessid );

If (hprocess)
{
Dbguidebugactiveprocess (hprocess );
Ntclose (hprocess );
}
}
Return false;
}

Dbguiconnecttodbg function (ntos/dll/dlluistb. c: 27) try to connect to the debugging subsystem port (named "/dbguiapiport") provided by the core. If the connection is successful, a port object will be obtained (saved in dbguiapiport ntcurrentteb () -> dbgssreserved [1]), and a signal lamp handle for debugging status conversion (stored in dbgstatechangesemaphore ntcurrentteb ()-> dbgssreserved [0]) for waiting for debugging events. The pseudocode is as follows:

# Define dbgstatechangesemaphore (ntcurrentteb ()-> dbgssreserved [0])
# Define dbguiapiport (ntcurrentteb ()-> dbgssreserved [1])

Ntstatus dbguiconnecttodbg (void)
{
Ntstatus ST = ntconnectport (& dbguiapiport, l "// dbguiapiport",..., & dbgstatechangesemaphore );

If (nt_success (ST ))
{
Ntregisterthreadterminateport (dbguiapiport );
}
Else
{
Dbguiapiport = NULL;
}
Return st;
}

If the connection to the debug subsystem is successful, call the ntregisterthreadterminateport function (ntos/PS/psdelete. c: 1202) Add the debug port to the end port list (ETHREAD-> terminationportlist) of the current thread control block. Before the thread ends, the port in this list will be activated to give the debugger a chance to clean up.

The dbguidebugactiveprocess function activates the debugging server function of the debugged process. The pseudocode is as follows:

# Define dbguiapiport (ntcurrentteb ()-> dbgssreserved [1])

Void dbguidebugactiveprocess (handle hprocess)
{
Return ntdebugactiveprocess (dbguiapiport )&&
Dbguiissueremotebreakin (hprocess )&&
Dbguistopdebugging (hprocess );
}

The specific implementation of these functions will be explained in detail when the Win32 debugging subsystem is analyzed in later sections.

After the debugging support is started by the debugging process, the debugger calls the waitfordebugevent function to wait for the debugging event to occur. This function is actually a simple packaging of the dbguiwaitstatechange function (ntos/dll/dlluistb. C: 93). It completes the actual function by waiting for the debug event signal obtained by the dbguiconnecttodbg function. If the debugging event is obtained successfully, the debug server will be notified of dbguiwaitstatechangeapi through the ntrequestwaitreplyport function (ntos/LPC/lpcsend. C: 717.

After the debugging event is handled, the debugger calls the continuedebugevent function, which is a simple package of the dbguicontinue function. It also uses the ntrequestwaitreplyport function to notify the debugging server of the dbguicontinueapi message.

After debugging, WINXP/2003 also provides the debugactiveprocessstop function to stop debugging. The pseudocode is as follows:

Bool debugactiveprocessstop (DWORD dwprocessid)
{
Handle hprocess = processidtohandle (dwprocessid );

If (hprocess)
{
Closeallprocesshandles (dwprocessid );
Dbguistopdebugging (hprocess );
If (ntclose (hprocess ))
Return true;
}
Return false;
}

Dbguistopdebugging((ntdll.dll.pdf call zwremoveprocessdebug.exe (ntoskrnl.exe) to close the debugging port of the specified process. The incoming port handle and Process Handle are used to call system service 0xc7 to complete the final function. This will not be discussed in depth for the moment.

After understanding this, we should have a framework for implementing the user-mode Debugger: its structure is an event-based model, then, request the debug event from the debug subsystem and complete the specific operation.

[2] debugging events

As mentioned above, the user-state debugger under Win32 is actually a while loop. The loop body waits for a debugging event, processes it, and finally returns the control to the debugging server, it is like a window message loop. The core of debugging events is actually a debug_event structure, which is defined in WINBASE. h as follows:

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, * lpdebug_event;

The dwdebugeventcode field shows the type of the debugging event. The dwprocessid and dwthreadid fields respectively indicate the process and thread ID of the debugging event.

Debugging events generally include the following types:

# Define exception_debug_event 1
# Define create_thread_debug_event 2
# Define create_process_debug_event 3
# Define exit_thread_debug_event 4
# Define exit_process_debug_event 5
# Define load_dll_debug_event 6
# Define unload_dll_debug_event 7
# Define output_debug_string_event 8
# Define rip_event 9

The create_process_debug_event event is triggered when the first thread of a new process is created. The exit_process_debug_event event is triggered when the last thread of the debugged process ends; create_thread_debug_event/exit_thread_debug_event will be triggered each time a thread is created/exited. The load_dll_debug_event/unload_dll_debug_event event will be triggered each time a DLL is loaded/detached; when the debugged program uses the outputdebugstring function to output a debug string, the debugger receives an output_debug_string_event event. When an exception is thrown, the debugger receives the prediction_debug_event event as soon as possible. If the debugger does not handle this exception, the normal seh call chain of the program to be debugged is entered. If the process to be debugged is not processed, this event will be triggered again; Rip_event is generally used to report error events.
Generally, program debugging events are triggered in the following order:

Create_process_debug_event

Load_dll_debug_event x N // DLL loaded statically

Create_thread_debug_event & exit_thread_debug_event // a pair appears in A multithreaded Program

Load_dll_debug_event & unload_dll_debug_event // a pair is generated when the DLL is dynamically loaded.

Prediction_debug_event x N // random appearance

Output_debug_string_event x N // when the program writes debugging information

Exit_process_debug_event

Next, we will analyze in detail the causes and timing of each debugging event. The content of the specific debugging event is not too long here. If you are interested in writing a debugger, refer to the relevant content in msdn and <debugging Applications>.

First, create the create_process_debug_event event of the process and the create_thread_debug_event event of the thread. Both events are caused by the dbgkcreatethread function (ntos/dbgk/dbgkproc. h: 211. This function first checks whether the current thread is an active thread with a debug port, and then checks whether the current thread is the first thread created by the process. If it is not the first thread, or the debugger is attach) to an active process (judging whether the process has occupied the user-state CPU time), the create_thread_debug_event event is triggered to the debugging server of the debugging subsystem; otherwise, the create_process_debug_event event is reported.

The pseudo code of the dbgkcreatethread function is as follows:

Void dbgkcreatethread (pvoid startaddress)
{
If (! Psgetcurrentprocess ()-> debugport | psgetcurrentthread ()-> deadthread)
{
Return;
}

Pslockprocess (process, kernelmode, pslockwaitforever); // lock all threads in the process

If (psgetcurrentprocess ()-> PCB. usertime &&
Psgetcurrentprocess ()-> createprocessreported = false)
{
Psgetcurrentprocess ()-> createprocessreported = true;

// Trigger the create_process_debug_event event
}
Else
{
// Trigger the create_thread_debug_event event
}

Psunlockprocess (psgetcurrentprocess ());
}

The general process of Win32 when creating a user-state thread is as follows:

Createthread (kernel32.dll)
Createremotethread (kernel32.dll)
Ntcreatethread (ntoskrnl.exe)
Pspcreatethread (ntos/PS/create. C: 237)

The pspcreatethread function uses the pspuserthreadstartup function (ntos/PS/create. C: 1639) as the thread entry function when creating a user-state thread. Therefore, the thread directly enters this function after being created. The pspuserthreadstartup function initializes the APC of non-dead and non-terminated threads. Then, the dbgkcreatethread function is called to notify the debugger to take corresponding actions. Finally, the user-state CPU time of the process is set to 1, to indicate that the process has been started. For a special thread, if the thread is not dead but has stopped at startup, dbgkcreatethread is called directly and then pspexitthread is called immediately to notify the debugger to take corresponding actions. The pseudo code of the pspuserthreadstartup function is as follows:

Void pspuserthreadstartup (in pkstart_routine startroutine, in pvoid startcontext)
{
If (! Psgetcurrentthread ()-> deadthread &&! Psgetcurrentthread ()-> hasterminated)
{
// Initialize the APC thread
}
Else
{
If (! Psgetcurrentthread ()-> deadthread)
{
Dbgkcreatethread (startcontext );
}
Pspexitthread (status_thread_is_terminating );
}

Dbgkcreatethread (startcontext );

If (psgetcurrentprocess ()-> PCB. usertime = 0)
{
Psgetcurrentprocess ()-> PCB. usertime = 1;
}
}

Corresponding to the dbgkcreatethread function is the dbgkexitthread function (ntos/dbgk/dbgkproc. c: 384) and dbgkexitprocess functions (ntos/dbgk/dbgkproc. c: 439), which triggers the exit_thread_debug_event and exit_process_debug_event events to the debugging server respectively.
These two functions are called at an appropriate time by the pspexitthread function (ntos/PS/psdelete. C: 622) That exits the thread from the system kernel. The pspexitthread function checks whether the PCB thread list of the current process has only one thread. If there are no other threads, call the dbgkexitprocess function; otherwise, call the dbgkexitthread function.

In the Win32 system, the actual function call process is as follows:

Loadlibrary (kernel32.dll)
LoadLibraryEx (kernel32.dll)
Baseploadlibraryasdatafile (kernel32.dll)
Ntmapviewofsection (ntos/MM/mapview. C: 204)
Mmmapviewofsection (ntos/MM/mapview. C: 699)

The ntmapviewofsection function calls the mmmapviewofsection function (ntos/MM/mapview. c: 699) after the actual memory file ing is completed, the system checks whether to call the dbgkmapviewofsection function (ntos/dbgk/dbgkproc. c: 495), notifies the debugging server that a new image file is loaded. And the corresponding mmunmapviewofsection function (ntos/MM/umapview. c: 88) after determining whether the flag and target processes are the current processes, call the dbgkunmapviewofsection function (ntos/dbgk/dbgkproc. c: 567) notifies the debugging server that an image file has been uninstalled.

Unlike the previous events, the outputdebugstring function (kernel32.dll) is actually implemented through exceptions. It is also interesting that this function is one of the few examples of converting the Unicode version with the W suffix and calling the ANSI version with the suffix to complete the actual function. The outputdebugstringa function (kernel32.dll) actually uses the raiseexception function to cause a software exception with the Exception Code 0x40010006 and transmits the pointer and length of the string as an exception parameter.

Dbgkforwardexception function (ntos/dbgk/dbgkport. c: 96) as the function that actually triggers the exception_debug_event debugging event, the kidispatchexception function (ntos/ke/i386/effectn) is distributed in the system exception. c: 797. The kidispatchexception function completes core and user State Exception Handling Based on the status when an exception is thrown.

For core State exceptions, the core debugging program is first given a processing opportunity, and then tried to distribute it to the frame-based seh exception chain. if not handled, the core debugging program is given another opportunity, if it is still not processed, you can only call the kebugcheckex function (ntos/ke/bugcheck. c: 157) blue screen, huh, huh.
For user State exceptions, the core debugger should first try to process them. If not, the dbgkforwardexception function can be called for distribution. If not, the system will try multiple times. If not, stop the thread and report an exception to the user. The pseudo code of the kidispatchexception function is as follows:

Void kidispatchexception (in pexception_record exceptionrecord, in pkexception_frame exceptionframe,
In pktrap_frame trapframe, in kprocessor_mode previusmode, in Boolean firstchance)
{
Context contextframe;

Kecontextfromkframes (trapframe, exceptionframe, & contextframe); // construct Context)

If (exceptionrecord-> exceptioncode = status_breakpoint) // process the debug breakpoint INT 3
{
Contextframe. EIP --;
}

If (previusmode = kernelmode)
{
If (firstchance = true)
{
If (kidebugroutine & kidebugroutine (..., false )! = False) goto handle1

If (rtldispatchexception (exceptionrecord, & contextframe) = true) goto handled1;
}

If (kidebugroutine & kidebugroutine (..., true )! = False) goto handle1

Kebugcheckex (...); // core error, crash in a controllable way-_-B to put it bluntly: deadth blue screen, huh, huh
}
Else // previousmode = usermode
{
If (firstchance = true)
{
If (kidebugroutine & kidebugroutine (..., false )! = False) goto handle1

If (dbgkforwardexception (exceptionrecord, true, false) goto handled2;

// Convert the exception information to user mode and try to distribute it
}

If (dbgkforwardexception (exceptionrecord, true, true ))
{
Goto handled2;
}
Else if (dbgkforwardexception (predictionrecord, false, true ))
{
Goto handled2;
}
Else
{
Zwterminatethread (ntcurrentthread (), exceptionrecord-> exceptioncode );
Kebugcheckex (...);
}
}

Handled1:
Kecontexttokframes (trapframe, predictionframe, & contextframe,
Contextframe. contextflags, previusmode );

Handled2:
}

The dbgkforwardexception function is called for three combinations of debugexception and secondchance parameters. If debugexception is set to true, messages are sent to the debug port; otherwise, messages are sent to the exception port.

So far, we have a general understanding of the triggering mechanisms of several common debugging events, the next section describes how to implement the debugging subsystem in Win32 that associates these debugging events with the end-user debugger.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.