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#

  • The exception object is created in a same way as any other managed object
  • vm\jitinterface.cpp:JIT_Throw is called
  • HelperFrame is pushed to mark the transition from jitted to EE code. This is needed for the reliable stack walk to work - security and garbage collection are main users of the reliable stackwalk.
  • vm\excep.cpp:RaiseTheException is called
  • The exception object is stored in the current Thread object
  • EXCEPTION_COMPLUS exception is raised using the Win32's RaiseException API

Software exceptions in the execution engine code - exceptions thrown using COMPlusThrow(...) inside clr\src\vm directory

  • The exception object is created in a same way as any other managed object if not already supplied to the call
  • vm\excep.cpp:RaiseTheException is called and the rest of the story is identical to the first case.
  • Note: There is no need to push any TransitionFrame
  • Note: The C++ stacktrace is not included in the managed exception stacktrace

Hardware exceptions in jitted code - access to null object, division by zero, etc.

  • The exception handler for jitted code is called. It is vm\i386\excepx86.cpp:COMPlusFrameHandler most of the time.
  • The exception is identified as coming straight from the jitted code in vm\i386\excepx86.cpp:CPFH_HandleManagedFault.
  • The FaultingExceptionFrame is pushed to mark the special transition from jitted to EE code.
  • The exception is rethrown using vm\i386\excepx86.cpp:LinkFrameAndThrow and associated asm helpers.

    For Win32/i386 SEH, the rethrow is done through returning EXCEPTION_CONTINUE_EXECUTION from the exception filter with IP modified to point to NakedThrowHelper.

    For PAL_PORTABLE_SEH, the rethrow is done by a simple call to LaunchNakedThrowHelper.
  • Second time in the exception filter, the exception object of the right type is created for the hardware exception. The mapping of hardware exceptions to BCL exception types is done in vm\excep.cpp:MapWin32FaultToCOMPlusException.
  • Note: The hardware exception rethrow is a implementation detail that is supposed to allow continuable exception (EXCEPTION_CONTINUE_EXECUTION) to work seamlessly. The continuable exceptions are not supported by CLI at the moment, but it may change someday. Also, there may be some interop scenarios where EXCEPTION_CONTINUE_EXECUTION is important.
  • Note: The rethrow of the hardware exceptions is done differently for PAL_PORTABLE_SEH because of EXCEPTION_CONTINUE_EXECUTION was not supported by the PAL when this work was done. EXCEPTION_CONTINUE_EXECUTION was added later for the debugger support.

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:

  • Access violations in the write barrier helpers (vm\i386\jithelp.asm:JIT_WriteBarrier and friends) are translated to access violations in the callers. The translation is done in vm\i386\excepx86.cpp:CPFH_AdjustContextForWriteBarrier. This allows JIT to optimize out the null check when calling the writebarrier. FJIT does not use this feature currently.
  • Although there is some shielding against stack overflow exceptions (vm\stackprobe.h), they can happen in the execution engine code which is usually really bad. This is by design for .NET v1, the CLR team is working on a better solution for Whidbey. The portable stack overflow handling does not ship in Rotor - we just die when stack overflow happens. Design and implementation of a portable stack overflow handling is left as an exercise for the reader ;-)
  • The metadata code is instrumented for access monitoring (#define ZAP_MONITOR) to allow profile guided metadata layout optimization. The instrumentation is done by causing all metadata accesses to A/V, log them and recover from them through EXCEPTION_CONTINUE_EXECUTION. (Yes, the performance of this solutions sucks.) The metadata instrumentation does not ship as part of Rotor, but it is important to keep it in mind to avoid code breakage.

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.

  • If the helper frame is not pushed, the exceptions are thrown from FCALLs using FCThrow helpers. (If the helperframe is pushed, it is a simple throw of the software exception in the execution engine code using COMPlusThrow)
  • FCThrow helper pushes the helper frame with a special FRAME_ATTR_CAPTURE_DEPTH_2. Because of this, FCThrow can be called only directly from the FCALL. It can't be called from function that is called from FCALL.
  • Once the helper frame is pushed, a COMPlusThrow is used to actually throw the exception like any other software exceptions in the execution engine code.
  • Note: FCThrow is just shortcut for HELPER_METHOD_FRAME_BEGIN(...); COMPlusThrow(...); HELPER_METHOD_FRAME_END(). If you see the long code pattern, be careful when trying to optimize it using FCThrow - it may be there for a good reason. FCALLs depend on a very fragile x86 interpreter. This interpreter needs help sometimes...

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.

First pass

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.

Second pass

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.