After quite the lengthy tread through GCC and C-land (mind you everything discussed before was written in C), we finally find ourselves back at C++. C++ exception semantics seem simple, but actually hide many surprising edge cases.

Functions

Like before, let’s discuss the API of the C++ exception handling regime. Note that while you’re not supposed to call these functions directly, these functions are called in compiler generated code anyways, so are a part of the public ABI.

The functions are found in cxxabi.h:

  • __cxa_allocate_exception: Allocates space for an exception
  • __cxa_free_exception: Frees memory from an exception
  • __cxa_throw: Throws an exception
  • __cxa_get_exception_ptr: Used to get the pointer to an exception in catch blocks
  • __cxa_begin_catch: does bookkeeping
  • __cxa_end_catch: More bookkeeping
  • __cxa_rethrow: Used when rethrowing an exception.

Many of the functions have either been already covered or are fairly self-explanatory. __cxa_throw and __cxa_rethrow mostly wrap libunwind, but also do bookkeeping such as incrementing the reference counter.

Data Structures

To explain the bookkeeping done by the C++ exception runtime, it’s best to look at the __cxa_exception type found in unwind-cxx.h.

struct __cxa_exception
{
  std::type_info *exceptionType;
  void (_GLIBCXX_CDTOR_CALLABI *exceptionDestructor)(void *);

  std::terminate_handler unexpectedHandler;
  std::terminate_handler terminateHandler;

  __cxa_exception *nextException;

  int handlerCount;

  // Cache parsed handler data from the personality routine Phase 1
  // for Phase 2 and __cxa_call_unexpected.
  int handlerSwitchValue;
  const unsigned char *actionRecord;
  const unsigned char *languageSpecificData;
  _Unwind_Ptr catchTemp;
  void *adjustedPtr;

  _Unwind_Exception unwindHeader;
};

The first two parameters, exceptionType and exceptionDestructor, contain the exception type and destructor, which is passed in __cxa_throw. The terminate and unexpected handler are from std::set_unexpected and std::set_terminate, which have to be stored per exception because if an exception terminates, it must terminate with the handler that was active when it was thrown. I don’t know why that is either, but the standard mandates it.

nextException is a pointer to the next thrown exception, if there were more. Again, this is because multiple exceptions can be in flight at once. handlerCount is an integer because for some reason nested catch blocks exist.

handlerSwitchValue is apparently used in __cxa_call_unexpected, although nobody used exception specifications anyways, so it’s irrelevant. Similarly, I couldn’t find anything regarding the purpose of actionRecord, and it seems to be simply an artifact of previous code.

languageSpecificData is a pointer to the LSDA area, which contains information on handlers in a function. In this case, it appears to be actually unused, similarly to actionRecord.

catchTemp is a temporary value used in the personality function, and is purely used to hold information between Phase 1 and Phase 2. adjustedPtr refers to the thrown type, and also appears to be unused.

Finally, the unwindHeader is a struct from libunwind that contains minor bookkeeping for libunwind that changes control flow. Much of the control flow of libunwind has already been discussed.

Book Keeping

Much of the complexity of the book-keeping has to do with the presence of nested exception handlers. __cxa_throw adds an exception onto the linked list of exceptions and increments a global counter of flying exceptions. __cxa_begin_catch decrements the counter of flying exceptions, but increments the handlerCount on the caught exception. __cxa_end_catch decrements the handlerCount, and if it goes to zero frees the exception too. __cxa_rethrow makes the handlerCount negative to mark if it was rethrown, which makes __cxa_end_catch keep the exception alive instead of freeing it.

Conclusions

The C++ exception runtime is a tad complex, but only because the rules around exceptions are kind of weird. To be honest, I have never seen a nested exception handler. Multiple exceptions flying about by itself is somewhat of an edge case, as it would imply that a destructor threw (very rare) or a catch statement threw (also very rare).