C++ exception handling ABI

I wrote an article a few weeks ago to introduce stack unwinding in detail. Today I will introduce C++ exception handling, an application of stack unwinding. Exception handling has a variety of ABI (interoperability of C++ implementations), the most widely used of which is Itanium C++ ABI: Exception Handling

Itanium C++ ABI: Exception Handling

Simplified exception handling process (from throw to catch):

Note: each stack frame may use a different personality routine. It is common that many may share the same routine, though.

Among these steps, _Unwind_RaiseException is responsible for stack unwinding and is language independent. The language-related concepts (catch block, out-of-scope variable) in stack unwinding are interpreted/encapsulated by the personality. This is a key idea that makes the ABI applicable to other languages and allows other languages to be mixed with C++.

Therefore, Itanium C++ ABI: Exception Handling is divided into Level 1 Base ABI and Level 2 C++ ABI. Base ABI describes the language-independent stack unwinding part and defines the _Unwind_* API. Common implementations are:

The C++ ABI is related to the C++ language and defines the __cxa_* API (__cxa_allocate_exception, __cxa_throw, __cxa_begin_catch, etc.). Common implementations are:

The C++ standard library implementation in llvm-project, libc++, can leverage libc++abi, libcxxrt or libsupc++, but libc++abi is recommended.

Level 1 Base ABI

Data structures

The main data structure is:

// Level 1
struct _Unwind_Exception {
  _Unwind_Exception_Class exception_class; // an identifier, used to tell whether the exception is native
  _Unwind_Exception_Cleanup_Fn exception_cleanup;
  _Unwind_Word private_1; // zero: normal unwind; non-zero: forced unwind, the _Unwind_Stop_Fn function
  _Unwind_Word private_2; // saved stack pointer
} __attribute__((aligned));

exception_class and exception_cleanup are set by the API that throws exceptions in Level 2. The Level 1 API does not process exception_class, but passes it to the personality routine. Personality routines use this value to distinguish native and foreign exceptions.

libc++abi __cxa_throw will set exception_class to uint64_t representing "CLNGC++\0". libsupc++ uses uint64_t which means "GNUCC++\0". The ABI requires that the lower bits contain "C++\0". The exceptions thrown by libstdc++ will be treated as foreign exceptions by libc++abi. Only catch (...) can catch foreign exceptions.

Exception propagation implementation mechanism will use another exception_class identifier to represent dependent exceptions.

exception_cleanup stores the destroying delete function of this exception object, which is used by __cxa_end_catch to destroy a foreign exception.

The private unwinder state (private_1 and private_2) in an exception object should be neither read by nor written to by personality routines or other parts of the language-specific runtime.

The information required for the Unwind operation (for a given IP/SP, how to obtain the register information such as the IP/SP of the upper stack frame) is implementation-dependent, and Level 1 ABI does not define it. In the ELF system, .eh_frame and .eh_frame_hdr (PT_EH_FRAME program header) store unwind information. See Stack unwinding.

Level 1 API

_Unwind_Reason_Code _Unwind_RaiseException(_Unwind_Exception *obj); Perform stack unwinding for exceptions. It is noreturn under normal circumstances, and will give control to matched catch handlers (catch block) or non-catch handlers (code blocks that need to execute destructors) like longjmp. It is a two-phase process, divided into phase 1 (search phase) and phase 2 (cleanup phase).

The point of the two-phase process is to avoid any actual stack unwinding if there is no handler. If there are just cleanup frames, an abort function can be called. Cleanup frames are also less expensive than matching a handler. However, parsing .gcc_except_table is probably not much less expensive than additionally matching a handler:)

static _Unwind_Reason_Code unwind_phase1(unw_context_t *uc, _Unwind_Context *ctx,
                                         _Unwind_Exception *obj) {
  // Search phase: unwind and call personality with _UA_SEARCH_PHASE for each frame
  // until a handler (catch block) is found.
  unw_init_local(uc, ctx);
  for(;;) {
    if (ctx->fdeMissing) return _URC_END_OF_STACK;
    if (!step(ctx)) return _URC_FATAL_PHASE1_ERROR;
    ctx->getFdeAndCieFromIP();
    if (!ctx->personality) continue;
    switch (ctx->personality(1, _UA_SEARCH_PHASE, obj->exception_class, obj, ctx)) {
    case _URC_CONTINUE_UNWIND: break;
    case _URC_HANDLER_FOUND:
      unw_get_reg(ctx, UNW_REG_SP, &obj->private_2);
      return _URC_NO_REASON;
    default: return _URC_FATAL_PHASE1_ERROR; // e.g. stack corruption
    }
  }
  return _URC_NO_REASON;
}

