Home > Sample chapters

How Windows Debuggers Work

Page 1 of 6 Next >
This chapter from Inside Windows Debugging explains several debugging types and mechanisms in Windows and how they work at the system level.

In this chapter

  • User-Mode Debugging

  • Kernel-Mode Debugging

  • Managed-Code Debugging

  • Script Debugging

  • Remote Debugging

  • Summary

This chapter explains how different types of debuggers work in Microsoft Windows. If you know these architectural foundations, many debugger concepts and behaviors suddenly start making sense. For example, this chapter explains why certain debugger commands and features work only in user-mode or kernel-mode debugging. You’ll also dive into the architecture of managed-code debugging and discover why .NET source-level debugging isn’t currently supported by the Windows debuggers.

Following that discussion, you’ll learn how the architecture of script debugging relates to that of .NET debugging. With HTML5 and JavaScript becoming more prevalent development technologies for rich client user interfaces, script debugging is likely to garner even more attention from Windows developers in the future. This chapter concludes with a section that explains remote debugging and the key concepts that drive its architecture.

User-Mode Debugging

A user-mode debugger gives you the ability to inspect the memory of the target program you’re trying to debug, as well as the ability to control its execution. In particular, you want to be able to set breakpoints and step through the target code, one instruction or one source line at a time. These basic requirements drive the design of the native user-mode debugging architecture in Windows.

Architecture Overview

To support controlling the target in user-mode debugging, the Windows operating system (OS) has an architecture based on the following principles:

  • When important debug events, such as new module loads and exceptions, occur in the context of a process that’s being debugged by a user-mode debugger, the OS generates message notifications on behalf of the target and sends them to the debugger process, giving the debugger program a chance to either handle or ignore those notifications. During each notification, the target process blocks and waits until the debugger is done responding to it before it resumes its execution.

  • For this architecture to work, the native debugger process must also implement its end of the handshake, so to speak, and have a dedicated thread to receive and respond to the debug events generated by the target process.

  • The interprocess communication between the two user-mode programs is based on a debug port kernel object (owned by the target process), where the target queues up its debug event notifications and waits on the debugger to process them.

This generic interprocess communication model is sufficient to handle all the requirements for controlling the target in a user-mode debugging session, providing the debugger with the capability to respond to code breakpoints or single-step events, as illustrated in Figure 3-1.

Figure 3-1

Figure 3-1 Native user-mode debugging architecture in Windows.

The other high-level requirement of user-mode debugging is for the debugger to be able to inspect and modify the virtual address space of the target process. This is necessary, for example, to be able to insert code breakpoints or walk the stacks and list the call frames in the threads of execution contained within the target process.

Windows provides facilities exposed at the Win32 API layer to satisfy these requirements, allowing any user-mode process to read and write to the memory of another process—as long as it has sufficient privileges to do so. This system-brokered access is why you can debug only your own processes unless you’re an administrator running in an elevated User Acount Control (UAC) context with full administrative privileges (which include, in particular, the special SeDebugPrivilege). If you do have those privileges, you can debug processes from any other user on the system—including LocalSystem processes.

Win32 Debugging APIs

Debugger programs can implement their functionality and follow the conceptual model described in the previous section by using published APIs in the operating system. Table 3-1 summarizes the main Win32 functions used in Windows user-mode debuggers to achieve their requirements.

Table 3-1 Win32 API Support for User-Mode Windows Debuggers


Win32 API Function

WinDbg Command(s)

Start a target process directly under the control of a user-mode debugger.

CreateProcess, with dwCreationFlags:



Ctrl+E UI shortcut or windbg.exe target.exe

Dynamically attach a user-mode debugger to an existing process.

OpenProcess, with at least the following dwDesiredAccess flags:




DebugActiveProcess, with the handle obtained in the previous step

F6 UI shortcut or windbg.exe –pn target.exe or windbg.exe –p [PID]

Stop debugging the target process, but without terminating it.


qd (“quit and detach”)

Break into the debugger to inspect the target.


Ctrl+Break UI shortcut or Debug\Break menu action

Wait for new debug events.



