As it turns out there’s not really a good in depth explanation of how exceptions work. There are disparate explanations of how separate parts of exceptions in C++ work, but not really the whole picture. So I’m writing this down here as a reference to how everything in C++ exceptions work.

Disclaimer

My experiences are exclusively in GCC. Broadly, Clang should follow the same Architecture as GCC, but may vary in implementation details.

Architecture

The architecture of C++ exception handling can be thought of as two parts: C++ specific, and “language independent.” The architecture of exception handling is laid out in the Itanium ABI, specifically the section on C++ exception handling.

When an exception is thrown, the compiler transforms the throw expression as laid out by the abi. First, it calls __cxa_allocate_exception(), which is either a wrapper for malloc or accesses a static buffer of memory, depending on how the compiler compiler configured it. 1 Obviously, this function and all others cannot throw exceptions themselves. The exception is then constructed into the buffer, typically but not necessarily bypassing the copy constructor. Then __cxa_throw() is called with the pointer to the allocated exception, its type_info, and the function pointer to its destructor. __cxa_throw() never returns.

After that, the C++ exception runtime creates the __cxa_exception object, which contains the aforementioned type info and destructor, as well as a variety of other state used in control flow. It then hands control over to the language independent runtime.

The language independent runtime, which I will refer to as libunwind for reasons that shall become readily apparent, has the job of actually unwinding exceptions. The ABI describes many functions, but only three are actually called externally. _Unwind_RaiseException() is called by __cxa_throw() to start the unwinding process. If it was a longjmp instead of an unwind, it might instead decide to call _Unwind_ForcedUnwind(), which is very similar and also irrelevant at the moment. _Unwind_Resume() is called by the personality to resume unwinding. _Unwind_DeleteException is used to cleanup the libUnwind specific exception object. It just calls a function pointer to __gxx_exception_cleanup, which manages the internal reference counting in the exception object.

libUnwind unwinds the stack by interpreting information held in the eh_frame section, which contains the offsets of saved registers as well as the stack usage of a function. This information is used to undo the prologue of a function, in effect uncalling it. But before unwinding anything, the unwinder first needs to check if the language specific runtime needs to do anything. The part of the language specific runtime responsible for this is called the personality function, which is fed data from the Language Specific Data Area (LSDA). The Frame Descriptor Entry (FDE) for the function contains a pointer to the specific LSDA for the function, if there is any. From there, the personality, in the case of C++, checks for destructors and try blocks, and if there are any, jumps to the “landing pad”. (I’ll explain later) If the exception is caught, unwinding just stops. Otherwise, it calls _Unwind_Resume, which passes control back to the unwinder, which loads any saved registers and removes space from the stack.

In reality, the LSDA is actually language independent, and is compiler specific rather than language specific. It contains a list of try blocks and catch blocks. If the LSDA contains no types but only has a single action, then the action is to run destructors. Alternatively, if there are types and actions, then they correspond to catch statements and their caught types. In any case, we have found which destructors and catch block to run here.

This is done by setting the registers to the state they were in the function, then jumping directly into a “landing pad”. The landing pad is a pseudo function which has no prologue, and only takes two register arguments. The arguments are the pointer to the current exception object and an index for which catch block activated. The destructors are run first, then it branches to the catch block. Depending on whether the exception was caught, it simply jumps back to normal control flow, or calls _Unwind_Resume, which resumes exception handling as before.


  1. See --enable-libstdcxx-static-eh-pool in [https://gcc.gnu.org/onlinedocs/libstdc++/manual/configure.html] ↩︎