static _Unwind_Reason_Code unwind_phase2(unw_context_t *uc, _Unwind_Context *ctx,
                                         _Unwind_Exception *obj) {
  // Cleanup phase: unwind and call personality with _UA_CLEANUP_PHASE for each frame
  // until reaching the handler. Restore the register state and transfer control.
  unw_init_local(uc, ctx);
  for(;;) {
    if (ctx->fdeMissing) return _URC_END_OF_STACK;
    if (!step(ctx)) return _URC_FATAL_PHASE2_ERROR;
    ctx->getFdeAndCieFromIP();
    if (!ctx->personality) continue;
    _Unwind_Action actions = _UA_CLEANUP_PHASE;
    size_t sp;
    unw_get_reg(ctx, UNW_REG_SP, &sp);
    if (sp == obj->private_2) actions |= _UA_HANDLER_FRAME;
    switch (ctx->personality(1, actions, obj->exception_class, obj, ctx)) {
    case _URC_CONTINUE_UNWIND:
      break;
    case _URC_INSTALL_CONTEXT:
      unw_resume(ctx); // Return if there is an error
      return _URC_FATAL_PHASE2_ERROR;
    default: return _URC_FATAL_PHASE2_ERROR; // Unknown result code
    }
  }
  return _URC_FATAL_PHASE2_ERROR;
}

_Unwind_Reason_Code _Unwind_RaiseException(_Unwind_Exception *obj) {
  unw_context_t uc;
  _Unwind_Context ctx;
  __unw_getcontext(&uc);
  _Unwind_Reason_Code phase1 = unwind_phase1(&uc, &ctx, obj);
  if (phase1 != _URC_NO_REASON) return phase1;
  unwind_phase2(&uc, &ctx, obj);
}

C++ does not support resumptive exception handling (correcting the exceptional condition and resuming execution at the point where it was raised), so the two-phase process is not necessary, but two-phase allows C++ and other languages to coexist on the call stack.

_Unwind_Reason_Code _Unwind_ForcedUnwind(_Unwind_Exception *obj, _Unwind_Stop_Fn stop, void *stop_parameter); Execute forced unwinding: Skip the search phase and perform a slightly different cleanup phase. private_2 is used as the parameter of the stop function. It is similar to a foreign exception but is rarely used.

void _Unwind_Resume(_Unwind_Exception *obj); Continue the unwind process of phase 2. It is similar to longjmp, is noreturn, and is the only Level 1 API that is directly called by the compiler. The compiler usually calls this function at the end of non-catch handlers.

void _Unwind_DeleteException(_Unwind_Exception *obj); Destroy the specified exception object. It is the only Level 1 API that handles exception_cleanup and is called by __cxa_end_catch.

Many implementations provide extensions. Notably _Unwind_Reason_Code _Unwind_Backtrace(_Unwind_Trace_Fn callback, void *ref); is another special unwind process: it ignores personality and notifies an external callback of stack frame information.

Level 2 C++ ABI

This part deals with language-related concepts such as throw, catch blocks, and out-of-scope variable destructors in C++.

Data structures

Each thread has a global exception stack, caughtExceptions stores the top (latest) exception on the stack, and __cxa_exception::nextException points to the next exception in the stack.

struct __cxa_eh_globals {
  __cxa_exception *caughtExceptions;
  unsigned uncaughtExceptions;
};

The definition of __cxa_exception is as follows, and the end of it stores the _Unwind_Exception defined by Base ABI. __cxa_exception adds C++ semantic information on the basis of _Unwind_Exception.

// Level 2
struct __cxa_exception {
  void *reserve; // here on 64-bit platforms
  size_t referenceCount; // here on 64-bit platforms
  std::type_info *exceptionType;
  void (*exceptionDestructor)(void *);
  unexpected_handler unexpectedHandler; // by default std::get_unexpected()
  terminate_handler terminateHandler; // by default std::get_terminate()
  __cxa_exception *nextException; // linked to the next exception on the thread stack
  int handlerCount; // incremented in __cxa_begin_catch, decremented in __cxa_end_catch, negated in __cxa_rethrow; last non-dependent performs the clean

