When a program divides by zero, dereferences a bad pointer, or hits a breakpoint, the CPU does not simply give up — it raises an exception, and Windows runs a carefully ordered dispatch process to find something willing to handle it. That same machinery serves both the mundane (a try/except block catching a bad file read) and the exotic (a packer using an exception handler as a control-flow trampoline). Understanding how exceptions are dispatched, where handlers are registered, and how the stack is unwound is essential for debugging crashes, reversing obfuscated code, and reasoning about a whole class of memory-corruption exploits. This article follows an exception from the faulting instruction all the way to the handler that resolves it.
Exceptions vs Interrupts
It helps to distinguish two things the CPU treats similarly but that mean different things. An interrupt is asynchronous — it is triggered by an external source (a device, a timer) and is unrelated to the instruction currently executing. An exception is synchronous — it is a direct consequence of the instruction the processor just tried to run: an access violation from a bad memory reference, an integer divide-by-zero, an invalid opcode, a single-step or breakpoint trap.
When a processor exception fires, the CPU traps into the kernel through the appropriate entry in the Interrupt Descriptor Table, and Windows funnels it into its exception dispatcher (KiExceptionDispatch / KiDispatchException). The kernel packages two structures that travel with the exception for the rest of its life: an EXCEPTION_RECORD, which describes what happened (the exception code such as STATUS_ACCESS_VIOLATION, the faulting address, and flags), and a CONTEXT, which is a full snapshot of the CPU register state at the point of the fault. Handlers receive both, which is what lets a debugger show you exactly where and how a crash occurred.
The Dispatch Path
Windows gives interested parties two chances to handle an exception, in a fixed order:
- First chance. The kernel first offers the exception to an attached kernel debugger. If none handles it and the fault occurred in user mode, the kernel transfers control back to user mode through
ntdll!KiUserExceptionDispatcher, which runs the user-mode handler search — vectored handlers first, then the frame-based (SEH) handlers. - Second chance. If nothing claims the exception on the first pass, it is raised again. The debugger gets a final opportunity, and if there is still no handler, the unhandled exception filter runs — normally terminating the process and producing a crash dump or a Windows Error Reporting event.
This first-chance / second-chance model is exactly what you are choosing between in a debugger when you decide whether to break on an exception when it is raised or only when it goes unhandled.
Structured Exception Handling (SEH)
Structured Exception Handling is the compiler-supported mechanism behind __try/__except/__finally. How the registration is stored differs dramatically between architectures, and that difference has enormous security consequences.
On x86, SEH is stack-based. Each __try pushes an EXCEPTION_REGISTRATION_RECORD onto the stack — a small node containing a pointer to the next record and a pointer to the handler function. These nodes form a singly linked list whose head is stored in the Thread Environment Block at fs:[0] (the TEB ExceptionList field). When an exception occurs, the dispatcher walks this chain from the TEB outward, calling each handler until one handles the exception. Because these records live on the stack, a stack buffer overflow can overwrite a handler pointer — the root of classic SEH exploitation.
On x64 (and ARM64), SEH is table-based. There is no per-frame record on the stack at all. Instead, the compiler emits static RUNTIME_FUNCTION entries into the image’s .pdata section, describing each function’s code range and pointing at its unwind and exception information in .xdata. When an exception occurs, the dispatcher looks up the faulting instruction pointer, finds the owning function’s entry, and consults the read-only tables to locate handlers and unwind rules. Because the handler metadata lives in read-only image sections rather than on the writable stack, the classic stack-overwrite-the-handler attack simply does not apply.
Vectored Exception Handling (VEH)
Vectored Exception Handling is a newer, complementary mechanism. A process registers a handler with AddVectoredExceptionHandler, and — crucially — vectored handlers are process-wide and are called before any frame-based SEH handler, regardless of which stack frame the exception occurred in. The registered handlers live in a linked list inside ntdll, not on the stack and not tied to any function. This makes VEH ideal for cross-cutting concerns: a runtime that wants to observe every exception, or a component that needs to react to faults anywhere in the process.
The order matters in practice: VEH handlers see the exception first and can choose to handle it (EXCEPTION_CONTINUE_EXECUTION) or pass it along (EXCEPTION_CONTINUE_SEARCH) to the normal SEH search.
Unwinding
Handling an exception is a two-pass operation. In the first pass — the search pass — the dispatcher walks the handler chain (or the table entries) asking each filter whether it wants to handle this exception, without yet modifying the stack. Once a handler accepts, the second pass — the unwind pass — runs. Unwinding walks back down through the intervening stack frames, and for each one it executes any __finally blocks and, in C++, runs the destructors of objects going out of scope. Only after the stack has been cleanly unwound to the handling frame does control jump into the __except body. This is why __finally blocks and C++ destructors are guaranteed to run even when an exception tears through several frames.
Mitigations
Because stack-based SEH was such a productive exploitation target on x86, several mitigations were introduced:
- SafeSEH — a link-time mitigation. The linker records a table of all legitimate exception handlers in the image, and the dispatcher refuses to call a handler that is not in that table. An attacker can no longer point a handler at arbitrary code in a SafeSEH image.
- SEHOP (SEH Overwrite Protection) — a runtime mitigation that validates the integrity of the SEH chain before dispatching. It walks the chain to confirm it still ends at a known final record; an overwrite that corrupts a
Nextpointer breaks the chain and is detected. SEHOP works even for modules not compiled with SafeSEH. - x64 table-based handling — as described above, moving handler metadata off the stack and into read-only image sections structurally eliminates the stack-overwrite-the-handler vector on 64-bit code.
Inspecting Exception Handling
A debugger exposes every part of this machinery. The following commands are the core workflow for exception analysis in WinDbg:
# Control which exceptions break into the debugger.
# sxe = break on first chance; sxd = ignore first chance (break second)
sxe av # break when an access violation is first raised
sxd av # only break if the access violation goes unhandled
# On x86: dump the stack-based SEH handler chain for the current thread
!exchain
# Display the exception record and the context record for the
# current exception (e.g. after the debugger catches a fault)
.exr -1
.cxr <context-address>
# From the command line, inspect a function's x64 unwind/exception
# info stored in .pdata/.xdata
dumpbin /unwindinfo yourbinary.exe
sxe/sxd are how you tell the debugger whether to stop on first-chance or only unhandled exceptions — the practical face of the two-chance model. !exchain walks the x86 fs:[0] SEH chain and is invaluable for spotting a corrupted or overwritten handler. .exr and .cxr decode the EXCEPTION_RECORD and CONTEXT so you can see the faulting address and the exact register state, and dumpbin /unwindinfo lets you read the x64 unwind tables statically.
Security Relevance
The classic SEH overwrite exploit is a defining technique of the x86 era. A stack buffer overflow overwrites the EXCEPTION_REGISTRATION_RECORD sitting further up the stack — replacing the handler pointer with an attacker-chosen address (often a POP POP RET gadget that pivots execution to attacker-controlled data). When the overflow then triggers an exception, the dispatcher dutifully calls the corrupted handler, and control transfers to the payload. SafeSEH and SEHOP were designed precisely to break this, and x64’s table-based handling removes the on-stack target entirely — a good illustration of how understanding an internal mechanism directly informs the mitigation that defends it.
VEH shows up frequently in reverse engineering. Packers and anti-analysis code register vectored handlers and then deliberately trigger exceptions (for example by reading a PAGE_GUARD page or executing an int 3), using the handler as an obfuscated jump table or as a way to detect and confuse debuggers — since a debugger that swallows the exception changes the program’s behavior. The same guard-page-plus-VEH pattern is also used defensively as a lightweight way to monitor access to a region of memory. Recognizing these patterns turns confusing control flow into something you can follow. Everything here is described for defensive understanding, debugging, and authorized research.
Conclusion
Exception dispatch is one of the more intricate corners of Windows, weaving together the CPU, the kernel trap handlers, and user-mode runtime code. The key mental model is the ordered dispatch — first-chance to the debugger, then vectored handlers, then the frame-based SEH search, with a second chance before the process dies — and the architectural split between fragile stack-based x86 SEH and robust table-based x64 handling. That split is the reason a whole exploitation technique flourished and was then designed out of existence. In the next article we turn to how Windows components talk to one another efficiently across process boundaries: Advanced Local Procedure Call (ALPC).
You Might Also Like
This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:



Comments