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 .
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.
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.
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.
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
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 .
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.
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.
中文版:http://www.xfocus.net/articles/200503/785.html
|