  // The following fields cache information the catch handler found in phase 1.
  int handlerSwitchValue; // ttypeIndex in libc++abi
  const char *actionRecord;
  const char *languageSpecificData;
  void *catchTemp; // landingPad
  void *adjustedPtr; // adjusted pointer of the exception object

  _Unwind_Exception unwindHeader;
};

The information needed to process the exception (for a given IP, whether it is in a try catch, whether there are out-of-scope variable destructors that need to be executed, whether there is a dynamic exception specification) is called language-specific data area (LSDA), which is the implementation Related, Level 2 ABI is not defined.

Landing pad

A landing pad is a section of code related to exceptions in the text section which performs one of the three tasks:

If a try block has multiple catch clauses, there will be multiple action table entries in series in the language-specific data area, but the landing pad includes all (conceptually merged) catch clauses. Before the personality transfers control to the landing pad, it will call _Unwind_SetGP to set __buitin_eh_return_data_regno(1) to store switchValue and inform the landing pad which type matches.

A rethrow is triggered by __cxa_rethrow in the middle of the execution of the catch code. It needs to destruct the local variables defined by the catch clause and call __cxa_end_catch to offset the __cxa_begin_catch called at the beginning of the catch clause.

.gcc_except_table

The language-specific data area on the ELF platforms is usually stored in the .gcc_except_table section. This section is parsed by __gxx_personality_v0 and __gcc_personality_v0. Its structure is very simple:

Here is an example:

  .section        .gcc_except_table,"a",@progbits
  .p2align        2
GCC_except_table0:
.Lexception0:
  .byte   255                      # @LPStart Encoding = omit
  .byte   3                        # @TType Encoding = udata4
  .uleb128 .Lttbase0-.Lttbaseref0  # The start of action records
.Lttbaseref0:
  .byte   1                        # Call site Encoding = uleb128
  .uleb128 .Lcst_end0-.Lcst_begin0
.Lcst_begin0:                      # 2 call site code ranges
  .uleb128 .Ltmp0-.Lfunc_begin0    # >> Call Site 1 <<
  .uleb128 .Ltmp1-.Ltmp0           #   Call between .Ltmp0 and .Ltmp1
  .uleb128 .Ltmp2-.Lfunc_begin0    #     jumps to .Ltmp2
  .byte   1                        #   On action: 1
  .uleb128 .Ltmp1-.Lfunc_begin0    # >> Call Site 2 <<
  .uleb128 .Lfunc_end0-.Ltmp1      #   Call between .Ltmp1 and .Lfunc_end0
  .byte   0                        #     has no landing pad
  .byte   0                        #   On action: cleanup
.Lcst_end0:
  .byte   1                        # >> Action Record 1 <<
                                   #   Catch TypeInfo 1
  .byte   0                                #   No further actions
  .p2align        2
                                   # >> Catch TypeInfos <<
  .long   _ZTIi                           # TypeInfo 1

Each call site record has two values besides call site offset and length: landing pad offset and action record offset.

Each action record has two values:

The offset to next action record can be used not only as a singly linked list, but also as a trie, but it is rare such compression can find its usage in the wild.

The values of landing pad offset/action record offset corresponding to different areas in the program:

Level 2 API

void *__cxa_allocate_exception(size_t thrown_size);. The compiler generates a call to this function for throw A(); and allocates a section of memory to store __cxa_exception and A object. __cxa_exception is immediately to the left of A object. The following function illustrates the relationship between the address of the exception object operated by the program and __cxa_exception:

static void *thrown_object_from_cxa_exception(__cxa_exception *exception_header) {
  return static_cast<void *>(exception_header + 1);
}

void __cxa_throw(void *thrown, std::type_info *tinfo, void (*destructor)(void *)); Call the above function to find the __cxa_exception header, and fill in each field (referenceCount, exception_class, unexpectedHandler, terminateHandler, exceptionType, exceptionDestructor, unwindHeader.exception_cleanup) and then call _Unwind_RaiseException. This function is noreturn.

void *__cxa_begin_catch(void *obj); The compiler generates a call to this function at the beginning of the catch block. For a native exception,

For a foreign exception (there is not necessarily a __cxa_exception header),

Simplified implementation:

