| // =========================================================================== | |
| // Overview (plain language) | |
| // =========================================================================== | |
| // Problem: when gbmcweb hits an uncaught C++ exception it crashes, but by the | |
| // time the crash handler runs the stack has already unwound, so the usual | |
| // backtrace points at framework code (asio / sdbusplus re-throwing a stashed | |
| // exception) instead of the line that actually threw. That makes fleet crashes | |
| // very hard to debug. | |
| // | |
| // Trick: every C++ `throw` funnels through one libstdc++ function, __cxa_throw. | |
| // We define our own __cxa_throw in the main binary, so the linker makes the | |
| // whole process (libstdc++ included) call ours. On each throw we snapshot the | |
| // call stack *before* it unwinds, tag it with the exception object's address, | |
| // and stash it in a small fixed ring. If the program later dies from an | |
| // uncaught exception, our std::terminate handler finds that exception's | |
| // snapshot and logs it -- the real throw site. | |
| // | |
| // How the pieces fit: | |
| // * Capture: backtrace() returns a list of raw code addresses ("here, called | |
| // from here, ..."), with no names. | |
| // * Store: a 64-slot circular buffer (the newest throw overwrites the oldest) | |
| // with no memory allocation. Each slot is tagged with the exception's | |
| // address. A lock-free seqlock lets the crash handler read a slot while | |
| // other threads keep throwing, without locks and without ever trusting a | |
| // half-written entry. | |
| // * Report at crash: look up the dying exception's snapshot and print each | |
| // address as "module + hex offset". | |
| // * Decode later: a developer runs the standard `addr2line` tool on the | |
| // matching debug binary to turn "module + offset" into file:line. We do not | |
| // resolve names at crash time -- that needs locks/allocation and the | |
| // process may be corrupt -- so at startup we snapshot once which address | |
| // ranges belong to which loaded binary. | |
| // | |
| // Why so defensive (no allocation, no locks, no re-entry): the handler runs | |
| // when the process may already be corrupt; anything that could block, allocate, | |
| // or crash again would lose the log entirely. | |
| // | |
| // Scope: built only in the gcc/yocto firmware image, since it relies on GNU | |
| // libstdc++ exception internals; other builds compile it to "unavailable". It | |
| // works on x86-64, ARM64 and ARM32 (it uses the standard stack unwinder, not a | |
| // CPU-specific hack, so -fomit-frame-pointer and optimization levels do not | |
| // affect it). | |
| // | |
| // The links below point to the specs and mechanisms this file relies on; the | |
| // rationale now lives as comments next to the code each point describes. | |
| // =========================================================================== | |
| // References (formats and mechanisms this file depends on): | |
| // - Itanium C++ ABI, Exception Handling — __cxa_throw, the __cxa_exception | |
| // header layout, __cxa_current_exception_type, and the exception_ptr / | |
| // primary-object model this code keys on: | |
| // https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html | |
| // - folly exception_tracer — the __cxa_throw interposer this file reduces to a | |
| // single self-contained TU: | |
| // https://github.com/facebook/folly/blob/main/folly/debugging/exception_tracer/ExceptionTracerLib.cpp | |
| // - glibc backtrace(3) — the throw-site stack capture primitive: | |
| // https://man7.org/linux/man-pages/man3/backtrace.3.html | |
| // - dl_iterate_phdr(3) — source of the ELF program-header module table | |
| // snapshotted at install time: | |
| // https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html | |
| // - Seqlock — the lock-free single-writer scheme each ring slot uses: | |
| // https://www.kernel.org/doc/html/latest/locking/seqlock.html | |
| // - Unwind tables consumed by backtrace(): | |
| // DWARF .eh_frame (ARM64/x86-64) — LSB Core, Exception Frames: | |
| // https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html | |
| // ARM32 EHABI .ARM.exidx — Exception Handling ABI for the Arm Arch: | |
| // https://github.com/ARM-software/abi-aa/blob/main/ehabi32/ehabi32.rst | |
| // ARM64 frame-record chain (x29/x30), AAPCS64: | |
| // https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst | |
| #include "exception_stack_tracer.hpp" | |
| #include "exception_stack_tracer_internal.hpp" | |
| #include <cxxabi.h> | |
| #include <dlfcn.h> | |
| #include <execinfo.h> | |
| #include <link.h> | |
| #include <algorithm> | |
| #include <array> | |
| #include <atomic> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <cstdlib> | |
| #include <cstring> | |
| #include <exception> | |
| #include <typeinfo> | |
| #include "absl/base/internal/raw_logging.h" | |
| namespace { | |
| // Frame 0 is the interposer itself, so ~9 caller frames survive; enough to | |
| // clear the __throw_* helper and land in application code. | |
| constexpr int kMaxFrames = 10; | |
| constexpr uint32_t kRingSize = 64; | |
| constexpr size_t kMaxModules = 64; | |
| constexpr size_t kModuleNameSize = 40; | |
| // A captured call stack as written by backtrace(): an array of up to | |
| // kMaxFrames return addresses (code pointers / program counters), innermost | |
| // frame first. Each entry is an opaque instruction address, later resolved to | |
| // module + offset for offline addr2line; it is never dereferenced. | |
| using FrameArray = std::array<void*, kMaxFrames>; | |
| // A throw-site stack copied out of the ring for the terminate handler to print. | |
| struct ThrowRecordSnapshot { | |
| const std::type_info* type = nullptr; | |
| int depth = 0; | |
| FrameArray frames{}; | |
| }; | |
| // --------------------------------------------------------------------------- | |
| // ThrowSiteRing: a lock-free ring of throw-site stacks. | |
| // | |
| // Writers are the throwing threads (via __cxa_throw); the single reader is the | |
| // terminate handler. Each slot is published as a seqlock: `object` is the | |
| // sequence word, stored last with release order and cleared first by a writer, | |
| // so a reader trusts a copy only if `object` matched before and after. | |
| // --------------------------------------------------------------------------- | |
| class ThrowSiteRing { | |
| public: | |
| void setEnabled(bool enable) { | |
| enabled_.store(enable, std::memory_order_relaxed); | |
| } | |
| // Writer, called from __cxa_throw before the unwinder runs: at catch time the | |
| // throw-site frames no longer exist. | |
| void record(void* object, const std::type_info* type) { | |
| if (!enabled_.load(std::memory_order_relaxed)) { | |
| return; | |
| } | |
| // backtrace() writes up to kMaxFrames return addresses into `frames` and | |
| // returns how many it wrote (`depth`), innermost frame first. | |
| FrameArray frames{}; | |
| int depth = backtrace(frames.data(), kMaxFrames); | |
| nextSlot().publish(object, type, frames, depth); | |
| } | |
| // Reader: scan newest-first so that after wraparound a reused object address | |
| // resolves to the live exception's record, not an older evicted throw. | |
| bool lookup(void* object, ThrowRecordSnapshot& out) const { | |
| if (object == nullptr) { | |
| return false; | |
| } | |
| uint32_t end = next_.load(std::memory_order_acquire); | |
| for (uint32_t i = 0; i < kRingSize; ++i) { | |
| const Slot& slot = slots_[(end - 1 - i) % kRingSize]; | |
| switch (slot.tryRead(object, out)) { | |
| case Slot::ReadResult::kNoMatch: | |
| continue; | |
| case Slot::ReadResult::kCopied: | |
| return true; | |
| case Slot::ReadResult::kEvicted: | |
| // The address matched but the slot was rewritten during the copy. | |
| // The caller's exception_ptr pins `object` alive, so no concurrent | |
| // throw can record this address; a rewrite therefore means this slot | |
| // was reused by an unrelated throw and the record is gone. Report it | |
| // evicted rather than matching a stale older same-address slot. | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| // Test-only: re-enable recording and drop all records. | |
| void reset() { | |
| enabled_.store(true, std::memory_order_relaxed); | |
| next_.store(0, std::memory_order_relaxed); | |
| for (Slot& slot : slots_) { | |
| slot.invalidate(); | |
| } | |
| } | |
| private: | |
| class Slot { | |
| public: | |
| enum class ReadResult { kNoMatch, kCopied, kEvicted }; | |
| // Writer half of the seqlock: invalidate, fill the payload, then publish | |
| // `object` last so a concurrent reader sees either the old value, a | |
| // no-match sentinel, or the fully written record — never a torn mix. | |
| void publish(void* object, const std::type_info* type, | |
| const FrameArray& frames, int depth) { | |
| object_.store(nullptr, std::memory_order_relaxed); | |
| // A release *store* only keeps preceding writes above it; it does not stop | |
| // the relaxed payload writes below from being hoisted above the | |
| // invalidation. This release fence is the store-store barrier that keeps | |
| // the object_=nullptr invalidation ordered before the payload writes, so a | |
| // reader still seeing the old object during an eviction can never observe | |
| // a torn payload and revalidate it as kCopied. | |
| std::atomic_thread_fence(std::memory_order_release); | |
| type_.store(type, std::memory_order_relaxed); | |
| depth_.store(depth, std::memory_order_relaxed); | |
| for (size_t i = 0; i < kMaxFrames; ++i) { | |
| frames_[i].store(frames[i], std::memory_order_relaxed); | |
| } | |
| object_.store(object, std::memory_order_release); | |
| } | |
| // Reader half: copy the payload iff the slot holds `object`, then report | |
| // whether the sequence word still matches (kCopied) or was rewritten | |
| // mid-copy (kEvicted). Payload fields are atomic so the mid-copy read that | |
| // kEvicted discards is well defined rather than a data race. | |
| ReadResult tryRead(void* object, ThrowRecordSnapshot& out) const { | |
| if (object_.load(std::memory_order_acquire) != object) { | |
| return ReadResult::kNoMatch; | |
| } | |
| out.type = type_.load(std::memory_order_relaxed); | |
| out.depth = sanitizedDepth(depth_.load(std::memory_order_relaxed)); | |
| for (size_t i = 0; i < kMaxFrames; ++i) { | |
| out.frames[i] = frames_[i].load(std::memory_order_relaxed); | |
| } | |
| // Symmetric to publish()'s release fence. An acquire *load* on object_ | |
| // only orders operations that follow it; it would not stop the relaxed | |
| // payload loads above from being reordered *after* the revalidation, so a | |
| // slot rewritten mid-copy could be revalidated as kCopied over a torn | |
| // payload. This acquire fence is the load-load barrier that keeps the | |
| // payload reads ordered before the revalidation load, so a mid-copy | |
| // rewrite is reliably detected as kEvicted. With the fence in place the | |
| // revalidation load itself only needs to be relaxed. | |
| std::atomic_thread_fence(std::memory_order_acquire); | |
| return object_.load(std::memory_order_relaxed) == object | |
| ? ReadResult::kCopied | |
| : ReadResult::kEvicted; | |
| } | |
| void invalidate() { object_.store(nullptr, std::memory_order_relaxed); } | |
| private: | |
| // A valid frame count is 0..kMaxFrames; anything else can only come from a | |
| // torn read (later rejected by revalidation), so map it to 0 -- show no | |
| // frames rather than a bogus count. | |
| static int sanitizedDepth(int depth) { | |
| return (depth < 0 || depth > kMaxFrames) ? 0 : depth; | |
| } | |
| std::atomic<void*> object_{nullptr}; | |
| std::atomic<const std::type_info*> type_{nullptr}; | |
| std::atomic<int> depth_{0}; | |
| std::array<std::atomic<void*>, kMaxFrames> frames_{}; | |
| }; | |
| Slot& nextSlot() { | |
| return slots_[next_.fetch_add(1, std::memory_order_relaxed) % kRingSize]; | |
| } | |
| // Kill switch (--exception_stack_tracer=false): recording is a no-op and the | |
| // default terminate handler stays installed. | |
| std::atomic<bool> enabled_{true}; | |
| std::atomic<uint32_t> next_{0}; | |
| std::array<Slot, kRingSize> slots_{}; | |
| }; | |
| // --------------------------------------------------------------------------- | |
| // ModuleTable: maps a runtime code address back to (module, in-file offset). | |
| // | |
| // backtrace() gives us raw runtime addresses. To symbolize them offline we need | |
| // two things per address: which binary it came from, and its offset within that | |
| // binary's file (that is what `addr2line -e <file> <offset>` consumes). | |
| // | |
| // Why we keep our own table instead of resolving at crash time: | |
| // The normal way to map an address to its module is dladdr(), but dladdr | |
| // takes the dynamic loader lock and may allocate -- both forbidden in the | |
| // terminate handler, which runs under suspected corruption and must never | |
| // block or allocate. So we walk the loaded modules once at install time, | |
| // while the process is healthy, and cache exactly what we need in a | |
| // fixed-size table the handler can scan with plain array reads: no locks, no | |
| // allocation, no loader calls. This stays correct because bmcweb does not | |
| // dlopen anything after startup, so the module map never changes. | |
| // | |
| // What the table holds: | |
| // One ModuleInfo row per loaded module (the executable and each .so). Each | |
| // row records that module's runtime address range (to test which module a | |
| // frame falls in), its load bias (to convert the frame to an in-file offset), | |
| // and its short name (for the log line) -- everything needed to turn a raw | |
| // address into "module + offset". | |
| // | |
| // ELF background (how a program is laid out in memory, i.e. where those numbers | |
| // come from): | |
| // * A "module" is one ELF file mapped into the process: the main executable | |
| // plus every shared library (.so) it loaded. | |
| // * Each ELF file carries a "program header" table describing how the loader | |
| // should map it. Each entry is a segment; the ones with type PT_LOAD are | |
| // the chunks of code/data actually copied into memory (other types hold | |
| // dynamic-linking metadata, notes, etc., and take up no code space). | |
| // * Addresses inside the file are recorded relative to a link-time base of 0 | |
| // (for position-independent code -- PIEs and all .so files). At load time | |
| // ASLR places the module at a random base address; the loader then shifts | |
| // every address by a fixed amount called the "load bias". | |
| // runtime address = load bias + in-file address | |
| // * So a PT_LOAD segment lives at runtime in | |
| // [ bias + p_vaddr , bias + p_vaddr + p_memsz ) | |
| // where p_vaddr is the segment's in-file address and p_memsz its size in | |
| // memory. Unioning all PT_LOAD segments gives the module's whole runtime | |
| // address range (ModuleInfo::begin/end). | |
| // * To symbolize a frame we reverse the shift: in-file offset = addr - bias. | |
| // | |
| // The snapshot is taken with dl_iterate_phdr (see loadedRange below). | |
| // --------------------------------------------------------------------------- | |
| // One row of the table: a single loaded module (the executable or one .so), and | |
| // everything the terminate handler needs to place and symbolize an address that | |
| // falls inside it. | |
| struct ModuleInfo { | |
| // Load bias (dl_phdr_info::dlpi_addr): the address a position-independent | |
| // object was mapped at. `frame - bias` is the in-file offset addr2line wants. | |
| uintptr_t bias = 0; | |
| // Half-open runtime address range [begin, end) the module occupies, used to | |
| // match a captured frame to its module. | |
| uintptr_t begin = 0; | |
| uintptr_t end = 0; | |
| // Module basename ("exe" for the main program), NUL-terminated. | |
| std::array<char, kModuleNameSize> name{}; | |
| // Copy `n` into the fixed buffer, always leaving it NUL-terminated. | |
| void setName(const char* n) { | |
| std::strncpy(name.data(), n, kModuleNameSize - 1); | |
| name[kModuleNameSize - 1] = '\0'; | |
| } | |
| }; | |
| class ModuleTable { | |
| public: | |
| // Snapshot the currently loaded modules. Call once, at install time. | |
| void snapshot() { dl_iterate_phdr(&phdrCallback, this); } | |
| // Find the module containing `addr`, or nullptr if none does. | |
| const ModuleInfo* resolve(uintptr_t addr) const { | |
| size_t modules = count_.load(std::memory_order_acquire); | |
| for (size_t m = 0; m < modules; ++m) { | |
| if (addr >= modules_[m].begin && addr < modules_[m].end) { | |
| return &modules_[m]; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| void reset() { count_.store(0, std::memory_order_relaxed); } | |
| private: | |
| static int phdrCallback(dl_phdr_info* info, size_t /*size*/, void* self) { | |
| return static_cast<ModuleTable*>(self)->addModule(*info); | |
| } | |
| // A half-open [begin, end) address range, accumulated across PT_LOAD | |
| // segments. Starts empty (begin > end) so min/max fold correctly. | |
| struct AddressRange { | |
| uintptr_t begin = UINTPTR_MAX; | |
| uintptr_t end = 0; | |
| bool empty() const { return end == 0; } | |
| }; | |
| // dl_iterate_phdr hands us one dl_phdr_info per loaded ELF object (the main | |
| // program and each shared library). The fields we use: | |
| // dlpi_addr - load bias: the base address the object was mapped at. | |
| // dlpi_name - the object's file path ("" for the main program). | |
| // dlpi_phdr - array of the object's ELF program headers (ElfW(Phdr), | |
| // dlpi_phnum which is Elf32/Elf64 chosen for the target). | |
| // Each PT_LOAD program header describes a segment actually mapped into | |
| // memory; its runtime span is [dlpi_addr + p_vaddr, + p_memsz). Unioning the | |
| // PT_LOAD segments yields the whole object's occupied address range. | |
| static AddressRange loadedRange(const dl_phdr_info& info) { | |
| AddressRange range; | |
| for (int i = 0; i < info.dlpi_phnum; ++i) { | |
| // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) | |
| const ElfW(Phdr)& phdr = info.dlpi_phdr[i]; | |
| if (phdr.p_type != PT_LOAD) { | |
| continue; // only PT_LOAD segments are mapped into memory | |
| } | |
| uintptr_t segmentBegin = info.dlpi_addr + phdr.p_vaddr; | |
| uintptr_t segmentEnd = segmentBegin + phdr.p_memsz; | |
| range.begin = std::min(range.begin, segmentBegin); | |
| range.end = std::max(range.end, segmentEnd); | |
| } | |
| return range; | |
| } | |
| // Reduce a module path to its basename for compact logging; the main | |
| // program's dlpi_name is empty, so report it as "exe". | |
| static const char* moduleBasename(const char* path) { | |
| if (path == nullptr || path[0] == '\0') { | |
| return "exe"; | |
| } | |
| const char* slash = std::strrchr(path, '/'); | |
| // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) | |
| return slash != nullptr ? slash + 1 : path; | |
| } | |
| // dl_iterate_phdr callback. Returns 1 to stop iteration once the table is | |
| // full, 0 to continue. | |
| int addModule(const dl_phdr_info& info) { | |
| size_t count = count_.load(std::memory_order_relaxed); | |
| if (count >= kMaxModules) { | |
| return 1; | |
| } | |
| AddressRange range = loadedRange(info); | |
| if (range.empty()) { | |
| return 0; // no PT_LOAD segments (e.g. a linker placeholder); skip | |
| } | |
| ModuleInfo& m = modules_[count]; | |
| m.bias = info.dlpi_addr; | |
| m.begin = range.begin; | |
| m.end = range.end; | |
| m.setName(moduleBasename(info.dlpi_name)); | |
| count_.store(count + 1, std::memory_order_release); | |
| return 0; | |
| } | |
| // The snapshot: modules_[0, count_) are the loaded modules captured at | |
| // install time. Fixed capacity so no allocation is ever needed; count_ is | |
| // published with release so resolve() sees fully-written rows. | |
| std::array<ModuleInfo, kMaxModules> modules_{}; | |
| std::atomic<size_t> count_{0}; | |
| }; | |
| // The two process-wide tables. constinit guarantees constant initialization, | |
| // so they are ready before any static constructor (or early throw) runs. | |
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | |
| constinit ThrowSiteRing gThrowRing{}; | |
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | |
| constinit ModuleTable gModuleTable{}; | |
| // Returns the address that identifies the in-flight exception -- the same | |
| // pointer __cxa_throw recorded -- which the handler uses to look up its stack. | |
| // On libstdc++ (GNU), a std::exception_ptr is exactly one pointer to that | |
| // object, so we read it out directly (layout checked by static_assert). On any | |
| // other C++ runtime (e.g. clang/libc++) we don't know the layout, so we return | |
| // nullptr; the handler then just reports "stack unavailable" and everything | |
| // else keeps working. | |
| #if defined(__GLIBCXX__) | |
| void* primaryExceptionObject(const std::exception_ptr& p) { | |
| static_assert(sizeof(std::exception_ptr) == sizeof(void*), | |
| "libstdc++ exception_ptr layout assumption violated"); | |
| void* object = nullptr; | |
| std::memcpy(&object, &p, sizeof(object)); | |
| return object; | |
| } | |
| #else | |
| void* primaryExceptionObject(const std::exception_ptr& /*p*/) { | |
| return nullptr; // unknown exception_ptr layout: lookups report unavailable | |
| } | |
| #endif | |
| void logResolvedFrames(const FrameArray& frames, int depth) { | |
| for (int i = 0; i < depth; ++i) { | |
| // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) | |
| const auto addr = reinterpret_cast<uintptr_t>(frames[static_cast<size_t>(i)]); | |
| const ModuleInfo* module = gModuleTable.resolve(addr); | |
| if (module != nullptr) { | |
| // Module-relative offset: symbolizable offline against the matching | |
| // .debug file (addr2line -e) even for a stripped PIE. | |
| ABSL_RAW_LOG(ERROR, " #%02d %s+0x%lx", i, module->name.data(), | |
| static_cast<unsigned long>(addr - module->bias)); | |
| } else { | |
| ABSL_RAW_LOG(ERROR, " #%02d [0x%lx]", i, | |
| static_cast<unsigned long>(addr)); | |
| } | |
| } | |
| } | |
| [[noreturn]] void terminateWithThrowStack() { | |
| // If anything below terminates again (a throwing what(), a second uncaught | |
| // exception on another thread), die immediately instead of recursing. | |
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | |
| static std::atomic<bool> entered{false}; | |
| if (entered.exchange(true, std::memory_order_relaxed)) { | |
| std::abort(); | |
| } | |
| exception_stack_tracer_internal::logThrowSiteStackOfCurrentException(); | |
| std::abort(); | |
| } | |
| } // namespace | |
| namespace exception_stack_tracer_internal { | |
| void logThrowSiteStackOfCurrentException() { | |
| const std::type_info* type = abi::__cxa_current_exception_type(); | |
| if (type == nullptr) { | |
| ABSL_RAW_LOG(ERROR, "terminate: called without an active exception"); | |
| return; | |
| } | |
| ABSL_RAW_LOG(ERROR, "terminate: uncaught exception of type %s", type->name()); | |
| // __cxa_call_terminate (or a test's active catch) began the catch, so the | |
| // in-flight exception is rethrowable in place. Unlike std::rethrow_exception | |
| // this allocates nothing (same technique as libstdc++'s | |
| // __verbose_terminate_handler). | |
| try { | |
| throw; | |
| } catch (const std::exception& e) { | |
| // A user-defined what() override may return nullptr; passing that to %s is | |
| // undefined behaviour, so fall back to a literal (mirrors the type-name | |
| // guard below). | |
| const char* what = e.what(); | |
| ABSL_RAW_LOG(ERROR, " what(): %s", what != nullptr ? what : "(null)"); | |
| } catch (...) { | |
| } | |
| // For a native exception this only bumps a refcount on the existing object | |
| // — no allocation. It also pins the object for the ring's revalidation. | |
| if (std::exception_ptr eptr = std::current_exception()) { | |
| ThrowRecordSnapshot snapshot; | |
| if (gThrowRing.lookup(primaryExceptionObject(eptr), snapshot)) { | |
| ABSL_RAW_LOG(ERROR, "throw-site stack (recorded as %s):", | |
| snapshot.type != nullptr ? snapshot.type->name() : "?"); | |
| logResolvedFrames(snapshot.frames, snapshot.depth); | |
| } else { | |
| ABSL_RAW_LOG(ERROR, | |
| "throw-site stack unavailable (ring evicted, foreign " | |
| "exception, or non-libstdc++ runtime)"); | |
| } | |
| } | |
| } | |
| void resetForTest() { | |
| gThrowRing.reset(); | |
| gModuleTable.reset(); | |
| } | |
| } // namespace exception_stack_tracer_internal | |
| // Called once from run() at startup. Besides the one-time setup below, this | |
| // explicit call is also what keeps this file in the binary: with no reference to | |
| // it, the linker's --gc-sections would discard the whole translation unit -- and | |
| // our __cxa_throw override along with it. enable=false (the | |
| // --exception_stack_tracer kill switch) leaves the default terminate handler in | |
| // place and turns recording into a no-op. | |
| void installExceptionStackTracer(bool enable) { | |
| if (!enable) { | |
| gThrowRing.setEnabled(false); | |
| return; | |
| } | |
| // Snapshot the module table and force glibc's lazy libgcc unwinder load now | |
| // so the throw path and the terminate path never allocate, dlopen, or take | |
| // the loader lock. | |
| gModuleTable.snapshot(); | |
| std::array<void*, 2> warm{}; | |
| backtrace(warm.data(), 2); | |
| std::set_terminate(terminateWithThrowStack); | |
| } | |
| // Our replacement for the C++ runtime's throw entry point. Every `throw` in the | |
| // process -- including libstdc++'s own __throw_* helpers and throws originating | |
| // in other shared objects -- calls this function, because our definition is | |
| // exported from the main executable (default visibility + a linker | |
| // --dynamic-list entry for __cxa_throw; see meson.build) and the executable is | |
| // searched first in the global symbol scope, so it interposes the library's. | |
| // We record the throw site, then forward to the real __cxa_throw (found via | |
| // dlsym(RTLD_NEXT, ...)) so throwing behaves exactly as normal. | |
| // | |
| // NOTE: exporting this symbol is what makes cross-module capture work. Without | |
| // it (e.g. built LOCAL under -fvisibility=hidden), only throws whose __cxa_throw | |
| // call site is compiled into the executable are recorded; throws executing | |
| // inside libstdc++ or any other .so bind to the library's own __cxa_throw and | |
| // are missed. | |
| // | |
| // The function is named cxaThrowInterposer in C++ and bound to the __cxa_throw | |
| // symbol with an asm label rather than declared as __cxa_throw directly: GCC | |
| // implicitly declares __cxa_throw(void*, void*, void (*)(void*)) at the first | |
| // `throw` it compiles (GCC 15 reaches one through <optional>), and then rejects | |
| // an explicit declaration taking std::type_info* as conflicting. | |
| extern "C" [[gnu::visibility("default")]] void cxaThrowInterposer( | |
| void* thrown_object, std::type_info* tinfo, | |
| void (*dest)(void*)) __asm__("__cxa_throw"); | |
| extern "C" void cxaThrowInterposer(void* thrown_object, std::type_info* tinfo, | |
| void (*dest)(void*)) { | |
| gThrowRing.record(thrown_object, tinfo); | |
| using ThrowFn = void (*)(void*, std::type_info*, void (*)(void*)); | |
| // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) | |
| static const ThrowFn realCxaThrow = | |
| reinterpret_cast<ThrowFn>(dlsym(RTLD_NEXT, "__cxa_throw")); | |
| // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) | |
| realCxaThrow(thrown_object, tinfo, dest); | |
| __builtin_unreachable(); | |
| } |