Continue the target’s execution after a received debug event is processed.



Inspect and edit the virtual address space of the target process.



Dump memory (dd, db, and so on) Edit memory (ed, eb, and so on)

Insert code breakpoints (bp) Dump a thread’s stack trace (k, kP, kn, and so on)

With these Win32 APIs, a user-mode debugger can write the code in the thread that it uses to process the debug events it receives from the target using a loop like the one shown in the following listing (pseudo-code).

// Main User-Mode Debugger Loop //
CreateProcess("target.exe",..., DEBUG_PROCESS, ...);
while (1)
    WaitForDebugEvent(&event, ...);
    switch (event)
        case ModuleLoad:
        case TerminateProcess:
        case Exception (code breakpoint, single step, etc...):

When the debugger loop calls the WaitForDebugEvent Win32 API to check whether a new debug event arrived, the call internally transitions to kernel mode to fetch the event from the queue of the debug port object of the target process (the DebugPort field of the nt!_EPROCESS executive object). If the queue is found to be empty, the call blocks and waits for a new debug event to be posted to the port object. After the event is processed by the debugger, the ContinueDebugEvent Win32 API is called to let the target process continue its execution.

Debug Events and Exceptions

The OS generates several types of debug events when a process is being debugged. For instance, an event is generated for every module load, allowing the user-mode debugger to know when a new DLL is mapped into the address space of the target process. Similarly, an event is also raised when a new child process is created by the target process, enabling the user-mode debugging session to also handle the debug events from child processes if it wants to do so.

Debug events are similarly generated when any exceptions are raised in the context of the target process. As you’ll shortly see, code breakpoints and single-stepping are both internally implemented by forcing an exception to be raised in the context of the target application, which means that those events can also be handled by the user-mode debugger just like any other debug events. To better understand this type of debug event, a quick overview of exception handling in Windows is in order.

.NET, C++, and SEH Exceptions

Two categories of exceptions exist in Windows: language-level or framework-level exceptions, such as the C++ or .NET exceptions, and OS-level exceptions, also known as Structured Exception Handling (SEH) exceptions. Both Microsoft Visual C++ and the .NET Common Language Runtime (CLR) use SEH exceptions internally to implement support for their specific application-level, exception-handling mechanisms.

SEH exceptions, in turn, can be separated into two categories: hardware exceptions are raised in response to a processor interrupt (invalid memory access, integer divide-by-zero, and so on), and software exceptions are triggered by an explicit call to the RaiseException Win32 API. Hardware exceptions are particularly important to the functionality of user-mode debuggers in Windows because they’re also used to implement breakpoints and in single-stepping the target, two fundamental features of any debugger.

At the Visual C/C++ language level, the throw keyword used to throw C++ exceptions is translated by the compiler into a call implemented in the C runtime library, which ultimately invokes the RaiseException API. In addition, three keywords (__try, __except, and __finally) are also defined to allow you to take advantage of SEH exceptions and structure (hence the SEH name) your code so that you can establish code blocks to handle or ignore the SEH exceptions that get raised from within that code block. Despite this Visual C++ language support, it’s important to realize that SEH is a Windows operating system concept and that you can use it with any language, as long as the compiler has support for it.

Unlike C++ exceptions, which can be raised with any type,SEH exceptions deal with only one type: unsigned int. Each SEH exception, whether triggered in hardware or software, is identified in Windows using an integer identifier—the exception code—that indicates the type of fault that triggered the exception (divide-by-zero, access violation, and so on). You can find many of the exception codes defined by the OS in the winnt.h Software Development Kit (SDK) header file. In addition, applications are also free to define their own custom exception codes, which is precisely what the C++ and .NET runtimes do for their exceptions.

Table 3-2 lists a few common exception codes that you’ll see frequently during your debugging investigations.

Table 3-2 Common Windows SEH Exceptions and Their Status Codes

Exception Code



Invalid memory access


Arithmetic divide-by-zero operation


Arithmetic integer overflow


Stack overflow (running out of stack space)