void __cxa_throw(void *thrown, std::type_info *tinfo, void (*destructor)(void *)) {
  __cxa_exception *hdr = (__cxa_exception *)thrown - 1;
  hdr->exceptionType = tinfo; hdr->destructor = destructor;
  hdr->unexpectedHandler = std::get_unexpected();
  hdr->terminateHandler = std::get_terminate();
  hdr->unwindHeader.exception_class = ...;
  __cxa_get_globals()->uncaughtExceptions++;
  _Unwind_RaiseException(&hdr->unwindHeader);
  // Failed to unwind, e.g. the .eh_frame FDE is absent.
  __cxa_begin_catch(&hdr->unwindHeader);
  std::terminate();
}

void __cxa_end_catch(); is called at the end of the catch block or when rethrow. For native exception:

For a foreign exception,

void __cxa_rethrow(); will mark the exception object, so that when handlerCount is reduced to 0 by __cxa_end_catch, it will not be destroyed, because this object will be reused by the cleanup phase restored by _Unwind_Resume.

Note that, except for __cxa_begin_catch and __cxa_end_catch, most __cxa_* functions cannot handle foreign exceptions (they do not have the __cxa_exception header).

Examples

For the following code:

#include <stdio.h>
struct A { ~A(); };
struct B { ~B(); };
void foo() { throw 0xB612; }
void bar() { B b; foo(); }
void qux() { try { A a; bar(); } catch (int x) { puts(""); } }

The compiled assembly conceptually looks like this:

void foo() {
  __cxa_exception *thrown = __cxa_allocate_exception(4);
  *thrown = 42;
  __cxa_throw(thrown, &typeid(int), /*destructor=*/nullptr);
}
void bar() {
  B b; foo(); return;
  landing_pad: b.~B(); _Unwind_Resume();
}
void qux() {
  A a; bar(); return;
  landing_pad: __cxa_begin_catch(obj); puts(""); __cxa_end_catch(obj);
}

Control flow:

Next, _Unwind_RaiseException drives the two-phase process of Level 1.

__gxx_personality_v0

A personality routine is called by Level 1 phase 1 and phase 2 to provide language-related processing. Different languages, implementations or architectures may use different personality routines. Common personalities are as follows:

The most common C++ implementation on ELF systems is __gxx_personality_v0. It is implemented by:

_Unwind_Reason_Code (*__personality_routine)(int version, _Unwind_Action action, uint64 exceptionClass, _Unwind_Exception *exceptionObject, _Unwind_Context *context);

In the absence of errors:

Before transferring control to the landing pad, the personality will call _Unwind_SetGP to set two registers (architecture related, __buitin_eh_return_data_regno(0) and __buitin_eh_return_data_regno(1)) to store _Unwind_Exception * and switchValue.

Code:

_unwind_Reason_Code __gxx_personality_v0(int version, _Unwind_Action actions, uint64_t exceptionClass, _Unwind_Exception *exc, _Unwind_Context *ctx) {
  if (actions == (_UA_CLEANUP_PHASE | _UA_HANDLER_FRAME) && is_native) {
    auto *hdr = (__cxa_exception *)(exc+1) - 1;
    // Load cached results from phase 1.
    results.switchValue = hdr->handlerSwitchValue;
    results.actionRecord = hdr->actionRecord;
    results.languageSpecificData = hdr->languageSpecificData;
    results.landingPad = reinterpret_cast<uintptr_t>(hdr->catchTemp);
    results.adjustedPtr = hdr->adjustedPtr;

    _Unwind_SetGR(...);
    _Unwind_SetGR(...);
    _Unwind_SetIP(ctx, res.landingPad);
    return _URC_INSTALL_CONTEXT;
  }
  scan_eh_tab(results, actions, native_exception, unwind_exception, context);
  if (results.reason == _URC_CONTINUE_UNWIND ||
      results.reason == _URC_FATAL_PHASE1_ERROR)
    return results.reason;
  if (actions & _UA_SEARCH_PHASE) {
    auto *hdr = (__cxa_exception *)(exc+1) - 1;
    // Cache LSDA results in hdr.
    hdr->handlerSwitchValue = results.switchValue;
    hdr->actionRecord = results.actionRecord;
    hdr->languageSpecificData = results.languageSpecificData;
    hdr->catchTemp = reinterpret_cast<void *>(results.landingPad);
    hdr->adjustedPtr = results.adjustedPtr;
    return _URC_HANDLER_FOUND;
  }
  // _UA_CLEANUP_PHASE
  _Unwind_SetGR(...);
  _Unwind_SetGR(...);
  _Unwind_SetIP(ctx, res.landingPad);
  return _URC_INSTALL_CONTEXT;
}

For a native exception, when the personality returns _URC_HANDLER_FOUND in the search phase, the LSDA related information of the stack frame will be cached. When the personality is called again in the cleanup phase with the argument actions == (_UA_CLEANUP_PHASE | _UA_HANDLER_FRAME), the personality loads the cache and there is no need to parse .gcc_except_table.

In the remaining three cases, the personality has to parse .gcc_except_table:

static void scan_eh_tab(...) {
  ...
  const uint8_t *lsda = (const uint8_t *)_Unwind_GetLanguageSpecificData(context);
  if (lsda == nullptr) { res.reason = _URC_CONTINUE_UNWIND; return; }
  res.languageSpecificData = lsda;
  uintptr_t ipOffset = _Unwind_GetIP(context) - 1 - _Unwind_GetRegionStart(context);
  for each call site entry {
    if (!(start <= ipOffset && ipOffset < start + length))
      continue;
    res.landingPad = landingPad;
    if (landingPad == 0) { res.reason = _URC_CONTINUE_UNWIND; return; }
    if (actionRecord == 0) { // cleanup
      res.reason = actions & _UA_SEARCH_PHASE ? _URC_CONTINUE_UNWIND : _URC_HANDLER_FOUND;
      return;
    }
    // A catch or a dynamic exception specification.
    const uint8_t *action = actionTableStart + (actionRecord - 1);
    bool hasCleanup = false;
    for(;;) {
      res.actionRecord = action;
      int64_t switchValue = readSLEB128(&action);
      if (switchValue > 0) { // catch
        auto *catchType = ...;
        if (catchType == nullptr) { // catch (...)
          res.switchValue = switchValue; res.actionRecord = action;
          res.adjustedPtr = getThrownObjectPtr(exc); res.reason = _URC_HANDLER_FOUND;
          return;
        } else if (is_native) { // catch (T ...)
          auto *hdr = (__cxa_exception *)(exc+1) - 1;
          if (catchType->can_catch(hdr->exceptionType, adjustedPtr)) {
            res.switchValue = switchValue; res.actionRecord = action;
            res.adjustedPtr = adjustedPtr; res.reason = _URC_HANDLER_FOUND;
            return;
          }
        }
      } else if (switchValue < 0) { // dynamic exception specification
        if (actions & _UA_FORCE_UNWIND) {
          // Skip if forced unwinding.
        } else if (is_native) {
          if (!exception_spec_can_catch) {
            // The landing pad will call __cxa_call_unexpected.
            assert(actions & _UA_SEARCH_PHASE);
            res.switchValue = switchValue; res.actionRecord = action;
            res.adjustedPtr = adjustedPtr; res.reason = _URC_HANDLER_FOUND;
            return;
          }
        } else {
          // A foreign exception cannot be matched by the exception specification. The landing pad will call __cxa_call_unexpected.
          res.switchValue = switchValue; res.actionRecord = action;
          res.adjustedPtr = getThrownObjectPtr(exc); res.reason = _URC_HANDLER_FOUND;
          return;
        }
      } else { // switchValue == 0: cleanup
        hasCleanup = true;
      }
      const uint8_t *temp = action;
      int64_t actionOffset = readSLEB128(&temp);
      if (actionOffset == 0) { // End of action list
        res.reason = hasCleanup && actions & _UA_CLEANUP_PHASE
          ? _URC_HANDLER_FOUND : _URC_CONTINUE_UNWIND;
        return;
      }
      action += actionOffset;
    }
  }
  call_terminate();
}

