C++ Exception Basics
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 (likelibgcc_sor LLVMlibunwind) optimize this using caching strategies.
Overview
1) Overall Flow of Exception Handling
throw$\to$ Compiler-generated code allocates an exception object on the heap and calls__cxa_throw.__cxa_throw$\to$ Calls the Itanium ABI’s_Unwind_RaiseException.- The unwinder reads the DWARF CFI (Unwind Info) at the current stack frame position.
- Phase 1 (Search Phase):
- Calls the personality function for each frame to check: “Can this frame catch this exception?”
- 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.
- Traverses the frames again from the beginning:
- The landing pad executes
__cxa_begin_catch$\to$ Obtains a reference to the exception object. - Upon exiting the catch block,
__cxa_end_catchis 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:
- Determine whether this frame can catch the given exception type.
- Determine whether it is a cleanup-only region.
- 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
libunwindandlibgcc_smitigate 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.
Leave a comment