Raised in response to the debug break CPU interrupt (interrupt #3 on x86 and x64)


Raised in response to the single-step CPU interrupt (interrupt #1 on x86 and x64)

SEH Exception Handling in the User-Mode Debugger

When an exception occurs in a process that’s being debugged, the user-mode debugger gets notified by the OS exception dispatching code in ntdll.dll before any user exception handlers defined in the target process are given a chance to respond to the exception. If the debugger chooses not to handle this first-chance exception notification, the exception dispatching sequence proceeds further and the target thread is then given a chance to handle the exception if it wants to do so. If the SEH exception is not handled by the thread in the target process, the debugger is then sent another debug event, called a second-chance notification, to inform it that an unhandled exception occurred in the target process.

Figure 3-2 summarizes this OS exception dispatching sequence, specifically when a user-mode debugger is connected to the target process.

Figure 3-2

Figure 3-2 SEH exceptions and debug event notifications.

First-chance notifications are a good place for the user-mode debugger to handle exceptions that should be invisible to the code in the target process, including code breakpoints, single-step debug events, and the break-in signal. The sections that follow describe these important mechanisms in more detail.

Unlike first-chance notifications, which for user exceptions are simply logged to the debugger command window by default, the user-mode debugger always stops the target in response to a second-chance exception notification. Unhandled exceptions are always reason for concern because they lead to the demise of the target process when no debuggers are attached, which is why the user-mode debugger breaks in when they occur so that you can investigate them. You can see this sequence in action using the following program from the companion source code, which simply throws a C++ exception with a string type. For more details on how to compile the companion source code, refer to the procedure described in the Introduction of this book.

// C:\book\code\chapter_03\BasicException>main.cpp
int __cdecl wmain()                               
 throw "This program raised an error";            
 return 0;                                         

When you run this program under the WinDbg user-mode debugger, you see the debugger receive two notifications from the target: the first-chance notification is logged to the debugger command window, while the second-chance notification causes the debugger to break in, as illustrated in the following debugger listing. Notice also how the throw keyword used to raise C++ exceptions ends up getting translated into a call to the C runtime library (the msvcrt!_CxxThrowException function call in the following listing), which ultimately invokes the RaiseException Win32 API to raise an SEH exception with the custom C++ exception code.

0:000> vercommand
command line: '"c:\Program Files\Debugging Tools for Windows (x86)\windbg.exe"
0:000> .symfix
0:000> .reload
0:000> g
(aa8.1fc0): C++ EH exception - code e06d7363 (first chance)
(aa8.1fc0): C++ EH exception - code e06d7363 (!!! second chance !!!)
75dad36f c9 leave 0:000> k
ChildEBP RetAddr
000ffb60 75fd359c KERNELBASE!RaiseException+0x58
000ffb98 00cb1204 msvcrt!_CxxThrowException+0x48
000ffbac 00cb136d BasicException!wmain+0x1b
[c:\book\code\chapter_03\basicexception\main.cpp @ 7]
000ffbf0 76f9ed6c BasicException!__wmainCRTStartup+0x102
000ffbfc 779c377b kernel32!BaseThreadInitThunk+0xe
000ffc3c 779c374e ntdll!__RtlUserThreadStart+0x70
000ffc54 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> $ Quit the debugging session 
0:000> q

The Break-in Sequence

User-mode debuggers can intervene at any point in time and freeze the execution of their target process so that it can be inspected by the user—an operation referred to as breaking into the debugger. This is achieved by using the DebugBreakProcess API, which internally injects a remote thread into the address space of the target process. This “break-in” thread executes a debug break CPU interrupt instruction (int 3). In response to this interrupt, an SEH exception is raised by the OS in the context of the break-in thread. As shown in the previous section, this sends the user-mode debugger process a first-chance notification, allowing it to handle this special debug break exception (code 0x80000003, or STATUS_BREAKPOINT) and finally break in by suspending all the threads in the target process.

This is why the current thread context in the user-mode debugger after a break-in operation will be in this special thread, which isn’t a thread you’ll recognize as “yours” if you’re debugging your own target process. To see this break-in thread in action, start a new instance of notepad.exe under the WinDbg user-mode debugger, as shown in the following listing. If you’re running this experiment on 64-bit Windows, you can execute it again exactly as shown here by using the 32-bit version of notepad.exe located under the %windir%\SysWow64 directory on x64 Windows.

0:000> vercommand
command line: '"c:\Program Files\Debugging Tools for Windows (x86)\windbg.exe" notepad.exe'
0:000> .symfix
0:000> .reload
Reloading current modules..........................
0:000> ~
  0 Id: 1d90.1678 Suspend: 1 Teb: 7ffde000 Unfrozen
  0:000> g

Using the Debug\Break menu action, break back into the debugger. You’ll see that the current thread context is no longer thread #0 (the main UI thread in notepad.exe) but rather a new thread. As you can infer from the function name (ntdll!DbgUiRemoteBreakin) on the call stack that you obtain by using the k command, this is the remote thread that was injected by the debugger into the target address space in response to the break-in request.

(1938.1fb0): Break instruction exception - code 80000003 (first chance)
7799410c cc int 3 
0:001> ~
 0 Id: 1d90.1678 Suspend: 1 Teb: 7ffde000 Unfrozen 
. 1 Id: 1d90.17f0 Suspend: 1 Teb: 7ffdd000 Unfrozen
0:001> k
ChildEBP RetAddr
00a4fecc 779ef161 ntdll!DbgBreakPoint
00a4fefc 75e9ed6c ntdll!DbgUiRemoteBreakin+0x3c
00a4ff08 779b37f5 kernel32!BaseThreadInitThunk+0xe
00a4ff48 779b37c8 ntdll!__RtlUserThreadStart+0x70
00a4ff60 00000000 ntdll!_RtlUserThreadStart+0x1b

In addition, using the uf debugger command to disassemble the current function shows that this thread was executing an int 3 CPU interrupt instruction just before the debugger got sent the first-chance notification for the debug break exception.

0:001> uf .
7799410c cc int 3 
7799410d c3 ret

To see the actual threads in the target process, you can use the ~*k command to list the call stacks for every thread in the target. You can also use the s command to change (“switch”) the current thread context in the debugger to one of those threads, as illustrated in the following listing.

0:001> $ Switch over to thread #0 in the target 0:001> ~0s
0:000> k
ChildEBP RetAddr 
0019f8e8 760fcde0 ntdll!KiFastSystemCallRet 
0019f8ec 760fce13 USER32!NtUserGetMessage+0xc 
0019f908 0085148a USER32!GetMessageW+0x33 
0019f948 008516ec notepad!WinMain+0xe6 
0019f9d8 76f9ed6c notepad!_initterm_e+0x1a1 
0019f9e4 779c377b kernel32!BaseThreadInitThunk+0xe 
0019fa24 779c374e ntdll!__RtlUserThreadStart+0x70 
0019fa3c 00000000 ntdll!_RtlUserThreadStart+0x1b 
0:001> $ Terminate this debugging session...
0:001> q

Setting Code Breakpoints

Code breakpoints are also implemented using the int 3 instruction. Unlike the break-in case, where the debug break instruction is executed in the context of the remote break-in thread, code breakpoints are implemented by directly overwriting the target memory location where the code breakpoint was requested by the user.

The debugger program keeps track of the initial instructions for each code breakpoint so that it can substitute them in place of the debug break instruction when the breakpoints are hit, and before the user is able to inspect the target inside the debugger. This way, the fact that int 3 instructions are inserted into the target process to implement code breakpoints is completely hidden from the user debugging the program, as it should be.

This scheme sounds straightforward, but there is a catch: how is the debugger able to insert the int 3 instruction before the execution of the target process is resumed (using the g command) after a breakpoint hit? Surely, the debugger can’t simply insert the debug break instruction before the target’s execution is resumed because the next instruction to execute is supposed to be the original one from the target and not the int 3 instruction. The way the debugger solves this dilemma is the same way it is able to support single-stepping, which is by using the TF (“trap flag”) bit of the EFLAGS register on x86 and x64 processors to force the target thread to execute one instruction at a time. This single-step flag causes the CPU to issue an interrupt (int 1) after every instruction it executes. This allows the thread of the breakpoint to execute the original target instruction before the debugger is immediately given a chance to handle the new single-step SEH exception—which it does by restoring the debug break instruction again, as well as by resetting the TF flag so that the CPU single-step mode is disabled again.

Observing Code Breakpoint Insertion in WinDbg

To conclude this section, you’ll try a fun experiment in which you’ll debug the user-mode WinDbg debugger! Armed with the background information from this section and the familiarity with using WinDbg commands that you’ve gained so far, you have all the tools to confirm what WinDbg does when a new code breakpoint is added by the user without having to take my word for it.

To start this experiment, run notepad.exe under windbg.exe. This experiment once again uses the x86 flavor of notepad.exe and windbg.exe, but the concepts are identical on x64 Windows.

0:000> vercommand
command line: '"c:\Program Files\Debugging Tools for Windows (x86)\windbg.exe" notepad.exe'
0:000> .symfix
0:000> .reload

Set a breakpoint at USER32!GetMessageW, which is a function that you know is going to be hit in response to any user interaction with the notepad.exe user interface. Figure 3-3 represents this first debugging session.

0:000> bp user32!GetMessageW
Figure 3-3

Figure 3-3 First WinDbg debugger instance.

Before you use the g command to let the target notepad.exe continue its execution, start a new windbg.exe debugger instance with the same security context as the first one. From this new instance, attach to the first windbg.exe process using the F6 shortcut, as illustrated in Figure 3-4. This allows you to follow what happens after you unblock the execution of the notepad.exe process from the first debugger instance.

Figure 3-4

Figure 3-4 Debugging the debugger: the second WinDbg debugger instance.

In this new WinDbg instance, set a breakpoint at the kernel32!WriteProcessMemory API. As mentioned earlier in this chapter, this is the Win32 API used by user-mode debuggers to edit the virtual memory of their target processes.

0:002> $ Second WinDbg Session 0:002> .symfix
0:002> .reload
0:002> x kernel32!*writeprocessmemory*
75e51928 kernel32!_imp__WriteProcessMemory = <no type information>
75e520e3 kernel32!WriteProcessMemory = <no type information>
75eb959f kernel32!WriteProcessMemoryStub = <no type information>
0:002> bp kernel32!WriteProcessMemory
0:002> g

Now that you have this breakpoint in place, go back to the first windbg.exe instance and run the g command to let notepad.exe continue its execution.

0:000> $ First WinDbg Session 0:000> g

Notice that you immediately get a breakpoint hit inside the second windbg.exe instance, which is consistent with what you already learned in this chapter, because the first debugger tries to insert an int 3 instruction into the notepad.exe process address space (corresponding to the USER32!GetMessageW breakpoint you added earlier).

Breakpoint 0 hit 
0:001> $ Second WinDbg Session 
0:001> k
ChildEBP RetAddr 
0092edf4 58b84448 kernel32!WriteProcessMemory
0092ee2c 58adb384 dbgeng!BaseX86MachineInfo::InsertBreakpointInstruction+0x128
 0092ee7c 58ad38ee dbgeng!LiveUserTargetInfo::InsertCodeBreakpoint+0x64
0092eeb8 58ad62f7 dbgeng!CodeBreakpoint::Insert+0xae 
0092f764 58b67719 dbgeng!InsertBreakpoints+0x8c7 
0092f7e8 58b66678 dbgeng!PrepareForExecution+0x5d9 
0092f7fc 58afa539 dbgeng!PrepareForWait+0x28 
0092f840 58afaa60 dbgeng!RawWaitForEvent+0x19 
0092f858 00ebb6cf dbgeng!DebugClient::WaitForEvent+0xb0 
0092f874 75e9ed6c windbg!EngineLoop+0x13f 
0092f880 779b37f5 kernel32!BaseThreadInitThunk+0xe 
0092f8c0 779b37c8 ntdll!__RtlUserThreadStart+0x70 
0092f8d8 00000000 ntdll!_RtlUserThreadStart+0x1b

You can also check the arguments to the WriteProcessMemory API in the previous call stack by applying the technique described in Chapter 2, where the stack pointer and saved frame pointer values were used to get the arguments to each call from the stack of the current thread. Remember that in the __stdcall calling convention, the stack pointer register value points to the return address at the time of the breakpoint, followed by the arguments to the function call. This means that the second DWORD value in the following listing represents the first parameter to the Win32 API call. The values you’ll see will be different, but you can apply the same steps described here to derive the function arguments to this API call:

0:001> $ Second WinDbg Session 
0:001> dd esp
0092edd4 58ce14a2 00000120 7630cde8 58d1b5d8
0092ede4 00000001 0092edf0 00000000 0092ee2c 

In the documentation for the WriteProcessMemory Win32 API on the MSDN website at http://msdn.microsoft.com/, you’ll see that it takes five parameters.

BOOL WINAPI WriteProcessMemory(
 __in HANDLE hProcess,
    __in LPVOID lpBaseAddress,
    __in_bcount(nSize) LPCVOID lpBuffer,
    __in SIZE_T nSize,
    __out_opt SIZE_T * lpNumberOfBytesWritten     );

The first of these parameters is the user-mode handle for the target process object (hProcess) that the debugger is trying to write to. You can use the value you obtained from the dd command with the !handle debugger extension command to confirm that it was indeed the notepad.exe process. The !handle command also gives you the process ID (PID) referenced by the handle, which you can confirm is the PID of notepad.exe in the Windows task manager UI or, alternatively, by using the convenient .tlist debugger command, as demonstrated in the following listing.

0:001> $ Second WinDbg Session 
0:001> !handle 120 f
Handle 120 
  Type           Process 
  GrantedAccess  0x12167b:
  Object Specific Information  Process Id 5964
    Parent Process 3736 
 0:001> .tlist notepad*
   0n5964 notepad.exe

Next is the address that the debugger is trying to overwrite (lpBaseAddress). Using the u debugger command to disassemble the code located at that address, you can see that this second argument does indeed point to the USER32!GetMessageW API, which was the target location of the requested code breakpoint.

0:001> $ Second WinDbg Session 
0:001> u 0x7630cde8
7630cde8   8bff mov edi,edi 
7630cdea 55     push ebp 
7630cdeb 8bec   mov ebp,esp 
7630cded 8b5510 mov edx,dword ptr [ebp+10h]

Finally, the third parameter (lpBuffer) is a pointer to the buffer that the debugger is trying to insert into this memory location. This is a single-byte buffer (as indicated by the value of the fourth argument, nSize, from the previous listing), representing the int 3 instruction. On both x86 and x64, this instruction is encoded using the single 0xCC byte, as you can see by using either the u (“un-assemble”) or db (“dump memory as a sequence of bytes”) commands:

0:001> $ Second WinDbg Session 
0:001> u 58d1b5d8
58d1b5d8 cc int 3 0:001> db 58d1b5d8
58d1b5d8 cc 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ...................

If you continue this experiment with the same breakpoints and type g again inside the second windbg.exe instance, you can similarly analyze the next hit of the WriteProcessMemory breakpoint and confirm that the initial byte from the USER32!GetMessageW function (0x8b in this case) is surreptitiously restored as the USER32!GetMessageW breakpoint gets hit, right before the user is able to issue commands in the first debugger UI, as shown in the following listing.

0:001> $ Second WinDbg Session 
0:001> g
Breakpoint 0 hit 
0:001> k
ChildEBP RetAddr 0092f128 58b84542 kernel32!WriteProcessMemory 
0092f160 58adb6a9 dbgeng!BaseX86MachineInfo::RemoveBreakpointInstruction+0xa2
0092f1a4 58ad3bcd dbgeng!LiveUserTargetInfo::RemoveCodeBreakpoint+0x59 
0092f1ec 58ad6c0a dbgeng!CodeBreakpoint::Remove+0x11d ...
0:001> dd esp
0092f108 58ce14a2 00000120 7630cde8 00680cc4
0092f118 00000001 0092f124 00479ed8 0000000b ...
0:001> db 00680cc4
00680cc4 8b 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0:001> $ Terminate both debugging sessions now 
0:001> q