`_gccpersonality_v0’

libgcc and compiler-rt/lib/builtins implement this function to handle __attribute__((cleanup(...))). The implementation does not return _URC_HANDLER_FOUND in the search phase, so the cleanup handler cannot serve as a catch handler. However, we can supply our own implementation to return _URC_HANDLER_FOUND in the search phase… On x86-64, __buitin_eh_return_data_regno(0) is RAX. We can let the cleanup handler pass RAX to the landing pad.

// a.cc
#include <exception>
#include <stdio.h>

extern "C" void my_catch();
extern "C" void throw_exception() { throw 42; }

int main() {
  fprintf(stderr, "uncaught exceptions: %d\n", std::uncaught_exceptions());
  my_catch();
  fprintf(stderr, "uncaught exceptions: %d\n", std::uncaught_exceptions());
}
// b.c
#include <setjmp.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <unwind.h>

void throw_exception();

struct __cxa_eh_globals {
  struct __cxa_exception *caughtExceptions;
  unsigned uncaughtExceptions;
};

struct __cxa_eh_globals *__cxa_get_globals();

static uintptr_t readULEB128(const uint8_t **a) {
  uintptr_t res = 0, shift = 0;
  const uint8_t *p = *a;
  uint8_t b;
  do {
    b = *p++;
    res |= (b & 0x7f) << shift;
    shift += 7;
  } while (b & 0x80);
  *a = p;
  return res;
}

_Unwind_Reason_Code __gcc_personality_v0(int version, _Unwind_Action actions,
                                         uint64_t exception_class,
                                         struct _Unwind_Exception *obj,
                                         struct _Unwind_Context *ctx) {
  const uint8_t *lsda = _Unwind_GetLanguageSpecificData(ctx);
  if (lsda == 0)
    return _URC_CONTINUE_UNWIND;
  uintptr_t func = _Unwind_GetRegionStart(ctx);
  uintptr_t pc = _Unwind_GetIP(ctx) - 1 - func;
  if (*lsda++ != 255) // Skip LPStart
    readULEB128(&lsda);
  if (*lsda++ != 255) // Skip TType
    readULEB128(&lsda);
  uintptr_t call_site_table_len = 0;
  if (*lsda++ == 1)
    call_site_table_len = readULEB128(&lsda);
  const uint8_t *end = lsda + call_site_table_len;
  while (lsda < end) {
    uintptr_t start = readULEB128(&lsda), len = readULEB128(&lsda),
              lpad = readULEB128(&lsda);
    if (!(start <= pc && pc < start + len))
      continue;
    if (lpad == 0)
      return _URC_CONTINUE_UNWIND;
    if (actions & _UA_SEARCH_PHASE)
      return _URC_HANDLER_FOUND;
    _Unwind_SetGR(ctx, __builtin_eh_return_data_regno(0), (uintptr_t)obj);
    _Unwind_SetGR(ctx, __builtin_eh_return_data_regno(1), 0); // switchValue==0
    _Unwind_SetIP(ctx, func + lpad);
    return _URC_INSTALL_CONTEXT;
  }
  return _URC_FATAL_PHASE2_ERROR;
}

struct Catch {
  struct _Unwind_Exception *obj;
  jmp_buf env;
  bool do_catch;
};

__attribute__((used))
static void my_jump(struct Catch *c) {
  if (c->do_catch) {
    struct __cxa_eh_globals *globals = __cxa_get_globals();
    globals->uncaughtExceptions--;
    longjmp(c->env, 1);
  }
}

__attribute__((naked)) static void my_cleanup(struct Catch *c) {
  asm("movq %rax, (%rdi); jmp my_jump");
}

void my_catch() {
  __attribute__((cleanup(my_cleanup))) struct Catch c;
  if (setjmp(c.env) == 0) {
    c.do_catch = 1;
    throw_exception();
  } else {
    fprintf(stderr, "caught exception: %p\n", c.obj);
    fprintf(stderr, "value: %d\n", *(int *)(c.obj + 1));
    c.do_catch = 0;
  }
}
% clang -c -fexceptions a.cc b.c
% clang++ a.o b.o
% ./a.out
uncaught exceptions: 0
caught exception: 0x10f7f10
value: 42
uncaught exceptions: 0

Rethrow

The landing pad section briefly described the code executed by rethrow. Usually caught exception will be destroyed in __cxa_end_catch, so __cxa_rethrow will mark the exception object and increase handlerCount.

C++11 introduced Exception Propagation (N2179; std::rethrow_exception etc), and libstdc++ uses __cxa_dependent_exception to achieve. For design see https://gcc.gnu.org/legacy-ml/libstdc++/2008-05/msg00079.html

struct __cxa_dependent_exception {
  void *reserve;
  void *primaryException;
};

std::current_exception and std::rethrow_exception will increase the reference count.

In libstdc++, __cxa_rethrow calls GCC extension _Unwind_Resume_or_Rethrow which can resume forced unwinding.

LLVM IR

In construction.

if uwtables
  if nounwind
    CantUnwind
  else
    Unwind Table
else
  do nothing

Compiler behavior

When an exception propagates from a function to its caller:

Combined with the above description, when an exception will propagate to a caller of a noexcept function:

Misc

Use libc++ and libc++abi

On Linux, compared with clang, clang++ additionally links against libstdc++/libc++ and libm.

Dynamically link against libc++.so (which depends on libc++abi.so) (additionally specify -pthread if threads are used):

clang++ -stdlib=libc++ -nostdlib++ a.cc -lc++ -lc++abi
# clang -stdlib=libc++ a.cc -lc++ -lc++abi does not pass -lm to the linker.

If compile actions and link actions are separate (-stdlib=libc++ passes -lc++ but its position is undesired, so just don’t use it):

clang++ -nostdlib++ a.cc -lc++ -lc++abi

Statically link in libc++.a (which includes the members of libc++abi.a). This requires a -DLIBCXX_ENABLE_STATIC_ABI_LIBRARY=on build:

clang++ -stdlib=libc++ -static-libstdc++ -nostdlib++ a.cc -pthread

Statically link in libc++.a and libc++abi.a. This is a bit inferior because there is a duplicate -lc++ passed by the driver.

clang++ -stdlib=libc++ -static-libstdc++ -nostdlib++ a.cc -Wl,--push-state,-Bstatic -lc++ -lc++abi -Wl,--pop-state -pthread

libc++abi and libsupc++

It is worth noting that the <exception> <stdexcept> type layout provided by libc++abi (such as logic_error, runtime_error, etc.) are specifically compatible with libsupc++. After GCC 5 libstdc++ abandoned ref-counted std::string, libsupc++ still uses __cow_string for logic_error and other exception classes. libc++abi uses a similar ref-counted string.

libsupc++ and libc++abi do not use inline namespace and have conflicting symbol names. Therefore, usually a libc++/libc++abi application cannot use a shared object (ODR violation) of a dynamically linked libstdc++.so.

If you make some efforts, you can still solve this problem: compile the non-libsupc++ part of libstdc++ to get self-made libstdc++.so.6. The executable file link libc++abi provides the C++ ABI symbols required by libstdc++.so.6.

Monolithic .gcc_except_table

Prior to Clang 12, a monolithic .gcc_except_table was used. Like many other metadata sections, the main problem with the monolithic sections is that they cannot be garbage collected by the linker. For RISC-V -mrelax and basic block sections, there is a bigger problem: .gcc_except_table has relocations pointing to text sections local symbols. If the pointed text sections are discarded in the COMDAT group, these relocations will be rejected by the linker (error: relocation refers to a symbol in a discarded section).

`.eh_frame` with monolithic `.gcc_except_table`

monolithic `.gcc_except_table`

The solution is to use fragmented .gcc_except_table(https://reviews.llvm.org/D83655).

fragmented `.gcc_except_table`

But the actual deployment is not that simple :) LLD processes --gc-sections first (it is not clear which .eh_frame pieces are live), and then processes (and garbage collects) .eh_frame.

During --gc-sections, all .eh_frame pieces are live. They will mark all .gcc_except_table.* live. According to the GC rules of the section group, a .gcc_except_table.* will mark other sections (including .text.*) live in the same section group. The result is that .text.* in all section groups cannot be GC, resulting in increased input size.

bad GC with `.gcc_except_table.*`

https://reviews.llvm.org/D91579 fixed this problem: For .eh_frame, do not mark .gcc_except_table in section group.

good GC with `.gcc_except_table.*`

clang -fbasic-block-sections=

This option produces one section for each basic block (more aggressive than -ffunction-sections) for aggressive machine basic block optimizations. There are some challenges integrating LSDA into this framework.

You can either allocate a .gcc_except_table for each basic block section needing LSDA, or let all basic block sections use the same .gcc_except_table. The LLVM implementation chose the latter, which has several advantages:

There is only one LPStart when using the same .gcc_except_table, and it is necessary to ensure that all offsets from landing pads to LPStart can be represented by relocations. Because most architectures do not have a difference relocation type (R_RISCV_SUB*), placing landing pads in the same section is the choice.

Exception handling ABI for the ARM architecture

The overall structure is the same as Itanium C++ ABI: Exception Handling, with some differences in data structure, _Unwind_*, etc.

Stack unwinding contains a few notes.

Compact Exception Tables for MIPS ABIs

In construction.

Use .eh_frame_entry and .gnu_extab to describe.

Design thoughts:

It uses compact unwind descriptors similar to ARM EH. Builtin PR1 means there is no language-dependent data, Builtin PR2 is used for C/C++