Understanding the x64 Windows SEH Mechanism
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
This article breaks down the low-level mechanics of x64 Windows Structured Exception Handling (SEH) from a reverse-engineering perspective. It assumes you already understand SEH from a developer’s point of view; here we go one level deeper, into the data structures and the kernel-mode dispatch path.
For SEH to work, three parties must cooperate: the compiler, the CPU, and the operating system. The concrete implementation of SEH is architecture-specific — the x64 mechanism described here is fundamentally different from the legacy x86 frame-based SEH.
The diagram below summarizes the overall Windows exception-handling framework:

A minimal example
Here is a minimal program we’ll use as a running example:
// test.c
#include <windows.h>
#include <stdio.h>
int main() {
__try {
printf("__try block\n");
}
__except (EXCEPTION_EXECUTE_HANDLER) {
printf("__except block\n");
}
return 0;
}
The RUNTIME_FUNCTION structure
Unlike x86, the x64 SEH mechanism no longer places exception-handling information on the stack. Instead, the compiler emits the metadata into a dedicated PE section: .pdata. The first structure to understand is RUNTIME_FUNCTION. Several of these form an array inside .pdata:
typedef struct _RUNTIME_FUNCTION {
DWORD BeginAddress; // Start RVA of the SEH code chunk
DWORD EndAddress; // End RVA of the SEH code chunk
DWORD UnwindData; // RVA of an UNWIND_INFO structure that describes this code frame
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
The three fields:
BeginAddress— function start addressEndAddress— function end addressUnwindData— pointer to anUNWIND_INFOstructure
With a PE tool you can inspect this array. For the example program above, the exception directory (opened with CFF Explorer) looks like this:

The number of these structures does NOT depend on the number of __try blocks — it depends on the number of functions that contain a __try block (no matter how many __try blocks a single function holds). Also note that RUNTIME_FUNCTION entries in .pdata are sorted in ascending order by BeginAddress.
Why sorted? To support
O(log n)binary search. When an exception occurs, the kernel exception dispatcher (RtlLookupFunctionEntry) must locate the owning function from the faulting RIP as fast as possible. Binary search is the only algorithm that does this in logarithmic time, and binary search requires an ordered dataset.
Also note all three addresses are offsets relative to ImageBase — which is why they fit in 32 bits even on a 64-bit system.
In IDA you can jump straight to main’s RUNTIME_FUNCTION entry with the g shortcut:
.pdata:000000014000400C dd rva main ; FunctionStart
.pdata:0000000140004010 dd rva byte_140001096 ; FunctionEnd
.pdata:0000000140004014 dd rva stru_140002804 ; UnwindInfo
The first two fields are the start and end addresses of main. Next, the UNWIND_INFO structure pointed to by UnwindData.
The UNWIND_INFO structure
This structure is where the core of SEH lives:
// Unwind info flags
#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x01
#define UNW_FLAG_UHANDLER 0x02
#define UNW_FLAG_CHAININFO 0x04
// UNWIND_CODE: a 3-byte structure
typedef union _UNWIND_CODE {
struct {
UBYTE CodeOffset;
UBYTE UnwindOp : 4;
UBYTE OpInfo : 4;
};
USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;
typedef struct _UNWIND_INFO {
UBYTE Version : 3; // + 0x00 - Unwind info structure version
UBYTE Flags : 5; // + 0x00 - Flags (see above)
UBYTE SizeOfProlog; // + 0x01
UBYTE CountOfCodes; // + 0x02 - Count of unwind codes
UBYTE FrameRegister : 4; // + 0x03
UBYTE FrameOffset : 4; // + 0x03
UNWIND_CODE UnwindCode[1]; // + 0x04 - Unwind code array
UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
union {
OPTIONAL ULONG ExceptionHandler; // Exception handler routine
OPTIONAL ULONG FunctionEntry;
};
OPTIONAL ULONG ExceptionData[]; // C++ scope-table structure
} UNWIND_INFO, *PUNWIND_INFO;
The definition is dense; we focus on the key parts.
The Flags field
Depending on Flags, the anonymous union field of UNWIND_INFO is interpreted differently, and whether ExceptionData is valid also depends on Flags. The following definition expresses that relationship more clearly:
#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x1
#define UNW_FLAG_UHANDLER 0x2
#define UNW_FLAG_CHAININFO 0x4
typedef struct _UNWIND_INFO {
UBYTE Version : 3;
UBYTE Flags : 5;
UBYTE SizeOfProlog;
UBYTE CountOfCodes;
UBYTE FrameRegister : 4;
UBYTE FrameOffset : 4;
UNWIND_CODE UnwindCode[1];
union {
// If (Flags & UNW_FLAG_EHANDLER)
OPTIONAL ULONG ExceptionHandler;
// Else if (Flags & UNW_FLAG_CHAININFO)
OPTIONAL ULONG FunctionEntry;
};
// If (Flags & UNW_FLAG_EHANDLER)
OPTIONAL ULONG ExceptionData[];
} UNWIND_INFO, *PUNWIND_INFO;
To get to the point quickly, we only discuss the case where Flags is set to UNW_FLAG_EHANDLER. A typical function whose Flags is UNW_FLAG_EHANDLER looks like this:
void ExceptFunc() { __try {} __except(1) {} }
When Flags is UNW_FLAG_EHANDLER, the ExceptionHandler field in the union is valid and filled in by the compiler. If an exception occurs and the instruction pointer satisfies >= BeginAddress and < EndAddress, that ExceptionHandler is invoked.
ExceptionHandler parses the ExceptionData field to decide how to handle the exception.
The C_SCOPE_TABLE structure
ExceptionData is a pointer (an offset, really) to a C_SCOPE_TABLE structure. Read this one carefully:
// C Scope table entry
typedef struct _C_SCOPE_TABLE_ENTRY {
ULONG Begin; // +0x00 - Begin of the guarded code block; offset of the first instruction of the __try block
ULONG End; // +0x04 - End of the target code block; offset of the instruction after the last instruction of the __try block
ULONG Handler; // +0x08 - Exception filter function (or "__finally" handler)
ULONG Target; // +0x0C - Exception handler pointer (the code inside the __except block)
} C_SCOPE_TABLE_ENTRY, *PC_SCOPE_TABLE_ENTRY;
// C Scope table
typedef struct _C_SCOPE_TABLE {
ULONG NumEntries; // +0x00 - Number of entries
C_SCOPE_TABLE_ENTRY Table[1]; // +0x04 - Scope table array
} C_SCOPE_TABLE, *PC_SCOPE_TABLE;
RUNTIME_FUNCTION describes the whole range of a function that contains SEH, while SCOPE_TABLE describes each individual __try/__except block within that function.
Putting it together
VOID FrobThePointer(PUCHAR UserAddress)
{
__try
{
*UserAddress = 0;
*UserAddress = 1;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("Bad Address\n");
}
}
For the code above, the generated assembly looks roughly like this:
<00> mov [rsp+0x8],rcx
<05> sub rsp,0x28
<09> mov rax,[rsp+0x30] // Move UserAddress into RAX
<0e> mov byte ptr [rax],0x0 // *UserAddress = 0;
<11> mov rax,[rsp+0x30] // Move UserAddress into RAX
<16> mov byte ptr [rax],0x1 // *UserAddress = 1;
<19> jmp FrobThePointer+0x28 // Success!
<1b> lea rcx,"Bad Address\n" // Begin of code in the except block...
// prepare to DbgPrint
<22> call DbgPrint
<27> nop
<28> add rsp,0x28
<2c> ret
Mapped onto the RUNTIME_FUNCTION and UNWIND_INFO structures:

How the exception location is found (using a divide-by-zero as the example)
At this point we can summarize how x64 SEH locates the faulting point. Let EA be the address where the exception occurred. First, binary search the RUNTIME_FUNCTION array (matching BeginAddress < EA < EndAddress) to find the faulting function. Then walk the C_SCOPE_TABLE_ENTRY array pointed to by ExceptionData to find the faulting try block (matching Begin < EA < End).
That’s the basic flow, but we can go deeper. Let’s trace a divide-by-zero (#DE) in a 64-bit Windows program through the full system-level call chain and data-structure operations — from the CPU trap all the way to executing the __except block.
When an exception occurs, the internal Windows function
RtlDispatchExceptionis called. It is implemented in NTDLL for user-mode exceptions and in NTOSKRNL for kernel-mode exceptions, with slight differences. It begins with initial checks: if a user-mode VEH (vectored exception handler) is present, that handler is invoked; otherwise standard SEH processing proceeds.
Standard SEH processing: copy the thread context at the time of the exception, then use
RtlLookupFunctionEntryto perform the critical task — obtaining the PEImageBaseand theRUNTIME_FUNCTIONstructure.
Phase 1: hardware trap and kernel takeover
1. Triggering the exception
; executing in user mode
mov eax, 1
cdq
idiv dword ptr [rcx] ; assume [rcx]=0, triggers #DE
2. CPU hardware operation
- The CPU detects the divide-by-zero; interrupt vector 0 (
#DE) fires. - The CPU automatically switches to kernel mode (loading the
GSbase via the preconfigured TSS or MSR). - The CPU pushes the key user-mode context onto the current thread’s kernel stack, forming a
KTRAP_FRAME. This includes:RIP— pointing at the instruction afteridivRSP— the user-mode stack pointerRFLAGS,CS,SS, and other segment registers- general registers
RAX,RCX,RDX, etc.
- The CPU looks up IDT (Interrupt Descriptor Table) entry 0 and jumps to the preset interrupt handler entry. On Windows this is
KiDivideErrorFault(or a similar trap handler).
3. Kernel trap handling (KiDivideErrorFault)
// pseudocode, lives in ntoskrnl.exe
VOID KiDivideErrorFault() {
// 1. build a fuller trap frame
KTRAP_FRAME* pTrapFrame = GetCurrentTrapFrame();
// 2. package the exception info into an EXCEPTION_RECORD
EXCEPTION_RECORD ExceptionRecord = {0};
ExceptionRecord.ExceptionCode = STATUS_INTEGER_DIVIDE_BY_ZERO; // 0xC0000094
ExceptionRecord.ExceptionAddress = pTrapFrame->Rip;
ExceptionRecord.NumberParameters = 0;
// 3. call the common exception dispatch routine
KiExceptionDispatch(&ExceptionRecord, pTrapFrame);
}
Phase 2: kernel exception dispatch (KiExceptionDispatch / KiDispatchException)
This is the central dispatcher. Its internals are complex, but the key steps are:
1. Build CONTEXT and EXCEPTION_POINTERS
// inside KiDispatchException
CONTEXT ContextRecord = {0};
// fill ContextRecord's registers from the KTRAP_FRAME
RtlpCaptureContext(&ContextRecord, pTrapFrame);
EXCEPTION_POINTERS ExceptionPointers = {0};
ExceptionPointers.ExceptionRecord = &ExceptionRecord;
ExceptionPointers.ContextRecord = &ContextRecord;
2. First attempt: user-mode exception dispatch
The kernel checks the exception address and finds RIP is in user space, so it calls RtlDispatchException (the kernel entry point for user-mode exception dispatch).
BOOLEAN RtlDispatchException(PEXCEPTION_RECORD pExceptionRecord, PCONTEXT pContextRecord) {
// [Key point 1] check VEH (vectored exception handlers)
if (RtlCallVectoredExceptionHandlers(pExceptionRecord, pContextRecord)) {
return TRUE; // VEH handled it
}
// [Key point 2] locate the RUNTIME_FUNCTION
PRUNTIME_FUNCTION pRuntimeFunction = RtlLookupFunctionEntry(
pContextRecord->Rip, // control point
&ImageBase, // out: module base
NULL // history table
);
if (pRuntimeFunction == NULL) {
// no function table; usually JIT code, cannot unwind
return FALSE;
}
// [Key point 3] get UNWIND_INFO
PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(ImageBase + pRuntimeFunction->UnwindInfoAddress);
// [Key point 4] call the language-specific exception handler (e.g. __C_specific_handler)
if (pUnwindInfo->Flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER)) {
// compute the ExceptionData address
PVOID pHandlerData = RtlpGetHandlerData(pUnwindInfo);
// this function is provided by the MSVC runtime; it parses SCOPE_RECORD
EXCEPTION_DISPOSITION disposition = __C_specific_handler(
pExceptionRecord,
(PVOID)pContextRecord->Rip,
pContextRecord,
pHandlerData
);
if (disposition == ExceptionExecuteHandler) {
// handler found, prepare to unwind
return TRUE;
}
}
// [Key point 5] no handler: unwind one frame (for C++ exception propagation)
RtlUnwindEx(...);
return FALSE;
}
3. What RtlLookupFunctionEntry does
This function is the concrete implementation of the first-phase lookup.
PRUNTIME_FUNCTION RtlLookupFunctionEntry(
IN ULONG64 ControlPc,
OUT PULONG64 ImageBase,
IN OUT PT_RUNTIME_FUNCTION* HistoryTable
) {
// 1. find the PE module (DLL/EXE) that owns ControlPc
PLDR_DATA_TABLE_ENTRY pModule = LdrFindEntryForAddress(ControlPc);
*ImageBase = pModule->DllBase;
// 2. locate the .pdata section from the PE header
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(*ImageBase + ((PIMAGE_DOS_HEADER)*ImageBase)->e_lfanew);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++, pSection++) {
if (memcmp(pSection->Name, ".pdata", 6) == 0) {
break;
}
}
// 3. compute the start and end of the RUNTIME_FUNCTION array
PRUNTIME_FUNCTION pFunctionTable = (PRUNTIME_FUNCTION)(*ImageBase + pSection->VirtualAddress);
ULONG NumberOfFunctions = pSection->Misc.VirtualSize / sizeof(RUNTIME_FUNCTION);
// 4. [core] binary search
return RtlpBinarySearch(pFunctionTable, ControlPc - *ImageBase, NumberOfFunctions);
}
4. What __C_specific_handler does (a user-mode callback!)
This function is provided by the MSVC runtime library (vcruntimexxx.dll). Note: the CPU is still in kernel mode at this point, but this function is user-mode code — the kernel temporarily switches to user mode to execute it.
EXCEPTION_DISPOSITION __C_specific_handler(
PEXCEPTION_RECORD pExceptionRecord,
PVOID EstablisherFrame, // actually the stack-frame pointer of the faulting function
PCONTEXT pContextRecord,
PEXCEPTION_REGISTRATION_RECORD pDispatcherContext
) {
// 1. extract the SCOPE_RECORD array from DispatcherContext
PSCOPE_RECORD pScopeRecords = (PSCOPE_RECORD)pDispatcherContext;
DWORD scopeCount = pScopeRecords->Count;
pScopeRecords++; // point to the first SCOPE_RECORD
// 2. compute the offset of the exception within the function
DWORD_PTR ImageBase = ...;
PRUNTIME_FUNCTION pRF = ...;
DWORD offsetInFunction = (DWORD)(pContextRecord->Rip - ImageBase - pRF->BeginAddress);
// 3. linear scan the SCOPE_RECORD array
for (DWORD i = 0; i < scopeCount; i++) {
if (offsetInFunction >= pScopeRecords[i].BeginAddress &&
offsetInFunction < pScopeRecords[i].EndAddress) {
// 4. matching try block found! call the filter
if (pScopeRecords[i].HandlerAddress) {
int filterResult = ((FILTER_FUNC)pScopeRecords[i].HandlerAddress)();
if (filterResult == EXCEPTION_EXECUTE_HANDLER) {
// 5. set the jump target
pDispatcherContext->TargetIp = ImageBase + pRF->BeginAddress + pScopeRecords[i].JumpTarget;
return ExceptionExecuteHandler;
}
}
}
}
return ExceptionContinueSearch;
}
Phase 3: stack unwinding and final dispatch
1. What the kernel does after receiving ExceptionExecuteHandler
The kernel’s KiDispatchException receives TRUE from RtlDispatchException and knows a handler was found.
// inside KiDispatchException
if (FirstPassSuccess) { // i.e. RtlDispatchException returned TRUE
// 1. get the target address (set by __C_specific_handler)
ULONG64 TargetIp = GetTargetIpFromDispatcherContext();
// 2. perform the stack unwind
RtlUnwindEx(
TargetFrame, // which frame to unwind to
TargetIp, // jump target
pExceptionRecord,
ReturnValue, // return value
pContextRecord,
HistoryTable
);
// 3. rewrite RIP in the trap frame
pTrapFrame->Rip = TargetIp;
// 4. [key] resume execution
KiContinuePreviousMode(pContextRecord, pTrapFrame, PreviousMode);
}
2. The RtlUnwindEx unwind process
This is the actual frame unwinder; it interprets the UNWIND_CODE array.
VOID RtlUnwindEx(...) {
// unwind frame by frame until reaching the target frame
while (CurrentFrame < TargetFrame) {
// for the current frame, look up its RUNTIME_FUNCTION
PRUNTIME_FUNCTION pRF = RtlLookupFunctionEntry(CurrentRip, &ImageBase, NULL);
PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(ImageBase + pRF->UnwindInfoAddress);
// interpret the UNWIND_CODE array
for (int i = 0; i < pUnwindInfo->CountOfCodes; i++) {
switch (pUnwindInfo->UnwindCode[i].UnwindOp) {
case UWOP_PUSH_NONVOL:
// simulate a pop to restore the register
Context->Rsp += 8;
Context->Rbx = *(PULONG64)(Context->Rsp - 8);
break;
case UWOP_ALLOC_SMALL:
Context->Rsp += (pUnwindInfo->UnwindCode[i].OpInfo * 8) + 8;
break;
// ... other unwind opcodes
}
}
// if there's a termination handler (__finally), call it now
if (pUnwindInfo->Flags & UNW_FLAG_UHANDLER) {
CallTerminationHandler(pUnwindInfo, Context);
}
// move to the caller's frame
CurrentRip = *(PULONG64)Context->Rsp;
Context->Rsp += 8;
}
}
Phase 4: returning to user mode and executing the handler block
1. Final return (KiContinuePreviousMode)
The kernel writes the modified CONTEXT back into the trap frame, then executes iretq (or sysret) to return to user mode — but the RIP is no longer just after idiv; it is the address of the __except block.
2. User mode resumes
; user mode, but RIP now points at the __except block
; assume the __except block is at 0x140001050
0x140001050: mov rcx, str_inside_except
0x140001057: call printf
; ... exception handling code
Summary of the key data structures and call chain:
| Phase | Key function | Key structure | Operation |
|---|---|---|---|
| Hardware trap | KiDivideErrorFault |
KTRAP_FRAME |
save user context |
| Kernel dispatch | KiDispatchException |
EXCEPTION_RECORD, CONTEXT |
build exception info |
| User dispatch | RtlDispatchException |
- | coordinate the dispatch flow |
| Function lookup | RtlLookupFunctionEntry |
RUNTIME_FUNCTION array |
binary-search the function |
| Unwind info | - | UNWIND_INFO, UNWIND_CODE |
describe the stack-frame layout |
| Scope lookup | __C_specific_handler |
SCOPE_RECORD array |
linear-search the try block |
| Stack unwind | RtlUnwindEx |
UNWIND_CODE |
interpret unwind instructions |
| Return | KiContinuePreviousMode |
KTRAP_FRAME |
restore user-mode context |
The overall call chain:
KiUserExceptionDispatcher // Windows Kernel Internal (KI) API
-> RtlDispatchException // main logic for exception handling
-> RtlpCallVectoredHandlers // call any VEH
-> RtlLookupFunctionEntry // look for a valid PRUNTIME_FUNCTION entry in the ExceptionDirectory
-> RtlpLookupDynamicFunctionEntry // if no valid PRUNTIME_FUNCTION, run any dynamic callbacks
-> RtlVirtualUnwind / RtlpxVirtualUnwind // perform stack-frame unwinding
-> RtlpExecuteHandlerForException // execute the exception handler!
The UNWIND_CODE structure
Inside UNWIND_INFO there is a UnwindCode field that points to a UNWIND_CODE structure.
UNWIND_CODE is the “unwind script” or “reverse-operation instruction set” of the x64 SEH mechanism. It does not participate in the decision of exception handling (that’s SCOPE_RECORD’s job); it handles the execution side — how to safely and correctly restore the stack frame to its pre-call state. It is the cornerstone of stack unwinding.
The core role of UNWIND_CODE: the frame’s “construction blueprint” and “disassembly manual”
Think of a function call as building blocks:
- Prolog is the “building” phase — it allocates stack space and saves registers.
- Epilog is the “taking apart” phase — it restores registers, releases stack space, and returns.
- When an exception occurs, the program stops abruptly, mid-build.
UNWIND_CODEis the emergency disassembly manual that tells the system how to safely take the half-built structure back to its pre-call state, no matter which step it was interrupted at.
Key point: an exception can occur at any point after the prolog and before the epilog. The system must be able to restore the stack to the pre-function state from any point. UNWIND_CODE provides the exact, step-by-step reverse operations to do that.
UNWIND_CODE in detail
Defined in winnt.h:
typedef union _UNWIND_CODE {
struct {
UBYTE CodeOffset; // offset from the start of the prolog, in bytes
UBYTE UnwindOp:4; // unwind opcode
UBYTE OpInfo:4; // opcode additional info
};
USHORT FrameOffset; // for some UnwindOp values, the whole USHORT is a frame offset
} UNWIND_CODE, *PUNWIND_CODE;
Field meanings:
CodeOffset— the most critical and elegant design. It says after which “progress point” of the prolog this unwind operation should run. The system compares the exception address (RIP) against the function start to decide whichUNWIND_CODEentries to execute. This ensures the stack is correctly restored regardless of whether the exception occurred after the prolog, inside the function body, or right before the epilog.UnwindOp— the unwind opcode. Defines the concrete restore operation (e.g. pop a register, add toRSP, etc.).OpInfo— opcode additional info, usually a register number or a size parameter.
How UNWIND_CODE works in practice
Let’s trace a concrete example. Assume the following function:
// corresponding assembly prolog
MyFunc:
push rbx ; save non-volatile register
push rsi
sub rsp, 0x20 ; allocate local-variable space
... ; function body
The compiler generates the following logical UNWIND_CODE array (pseudocode):
UNWIND_CODE UnwindCode[] = {
{CodeOffset: 0x0, UnwindOp: UWOP_PUSH_NONVOL, OpInfo: RBX}, // corresponds to push rbx
{CodeOffset: 0x1, UnwindOp: UWOP_PUSH_NONVOL, OpInfo: RSI}, // corresponds to push rsi
{CodeOffset: 0x2, UnwindOp: UWOP_ALLOC_SMALL, OpInfo: 4} // corresponds to sub rsp,0x20 (4*8=0x20)
};
Note: CodeOffset is cumulative. The first entry corresponds to prolog byte 0 (push rbx), the second to byte 1 (push rsi), the third to byte 2 (sub rsp,0x20).
Scenario analysis: exceptions at different positions
Scenario 1: exception after the prolog, inside the function body (e.g. after sub rsp, 0x20)
- The system computes the
RIPoffset — say0x05. - It walks the
UNWIND_CODEarray and executes every entry withCodeOffset <= 0x05(all three):- reverse operation:
add rsp, 0x20(undo the stack allocation) - reverse operation: simulate
pop rsi(restorersifrom the stack into theCONTEXTstructure) - reverse operation: simulate
pop rbx(restorerbx)
- reverse operation:
RSP,RBX, andRSIare restored to their function-entry state.
Scenario 2: exception during the prolog (e.g. after push rsi, before sub rsp,0x20)
- The
RIPoffset is0x01. - Only entries with
CodeOffset <= 0x01run (the first two):- reverse operation: simulate
pop rsi - reverse operation: simulate
pop rbx
- reverse operation: simulate
- Note: the
UWOP_ALLOC_SMALLoperation (add rsp,0x20) is NOT executed, because the correspondingsub rsp,0x20hasn’t run yet! This is the essence of theCodeOffsetmechanism: only undo the prolog operations that have actually executed.
Scenario 3: unwinding for __finally
The process is identical for __finally. UNWIND_CODE ensures the stack is correctly restored, then the system jumps to the __finally block to run cleanup code, then continues unwinding.
The main UnwindOp opcodes and their reverse operations
| Opcode | Meaning | Example prolog instruction | Reverse operation at unwind (performed by RtlUnwindEx) |
|---|---|---|---|
UWOP_PUSH_NONVOL |
push a non-volatile register | push rbx |
pop the value from the stack and restore the corresponding register in CONTEXT. RSP += 8. |
UWOP_ALLOC_SMALL |
allocate small stack space | sub rsp, 0x20 |
RSP += allocated size. |
UWOP_ALLOC_LARGE |
allocate large stack space | sub rsp, 0x1000 |
RSP += allocated size. |
UWOP_SAVE_NONVOL |
save a non-volatile register onto the stack | mov [rsp+0x10], rbx |
read the value at the specified stack offset and restore the corresponding register in CONTEXT. |
UWOP_SAVE_XMM128 |
save an XMM register | movaps [rsp+0x20], xmm6 |
restore the XMM register in CONTEXT. |
UWOP_SET_FPREG |
establish a frame pointer | mov rbp, rsp |
restore RBP to the caller value saved in CONTEXT. |
UWOP_PUSH_MACHFRAME |
push a machine frame (for hardware interrupts) | done automatically by the hardware interrupt | special interrupt recovery. |
Important: the “pop” during unwinding is logical, not literal. What actually happens:
- Compute the original value’s position on the stack from the current
RSPandCodeOffset. - Read that value into the corresponding register field of
CONTEXT. - Update
CONTEXT’sRSPto simulate the stack-pointer movement.
The stack memory itself is not modified — only CONTEXT is updated, preparing for resumption of execution.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch