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).