C++ Exception Basics

3 minute read

TL;DR

  • C++ exception handling follows the rules of the Itanium C++ ABI.
  • The happy path incurs zero cost; stack unwinding is performed only when an exception is thrown.
  • Unwinding uses DWARF unwind information (.eh_frame) to trace back stack frames one by one. In each frame, a personality function determines whether to catch the exception or perform cleanup.
  • Exception objects are created on the heap, and their lifetimes are managed via reference counting.
  • During unwinding, only callee-saved registers are restored. Caller-saved registers are reconstructed by the landing pad code if needed.
  • Performance bottlenecks stem from the global loader lock protecting the dynamic library list and the cost of searching .eh_frame. Unwinder implementations (like libgcc_s or LLVM libunwind) optimize this using caching strategies.

Overview

1) Overall Flow of Exception Handling

  1. throw $\to$ Compiler-generated code allocates an exception object on the heap and calls __cxa_throw.
  2. __cxa_throw $\to$ Calls the Itanium ABI’s _Unwind_RaiseException.
  3. The unwinder reads the DWARF CFI (Unwind Info) at the current stack frame position.
  4. Phase 1 (Search Phase):
    • Calls the personality function for each frame to check: “Can this frame catch this exception?”
  5. Phase 2 (Cleanup Phase):
    • Traverses the frames again from the beginning:
      • Calls destructors (cleanup).
      • Rolls back the stack to the catching frame.
    • Jumps to the landing pad upon arrival.
  6. The landing pad executes __cxa_begin_catch $\to$ Obtains a reference to the exception object.
  7. Upon exiting the catch block, __cxa_end_catch is called $\to$ Decrements the reference count and deletes the object if it reaches 0.

2) Guarantees of the Itanium C++ ABI

  • Memory layout of exception objects.
  • Interfaces for __cxa_throw, __cxa_begin_catch, __cxa_end_catch, __gxx_personality_v0, etc.
  • Behaviors and decision rules that the personality function must follow.
  • DWARF-based stack unwinding procedures. $\to$ Ensures the exception model remains compatible across different compilers (GCC/Clang).

3) DWARF Unwind Info (.eh_frame)

  • Metadata recording changes in stack layout for each function and the location of saved registers in the prologue/epilogue.
  • The unwinder parses this to restore the previous frame’s SP, IP, FP, and saved registers.
  • Dedicated to exception handling and distinct from debug info (.debug_info).

4) Personality Function

Acts as the “language-specific handling engine” called for each frame.

  • C++: __gxx_personality_v0
  • Responsibilities:
    1. Determine whether this frame can catch the given exception type.
    2. Determine whether it is a cleanup-only region.
    3. Inform the unwinder of the landing pad’s location (IP).

5) Register Restoration Rules

  • Only callee-saved registers must be restored via DWARF (e.g., x86-64: RBX, RBP, R12~15, etc.).
  • Caller-saved registers are reloaded directly by the landing pad code if needed.
  • Frame-related registers like SP, IP (PC), and FP are always restored by the unwinder.

6) Performance Bottlenecks and Optimizations

  • The dynamic library list is protected by the loader’s global lock (dl loader lock). $\to$ A lock must be acquired when searching for .eh_frame.
  • Why exception costs surge in large programs:
    • Parsing DWARF for each frame.
    • Frequent global lock acquisition.
  • Implementations like LLVM libunwind and libgcc_s mitigate this using:
    • IP-to-CFI caching
    • Function-level or call-site-level caching
    • Backtrace fast path optimization

Example (Conceptual Flow)

Exception thrown $\to$ __cxa_throw
$\to$ _Unwind_RaiseException
$\to$ Check Frame N personality (no match)
$\to$ Check Frame N-1 personality (no match)
$\to$ Check Frame N-2 personality (match found $\to$ Search Phase ends)
$\to$ Cleanup Phase starts (destructors run in frames above)
$\to$ Jump to landing pad IP
$\to$ __cxa_begin_catch
$\to$ Catch block executes
$\to$ __cxa_end_catch $\to$ refcount reaches 0 $\to$ Delete exception object


Takeaways

  • C++ exception handling uses a zero-cost model, meaning there is virtually no cost during execution if no exception is thrown.
  • Unwinding works through the combination of DWARF + Itanium ABI + Personality Function.
  • Exception objects reside on the heap and have their lifetimes managed by reference counts.
  • Distinguishing between caller-saved and callee-saved registers is crucial for unwinding cost optimization.
  • In dynamic library environments, loader lock contention can worsen exception overhead; modern unwinders alleviate this using caching strategies.
  • Different C++ runtimes (GCC/Clang) are interoperable as long as they comply with the Itanium ABI.

References

Tags:

Categories:

Updated:

Leave a comment