libcxxabi and libunwind
libc is the high-level C standard library, but the runtime mechanics of C — exception throwing and catching, RTTI / `dynamic_cast`, vtable destructors, thread-local destructors, the typeinfo objects themselves — live in **libcxxabi** and **libunwind**, two LLVM projects that basalt builds and statically links into `libc.so`.
This page covers what each project provides, how exception throwing and catching is wired together, the implications of LIBCXXABI_BAREMETAL and LIBCXXABI_SILENT_TERMINATE, and how __cxa_atexit integrates with basaltc.
libcxxabi
libcxxabi implements the **Itanium C ABI**, which is the cross-platform standard for how C implementations represent and manipulate runtime objects.
Despite the name, the Itanium ABI is used on every modern Unix architecture (x86_64 Linux, aarch64 Linux, FreeBSD, macOS, SaltyOS).
Microsoft Windows uses a different ABI (MSVC); basalt does not need to support that for libc++.
Functions provided by libcxxabi:
| Symbol | Purpose |
|---|---|
|
Initiate a C++ exception throw. Allocates the exception object, fills the type info, and calls |
|
Per- |
|
Re-throw the currently caught exception. |
|
The personality routine called by libunwind during stack unwinding. Decides which |
|
Demangle a mangled C++ name back into source form. Used by debuggers, by |
|
Register destructors for objects with static or thread storage duration. basaltc provides these — see below. |
|
Thread-safe initialization of function-local statics ( |
|
Called when a pure virtual function is invoked through a destroyed object’s vtable. Calls |
|
Implementation of |
|
For every C type, libcxxabi defines `__class_type_info`, `__si_class_type_info` (single inheritance), `__vmi_class_type_info` (virtual / multiple inheritance) instances. These are linked into the user's executable, not into `libc.so`, but the vtables for these classes are in libcxxabi so all binaries share the same identity. |
|
Provided by |
The 18 source files in _cxxabi_srcs (see libc++ Runtime) cover all of the above plus internal helpers like fallback_malloc.cpp (used during exception handling when the heap is unavailable) and cxa_default_handlers.cpp (provides terminate / unexpected defaults).
Why libcxxabi Has RTTI Enabled
In lib/basalt/cpp/meson.build:
_cxx_flags = [..., '-fno-rtti', ...] # libcxx compiles WITHOUT RTTI
_cxxabi_flags = []
foreach f : _cxx_flags
if f != '-fno-rtti' # libcxxabi compiles WITH RTTI
_cxxabi_flags += f
endif
endforeach
The reason: libcxxabi is the RTTI implementation.
It defines the class_type_info hierarchy, the dynamic_cast function, and the typeinfo objects themselves.
Compiling libcxxabi with -fno-rtti would prevent it from emitting the RTTI infrastructure that the rest of the program needs.
libcxx, on the other hand, does not need RTTI internally — it uses templates and concepts to dispatch on types, never dynamic_cast.
Compiling libcxx with -fno-rtti saves a small amount of binary size and makes the build slightly faster.
The result is that user code built with RTTI (-frtti, the default) sees full RTTI support: typeid(x) works, dynamic_cast works, exception type matching works.
User code built without RTTI cannot use these features, but the runtime still has the typeinfo objects available because libcxxabi exported them.
Exception Throwing Flow
When user code does throw MyException(42):
-
The compiler generates a call to
__cxa_allocate_exception(sizeof(MyException)). -
The compiler calls `MyException’s constructor on the allocated buffer.
-
The compiler calls
__cxa_throw(buffer, &typeid(MyException), &MyException::~MyException). -
__cxa_throw(in libcxxabi) initializes the exception header, then calls_Unwind_RaiseException(in libunwind). -
_Unwind_RaiseExceptionwalks the call stack frame by frame using the DWARF CFI tables, calling the personality routine (__cxa_personality_v0) for each frame. -
The personality routine decides whether the current frame has a matching
catchblock. If yes, it tells libunwind to land at that block. If no, it tells libunwind to keep unwinding. -
When a matching catch is found, libunwind restores the registers to the catch block’s landing pad and resumes execution there.
-
The catch block calls
__cxa_begin_catchto record that the exception is being handled. -
After the catch block runs (with the exception object accessible via the parameter), it calls
__cxa_end_catchto release the exception. -
If
__cxa_end_catchis the last user, it calls the exception’s destructor and frees the buffer.
If no catch is found, the personality routine returns "no handler" for every frame, libunwind reaches the bottom of the stack, and __cxa_throw calls std::terminate(), which by default calls abort().
libunwind
libunwind is the low-level stack walker.
It does not know anything about C++ — it knows about call stacks, function frames, register save/restore, and the DWARF Call Frame Information (CFI) tables that compilers emit alongside object code.
The key entry points:
| Symbol | Purpose |
|---|---|
|
Begin unwinding for a thrown exception. Walks the stack calling the personality routine for each frame. |
|
Resume unwinding after a cleanup landing pad has run. Used to chain through |
|
Personality routine helpers — query and modify the unwind state during a stack walk. |
|
User-callable: walk the stack and call a callback for each frame. Used by stack trace formatters. |
|
Free an exception object after a destructor declines to handle it. |
The DWARF CFI tables are emitted by clang into .eh_frame sections at compile time.
libunwind parses these at runtime when an exception starts unwinding.
basaltc’s linker script discards .eh_frame* from libc.so (/DISCARD/ : { (.eh_frame) }), but the application binary’s link command keeps them — that is where the unwind tables for user code live.
libunwind walks the user’s .eh_frame to find the personality routine for each frame.
dl_iterate_phdr is the discovery mechanism: libunwind calls it to enumerate loaded DSOs and find their .eh_frame sections.
basaltc’s dlfcn.rs provides dl_iterate_phdr (see Dynamic Linking).
libunwind Source Composition
| File | Purpose |
|---|---|
|
Main C++ entry points: |
|
ARM EHABI variant of unwinding (used on aarch32). On aarch64 SaltyOS this is mostly inert; aarch64 uses DWARF. |
|
Low-level unwind primitives in C. Implement |
|
Architecture-specific assembly to restore the register file from a saved unwind context (used at the moment of jumping to a catch block). |
|
Architecture-specific assembly to capture the current register file into an unwind context (used at the start of a stack walk). |
The .S files are the only architecture-specific code in the libunwind build.
basalt builds them with the architecture target flag and includes them in the same libc++.so link as the rest of libcxxabi and libcxx.
Configuration Macros
lib/basalt/cpp/meson.build passes several macros via -D to the libcxxabi compile:
| Macro | Effect |
|---|---|
|
Drop assumptions about a hosted environment: no atexit-replacement initialization, no stdio-based logging, smaller terminate handler. Suitable for freestanding-style targets like SaltyOS. |
|
Make the default |
|
Suppress libcxx’s duplicate definitions of |
|
Standard libc++ macro that exposes internal symbols during the library compile. |
The LIBCXXABI_ENABLE_EXCEPTIONS=OFF mention in the meson.build header comment is misleading: that CMake variable disables building of the exception machinery in libcxxabi, but basalt still builds and uses libunwind, and libcxxabi sources are compiled with exceptions enabled (no -fno-exceptions in _cxxabi_flags). C++ exceptions work end-to-end on basalt.
__cxa_atexit Integration
The C++ ABI defines __cxa_atexit(destructor, arg, dso_handle) for registering destructors of namespace-scope objects (static MyClass instance;) and for thread-local storage with non-trivial destructors.
basaltc — not libcxxabi — provides cxa_atexit (and cxa_finalize).
The implementation is in lib/basalt/c/src/crt.rs.
The reason: __cxa_atexit needs to coordinate with C atexit, and the easiest way to do that is to have one implementation managing both arrays.
libcxxabi’s cxa_thread_atexit.cpp and the various cxa_*.cpp files call __cxa_atexit directly, picking up basaltc’s implementation through the DT_NEEDED libc.so dependency at link time.
See atexit and Process Exit for the C/C++ atexit pool details and the teardown sequence at process exit.
terminate and unexpected
std::terminate() is called when an exception is thrown but not caught, or when an exception escapes a noexcept function, or when the destructor of a thrown exception throws.
basaltc’s libcxxabi configures terminate to call _Exit(SIGABRT + 128) directly because of LIBCXXABI_SILENT_TERMINATE.
std::unexpected() is the (deprecated, removed in C17) handler for dynamic exception specifications.
libcxxabi still provides it for backward compatibility but the compiler does not emit calls to it for C17 and later code.
Conclusion
The C++ runtime stack on basalt is:
user code
↓ throw / catch / dynamic_cast / static initialization
libcxx (templates, containers, iostreams)
↓
libcxxabi (typeinfo, personality, __cxa_throw, RTTI, __cxa_demangle)
↓
libunwind (DWARF CFI walk, register save/restore)
↓
basaltc (__cxa_atexit, malloc, dl_iterate_phdr, abort)
↓
trona / kernite
All four C-runtime components live inside `libc.so`. The application sees a single DT_NEEDED libc++.so and gets the entire pipeline — no separate libcxxabi.so or libunwind.so to manage.
Related Pages
-
libc++ Runtime — composition and source list
-
libc++ Configuration —
__config_sitemacros that pin behavior -
atexit and Process Exit —
__cxa_atexitis in basaltc -
Dynamic Linking —
dl_iterate_phdrfor libunwind