I've seen a few questions floating around about the exception handling mechanism used in Rotor (and the CLR). Here's another list of notes about exception handling in Rotor. It was written by Jan Kotas back in the day to help Rotor developers debug and understand exceptions in the CLR.
Exception is Born and Thrown
This step heavily depends on the kind of the exception and where the exception is thrown from:
Software exceptions in the jitted code - exceptions thrown using throw new MyException(...) from C#
Software exceptions in the execution engine code - exceptions thrown using COMPlusThrow(...) inside clr\src\vm directory
Hardware exceptions in jitted code - access to null object, division by zero, etc.
Hardware exceptions in the execution engine code
The hardware exceptions in the execution engine code are not supposed to happen in general. If they do, it usually means that something went really wrong and a good amount of luck is needed to recover from the situation. However, there are a few exemptions to this rule:
Software exceptions in FCALLs - exceptions thrown using FCThrow(...) and its flavors
FCALLs smell like a jitted code but they are really not, so exception handling in FCALLs is special.
One can see that all exceptions are transformed into one uniform case that looks like a regular managed exception.
Every exception thrown from managed code - no matter whether it is hardware exception or software exception - goes down to Win32 RaiseException layer. This is done for consistency. It is believed to simplify the implementation of interoperability. Short-circuiting the software exceptions to avoid the system RaiseException layer when possible is left as an exercise for the reader.
Exception is handled
The handling of an exception follows the two pass Win32 model.
The stackwalker then calls callback for every method on the stack. Once some filter returns that it handles the exception, the 2nd pass of an exception unwinding starts.
The stack is unwound.
The structure of unwinding code differs between the native Win32 i386 exception handling and PAL portable exception handling.
Exceptions and Garbage Collector
Notice that the call to RaiseException is made from EE code and that this code is running in cooperative mode. Thus the garbage collection is not blocked during exception dispatch.
Exceptions and Frames
A linked list of frames (descendants of class Frame - vm\frames.h) is associated with every Thread object. These are a crucial part of the reliable stack walk infrastructure. The execution engine code is using a special mechanism to unwind this chain on exception. The COMPLUS_TRY / COMPLUS_CATCH macros (vm\exceptmacros.h) unwind the frame chain by calling UnwindFrameChain (vm\excep.cpp) on exception. In addition, these macros also unwind nested exception info (vm\excep.cpp:UnwindExInfo) and restore the GC mode which is really handy.
Notice that that UnwindFrameChain is accessing the stack space that is already unwound. This works, but it depends on subtle low-level implementation details of stack unwinding. Since the low-level implementation details of stack unwinding differ between Win32/i386 SEH and PAL_PORTABLE_SEH, the COMPLUS_TRY / COMPLUS_CATCH macros are different for these two. It also explains why it is necessary to catch exceptions using COMPLUS_TRY / COMPLUS_CATCH inside the execution engine, and not using the regular PAL_TRY / PAL_EXCEPT.
The CLR developers did not choose to unwind the chain of frames by creating C++ destructor for class Frame which would be a much straightforward solution. This may be a pure historic relic; they might have some performance fairy tale; or mixing of object destructors with structured exception handling that is not supported by MSVC would cause too much discomfort when writing code.
Exceptions and C++ Object Unwinding
The PAL_PORTABLE_SEH does not call C++ destructors for objects on the stack during exception unwind at all. This is ok because of the execution engine code does not seem to take advantage of this C++ feature currently. Well, we have not found the place where it depends on C++ destructors being called yet. This will probably change for Whidbey.
Exceptions and Security
It is important to get the stack walk right from within the exception processing because of the security implications. It is especially tricky for filters because of the real stack is not unwound when filters are called.
An arbitrary user code coming from source up the stack can be called between the time exception is thrown and finally is called. This has interesting implications for writing secure (managed) exception handlers and handling reentrancy in (managed) libraries. The exact analysis of this topic is left as an exercise for the reader.
This posting is provided “AS IS“ with no warranties, and confers no rights.