While RtlUnwind is a critical API for implementing
compiler-level SEH, it's not documented anywhere. While technically a
KERNEL32 function, the Windows NT KERNEL32.DLL forwards the call to
NTDLL.DLL, which also has an RtlUnwind function. I was able to piece
together some pseudocode for it, which appears in Figure 12.
Figure 12 RtlUnwind Pseudocode
void _RtlUnwind( PEXCEPTION_REGISTRATION pRegistrationFrame,
PVOID returnAddr, // Not used! (At least on i386)
PEXCEPTION_RECORD pExcptRec,
DWORD _eax_value )
{
DWORD stackUserBase;
DWORD stackUserTop;
PEXCEPTION_RECORD pExcptRec;
EXCEPTION_RECORD exceptRec;
CONTEXT context;
// Get stack boundaries from FS:[4] and FS:[8]
RtlpGetStackLimits( &stackUserBase, &stackUserTop );
if ( 0 == pExcptRec ) // The normal case
{
pExcptRec = &excptRec;
pExcptRec->ExceptionFlags = 0;
pExcptRec->ExceptionCode = STATUS_UNWIND;
pExcptRec->ExceptionRecord = 0;
// Get return address off the stack
pExcptRec->ExceptionAddress = RtlpGetReturnAddress();
pExcptRec->ExceptionInformation[0] = 0;
}
if ( pRegistrationFrame )
pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING;
else
pExcptRec->ExceptionFlags|=(EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND);
context.ContextFlags =
(CONTEXT_i486 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS);
RtlpCaptureContext( &context );
context.Esp += 0x10;
context.Eax = _eax_value;
PEXCEPTION_REGISTRATION pExcptRegHead;
pExcptRegHead = RtlpGetRegistrationHead(); // Retrieve FS:[0]
// Begin traversing the list of EXCEPTION_REGISTRATION
while ( -1 != pExcptRegHead )
{
EXCEPTION_RECORD excptRec2;
if ( pExcptRegHead == pRegistrationFrame )
{
_NtContinue( &context, 0 );
}
else
{
// If there's an exception frame, but it's lower on the stack
// then the head of the exception list, something's wrong!
if ( pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead) )
{
// Generate an exception to bail out
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
_RtlRaiseException( &exceptRec2 );
}
}
PVOID pStack = pExcptRegHead + 8; // 8==sizeof(EXCEPTION_REGISTRATION)
if ( (stackUserBase <= pExcptRegHead ) // Make sure that
&& (stackUserTop >= pStack ) // pExcptRegHead is in
&& (0 == (pExcptRegHead & 3)) ) // range, and a multiple
{ // of 4 (i.e., sane)
DWORD pNewRegistHead;
DWORD retValue;
retValue = RtlpExecutehandlerForUnwind(
pExcptRec, pExcptRegHead, &context,
&pNewRegistHead, pExceptRegHead->handler );
if ( retValue != DISPOSITION_CONTINUE_SEARCH )
{
if ( retValue != DISPOSITION_COLLIDED_UNWIND )
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
RtlRaiseException( &excptRec2 );
}
else
pExcptRegHead = pNewRegistHead;
}
PEXCEPTION_REGISTRATION pCurrExcptReg = pExcptRegHead;
pExcptRegHead = pExcptRegHead->prev;
RtlpUnlinkHandler( pCurrExcptReg );
}
else // The stack looks goofy! Raise an exception to bail out
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_BAD_STACK;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
RtlRaiseException( &excptRec2 );
}
}
// If we get here, we reached the end of the EXCEPTION_REGISTRATION list.
// This shouldn't happen normally.
if ( -1 == pRegistrationFrame )
NtContinue( &context, 0 );
else
NtRaiseException( pExcptRec, &context, 0 );
}
PEXCEPTION_REGISTRATION RtlpGetRegistrationHead( void )
{
return FS:[0];
}
_RtlpUnlinkHandler( PEXCEPTION_REGISTRATION pRegistrationFrame )
{
FS:[0] = pRegistrationFrame->prev;
}
void _RtlpCaptureContext( CONTEXT * pContext )
{
pContext->Eax = 0;
pContext->Ecx = 0;
pContext->Edx = 0;
pContext->Ebx = 0;
pContext->Esi = 0;
pContext->Edi = 0;
pContext->SegCs = CS;
pContext->SegDs = DS;
pContext->SegEs = ES;
pContext->SegFs = FS;
pContext->SegGs = GS;
pContext->SegSs = SS;
pContext->EFlags = flags; // __asm{ PUSHFD / pop [xxxxxxxx] }
pContext->Eip = return address of the caller of the caller of this function
pContext->Ebp = EBP of the caller of the caller of this function
pContext->Esp = Context.Ebp + 8
}
|
While
RtlUnwind looks imposing, it's not hard to understand if you
methodically break it down. The API begins by retrieving the current top
and bottom of the thread's stack from FS:[4] and FS:[8]. These values
are important later as sanity checks to ensure that all of the exception
frames being unwound fall within the stack region.
RtlUnwind
next builds a dummy EXCEPTION_RECORD on the stack and sets the
ExceptionCode field to STATUS_UNWIND. Also, the EXCEPTION_UNWINDING flag
is set in the ExceptionFlags field of the EXCEPTION_RECORD. A pointer
to this structure will later be passed as a parameter to each exception
callback. Afterwards, the code calls the _RtlpCaptureContext function to
create a dummy CONTEXT structure that also becomes a parameter for the
unwind call of the exception callback.
The
remainder of RtlUnwind traverses the linked list of
EXCEPTION_REGISTRATION structures. For each frame, the code calls the
RtlpExecuteHandlerForUnwind function, which I'll cover later. It's this
function that calls the exception callback with the EXCEPTION_UNWINDING
flag set. After each callback, the corresponding exception frame is
removed by calling RtlpUnlinkHandler.
RtlUnwind
stops unwinding frames when it gets to the frame with the address that
was passed in as the first parameter. Interspersed with the code I've
described is sanity-checking code to ensure that everything looks okay.
If some sort of problem crops up, RtlUnwind raises an exception to
indicate what the problem was, and this exception has the
EXCEPTION_NONCONTINUABLE flag set. A process isn't allowed to continue
execution when this flag is set, so it must terminate.
Unhandled Exceptions
Earlier
in the article, I put off a full description of the
UnhandledExceptionFilter API. You normally don't call this API directly
(although you can). Most of the time, it's invoked by the
filter-expression code for KERNEL32's default exception callback. I
showed this earlier in the pseudocode for BaseProcessStart.
Figure 13
shows my pseudocode for UnhandledExceptionFilter. The API starts out a
bit strangely (at least in my opinion). If the fault is an
EXCEPTION_ACCESS_
VIOLATION, the code calls _BasepCheckForReadOnlyResource. While I
haven't provided pseudocode for this function, I can summarize it. If
the exception occurred because a resource section (.rsrc) of an EXE or
DLL was written to, _BasepCurrentTopLevelFilter changes the faulting
page's attributes from its normal read-only state, thereby allowing the
write to occur. If this particular scenario occurs,
UnhandledExceptionFilter returns EXCEPTION_
CONTINUE_EXECUTION and execution restarts at the faulting instruction.
Figure 13 UnHandledExceptionFilter Pseudocode
UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *pExceptionPtrs )
{
PEXCEPTION_RECORD pExcptRec;
DWORD currentESP;
DWORD retValue;
DWORD DEBUGPORT;
DWORD dwTemp2;
DWORD dwUseJustInTimeDebugger;
CHAR szDbgCmdFmt[256]; // Template string retrieved from AeDebug key
CHAR szDbgCmdLine[256]; // Actual debugger string after filling in
STARTUPINFO startupinfo;
PROCESS_INFORMATION pi;
HARDERR_STRUCT harderr; // ???
BOOL fAeDebugAuto;
TIB * pTib; // Thread information block
pExcptRec = pExceptionPtrs->ExceptionRecord;
if ( (pExcptRec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
&& (pExcptRec->ExceptionInformation[0]) )
{
retValue = _BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]);
if ( EXCEPTION_CONTINUE_EXECUTION == retValue )
return EXCEPTION_CONTINUE_EXECUTION;
}
// See if this process is being run under a debugger...
retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &debugPort, sizeof(debugPort), 0 );
if ( (retValue >= 0) && debugPort ) // Let debugger have it
return EXCEPTION_CONTINUE_SEARCH;
// Did the user call SetUnhandledExceptionFilter? If so, call their
// installed proc now.
if ( _BasepCurrentTopLevelFilter )
{
retValue = _BasepCurrentTopLevelFilter( pExceptionPtrs );
if ( EXCEPTION_EXECUTE_HANDLER == retValue )
return EXCEPTION_EXECUTE_HANDLER;
if ( EXCEPTION_CONTINUE_EXECUTION == retValue )
return EXCEPTION_CONTINUE_EXECUTION;
// Only EXCEPTION_CONTINUE_SEARCH goes on from here
}
// Has SetErrorMode(SEM_NOGPFAULTERRORBOX) been called?
if ( 0 == (GetErrorMode() & SEM_NOGPFAULTERRORBOX) )
{
harderr.elem0 = pExcptRec->ExceptionCode;
harderr.elem1 = pExcptRec->ExceptionAddress;
if ( EXCEPTION_IN_PAGE_ERROR == pExcptRec->ExceptionCode )
harderr.elem2 = pExcptRec->ExceptionInformation[2];
else
harderr.elem2 = pExcptRec->ExceptionInformation[0];
dwTemp2 = 1;
fAeDebugAuto = FALSE;
harderr.elem3 = pExcptRec->ExceptionInformation[1];
pTib = FS:[18h];
DWORD someVal = pTib->pProcess->0xC;
if ( pTib->threadID != someVal )
{
__try
{
char szDbgCmdFmt[256]
retValue = _GetProfileStringA( "AeDebug", "Debugger", 0, szDbgCmdFmt, sizeof(szDbgCmdFmt)-1 );
if ( retValue )
dwTemp2 = 2;
char szAuto[8]
retValue = GetProfileStringA( "AeDebug", "Auto", "0", szAuto, sizeof(szAuto)-1 );
if ( retValue )
if ( 0 == strcmp( szAuto, "1" ) )
if ( 2 == dwTemp2 )
fAeDebugAuto = TRUE;
}
__except( EXCEPTION_EXECUTE_HANDLER )
{
ESP = currentESP;
dwTemp2 = 1
fAeDebugAuto = FALSE;
}
}
if ( FALSE == fAeDebugAuto )
{
retValue = NtRaiseHardError( STATUS_UNHANDLED_EXCEPTION | 0x10000000, 4, 0, &harderr, _BasepAlreadyHadHardError ? 1 : dwTemp2, &dwUseJustInTimeDebugger );
}
else
{
dwUseJustInTimeDebugger = 3;
retValue = 0;
}
if ( retValue >= 0 && ( dwUseJustInTimeDebugger == 3) && ( !_BasepAlreadyHadHardError ) && ( !_BaseRunningInServerProcess ) )
{
_BasepAlreadyHadHardError = 1;
SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE };
HANDLE hEvent = CreateEventA( &secAttr, TRUE, 0, 0 );
memset( &startupinfo, 0, sizeof(startupinfo) );
sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent);
startupinfo.cb = sizeof(startupinfo);
startupinfo.lpDesktop = "Winsta0\Default";
CsrIdentifyAlertableThread(); // ???
retValue = CreateProcessA(
0, // lpApplicationName
szDbgCmdLine, // Command line
0, 0, // process, thread security attrs
1, // bInheritHandles
0, 0, // creation flags, environment
0, // current directory.
&statupinfo, // STARTUPINFO
&pi ); // PROCESS_INFORMATION
if ( retValue && hEvent )
{
NtWaitForSingleObject( hEvent, 1, 0 );
return EXCEPTION_CONTINUE_SEARCH;
}
}
if ( _BasepAlreadyHadHardError )
NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode);
}
return EXCEPTION_EXECUTE_HANDLER;
}
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter )
{
// _BasepCurrentTopLevelFilter is a KERNEL32.DLL global var
LPTOP_LEVEL_EXCEPTION_FILTER previous= _BasepCurrentTopLevelFilter;
// Set the new value
_BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter;
return previous; // return the old value
}
|
The
next task of UnhandledExceptionFilter is to determine if the process is
being run under a Win32 debugger. That is, the process was created with
the DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS flag.
UnhandledExceptionFilter uses the NtQueryInformationProcess Function
that I describe in this month's Under the Hood column to tell if the
process is being debugged. If so, the API returns
EXCEPTION_CONTINUE_SEARCH, which tells some other part of the system to
wake up the debugger process and tell it that an exception occurred in
the debuggee.
Next
on UnhandledExceptionFilter's plate is a call to the user-installed
unhandled exception filter, if present. Normally, there isn't a
user-installed callback, but one can be installed via the
SetUnhandledExceptionFilter API. I've also provided pseudocode for this
API. The API simply bashes a global variable with the new user callback
address, and returns the value of the old callback.
With
the preliminaries out of the way, UnhandledExceptionFilter can get down
to its primary job: informing you of your ignominious programming
blunder with the ever- stylish Application Error dialog. There are two
ways that this dialog can be avoided. The first is if the process has
called SetErrorMode and specified the SEM_NOGPFAULTERRORBOX flag. The
other method is to have the Auto value under the AeDebug registry key
set to 1. In this case, UnhandledExceptionFilter skips the Application
Error dialog and automatically fires up whatever debugger is specified
in the Debugger value of the AeDebug key. If you're familiar with "just
in time debugging," this is where the operating system supports it. More
on this later.
In
most cases, neither of these dialog avoidance conditions are true and
UnhandledExceptionFilter calls the NtRaiseHardError function in
NTDLL.DLL. It's this function that brings up the Application Error
dialog. This dialog waits for you to hit the OK button to terminate the
process, or Cancel to debug it. (Maybe it's just me, but hitting Cancel
to launch a debugger seems a little backward.)
If
you hit OK in the Application Error dialog box,
UnhandledExceptionFilter returns EXCEPTION_EXECUTE_HANDLER. The code
that called UnhandledExceptionFilter usually responds by terminating
itself (as you saw in the BaseProcessStart code). This brings up an
interesting point. Most people assume that the system terminates a
process with an unhandled exception. It's actually more correct to say
that the system sets up things so that an unhandled exception causes the
process to terminate itself.
The
truly interesting code in UnhandledExceptionFilter executes if you
select Cancel in the Application Error dialog, thereby bringing up a
debugger on the faulting process. The code first calls CreateEvent to
make an event that the debugger will signal after it has attached to the
faulting process. This event handle, along with the current process ID,
is passed to sprintf, which formats the command line used to start the
debugger. Once everything is prepared, UnhandledExceptionFilter calls
CreateProcess to start the debugger. If CreateProcess succeeds, the code
calls NtWaitForSingleObject on the event created earlier. This call
blocks until the debugger process signals the event, indicating that it
has attached to the faulting process successfully. There are other
little bits and pieces to the UnhandledExceptionFilter code, but I've
covered the important highlights here.
Into the Inferno
If
you've made it this far, it wouldn't be fair to finish without
completing the entire circuit. I've shown how the operating system calls
a user-defined function when an exception occurs. I've shown what
typically goes on inside of those callbacks, and how compilers use them
to implement _try and _catch. I've even shown what happens when nobody
handles the exception and the system has to do the mopping up. All that
remains is to show where the exception callbacks originate from in the
first place. Yes, let's plunge into the bowels of the system and see the
beginning stages of the structured exception handling sequence.
Figure 14
shows some pseudocode I whipped up for KiUserExceptionDispatcher and
some related functions. KiUserExceptionDispatcher is in NTDLL.DLL and is
where execution begins after an exception occurs. To be 100 percent
accurate, what I just said isn't exactly true. For instance, in the
Intel architecture an exception causes control to vector to a ring 0
(kernel mode) handler. The handler is defined by the interrupt
descriptor table entry that corresponds to an exception. I'm going to
skip all that kernel mode code and pretend that the CPU goes straight to
KiUserExceptionDispatcher upon an exception
Figure 14 KiUserExceptionDispatcher Pseudocode
KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
{
DWORD retValue;
// Note: If the exception is handled, RtlDispatchException() never returns
if ( RtlDispatchException( pExceptRec, pContext ) )
retValue = NtContinue( pContext, 0 );
else
retValue = NtRaiseException( pExceptRec, pContext, 0 );
EXCEPTION_RECORD excptRec2;
excptRec2.ExceptionCode = retValue;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
RtlRaiseException( &excptRec2 );
}
int RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
{
DWORD stackUserBase;
DWORD stackUserTop;
PEXCEPTION_REGISTRATION pRegistrationFrame;
DWORD hLog;
// Get stack boundaries from FS:[4] and FS:[8]
RtlpGetStackLimits( &stackUserBase, &stackUserTop );
pRegistrationFrame = RtlpGetRegistrationHead();
while ( -1 != pRegistrationFrame )
{
PVOID justPastRegistrationFrame = &pRegistrationFrame + 8;
if ( stackUserBase > justPastRegistrationFrame )
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}
if ( stackUsertop < justPastRegistrationFrame )
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}
if ( pRegistrationFrame & 3 ) // Make sure stack is DWORD aligned
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}
if ( someProcessFlag )
{
// Doesn't seem to do a whole heck of a lot.
hLog = RtlpLogExceptionHandler( pExcptRec, pContext, 0,
pRegistrationFrame, 0x10 );
}
DWORD retValue, dispatcherContext;
retValue= RtlpExecuteHandlerForException(pExcptRec, pRegistrationFrame,
pContext, &dispatcherContext,
pRegistrationFrame->handler );
// Doesn't seem to do a whole heck of a lot.
if ( someProcessFlag )
RtlpLogLastExceptionDisposition( hLog, retValue );
if ( 0 == pRegistrationFrame )
{
pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL; // Turn off flag
}
EXCEPTION_RECORD excptRec2;
DWORD yetAnotherValue = 0;
if ( DISPOSITION_DISMISS == retValue )
{
if ( pExcptRec->ExceptionFlags & EH_NONCONTINUABLE )
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.ExceptionNumber = STATUS_NONCONTINUABLE_EXCEPTION;
excptRec2.ExceptionFlags = EH_NONCONTINUABLE;
excptRec2.NumberParameters = 0
RtlRaiseException( &excptRec2 );
}
else
return DISPOSITION_CONTINUE_SEARCH;
}
else if ( DISPOSITION_CONTINUE_SEARCH == retValue )
{
}
else if ( DISPOSITION_NESTED_EXCEPTION == retValue )
{
pExcptRec->ExceptionFlags |= EH_EXIT_UNWIND;
if ( dispatcherContext > yetAnotherValue )
yetAnotherValue = dispatcherContext;
}
else // DISPOSITION_COLLIDED_UNWIND
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.ExceptionNumber = STATUS_INVALID_DISPOSITION;
excptRec2.ExceptionFlags = EH_NONCONTINUABLE;
excptRec2.NumberParameters = 0
RtlRaiseException( &excptRec2 );
}
pRegistrationFrame = pRegistrationFrame->prev; // Go to previous frame
}
return DISPOSITION_DISMISS;
}
_RtlpExecuteHandlerForException: // Handles exception (first time through)
MOV EDX,XXXXXXXX
JMP ExecuteHandler
RtlpExecutehandlerForUnwind: // Handles unwind (second time through)
MOV EDX,XXXXXXXX
int ExecuteHandler( PEXCEPTION_RECORD pExcptRec
PEXCEPTION_REGISTRATION pExcptReg
CONTEXT * pContext
PVOID pDispatcherContext,
FARPROC handler ) // Really a ptr to an _except_handler()
{
// Set up an EXCEPTION_REGISTRATION, where EDX points to the
// appropriate handler code shown below
PUSH EDX
PUSH FS:[0]
MOV FS:[0],ESP
// Invoke the exception callback function
EAX = handler( pExcptRec, pExcptReg, pContext, pDispatcherContext );
// Remove the minimal EXCEPTION_REGISTRATION frame
MOV ESP,DWORD PTR FS:[00000000]
POP DWORD PTR FS:[00000000]
return EAX;
}
Exception handler used for _RtlpExecuteHandlerForException:
{
// If unwind flag set, return DISPOSITION_CONTINUE_SEARCH, else
// assign pDispatcher context and return DISPOSITION_NESTED_EXCEPTION
return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT
? DISPOSITION_CONTINUE_SEARCH
: *pDispatcherContext = pRegistrationFrame->scopetable,
DISPOSITION_NESTED_EXCEPTION;
}
Exception handler used for _RtlpExecuteHandlerForUnwind:
{
// If unwind flag set, return DISPOSITION_CONTINUE_SEARCH, else
// assign pDispatcher context and return DISPOSITION_COLLIDED_UNWIND
return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT
? DISPOSITION_CONTINUE_SEARCH
: *pDispatcherContext = pRegistrationFrame->scopetable,
DISPOSITION_COLLIDED_UNWIND;
}
|
The
heart of KiUserExceptionDispatcher is its call to RtlDispatchException.
This kicks off the search for any registered exception handlers. If a
handler handles the exception and continues execution, the call to
RtlDispatchException never returns. If RtlDispatchException returns,
there are two possible paths: either NtContinue is called, which lets
the process continues, or another exception is raised. This time, the
exception isn't continuable, and the process must terminate.
Moving
on to the RtlDispatchExceptionCode, this is where you'll find the
exception frame walking code that I've referred to throughout this
article. The function grabs a pointer to the linked list of
EXCEPTION_REGISTRATIONs and iterates over every node, looking for a
handler. Because of the possibility of stack corruption, the routine is
very paranoid. Before calling the handler specified in each
EXCEPTION_REGISTRATION, the code ensures that the EXCEPTION_REGISTRATION
is DWORD-aligned, within the thread's stack, and higher on the stack
than the previous EXCEPTION_REGISTRATION.
RtlDispatchException
doesn't directly call the address specified in the
EXCEPTION_REGISTRATION structure. Instead, it calls
RtlpExecuteHandlerForException to do the dirty work. Depending on what
happens inside RtlpExecuteHandlerForException, RtlDispatchException
either continues walking the exception frames or raises another
exception. This secondary exception indicates that something went wrong
inside the exception callback and that execution can't continue.
The
code for RtlpExecuteHandlerForException is closely related to another
function, RtlpExecutehandlerForUnwind. You may recall that I mentioned
this function earlier when I described unwinding. Both of these
"functions" simply load the EDX register with different values before
sending control to the ExecuteHandler function. Put another way,
RtlpExecuteHandlerForException and RtlpExecutehandlerForUnwind are
separate front ends to a common function, ExecuteHandler.
ExecuteHandler
is where the handler field from the EXCEPTION_REGISTRATION is extracted
and called. Strange as it may seem, the call to the exception callback
is itself wrapped by a structured exception handler. Using SEH within
itself seems a bit funky but it makes sense if you ponder it for a
moment. If an exception callback causes another exception, the operating
system needs to know about it. Depending on whether the exception
occurred during the initial callback or during the unwind callback,
ExecuteHandler returns either DISPOSITION_NESTED_
EXCEPTION or DISPOSITION_COLLIDED_UNWIND. Both are basically "Red Alert!
Shut everything down now!" kind of codes.
If
you're like me, it's hard to keep all of the functions associated with
SEH straight. Likewise, it's hard to remember who calls who. To help
myself, I came up with the diagram shown in Figure 15.
Now,
what's the deal with setting EDX before getting to the ExecuteHandler
code? It's simple, really. ExecuteHandler uses whatever's in EDX as the
raw exception handler if something goes wrong while calling the
user-installed handler. It pushes the EDX register onto the stack as the
handler field for a minimal EXCEPTION_REGISTRATION structure. In
essence, ExecuteHandler uses raw structured exception handling like I
showed in the MYSEH and MYSEH2 programs.

Figure
15 Who Calls Who in SEH
Conclusion
Structured
exception handling is a wonderful Win32 feature. Thanks to the
supporting layers that compilers like Visual C++ put on top of it, the
average programmer can benefit from SEH with a relatively small
investment in learning. However, at the operating system level, things
are more complicated than the Win32 documentation would lead you to
believe.
Unfortunately,
not much has been written about system-level SEH to date because almost
everyone considers it an extremely difficult subject. The lack of
documentation on the system-level details hasn't helped. In this
article, I've shown that system-level SEH revolves around a relatively
simple callback. If you understand the nature of the callback, and then
build additional layers of understanding on top of that, system-level
structured exception handling really isn't so hard to grasp.
From the January 1997 issue of Microsoft Systems Journal.
|