| // =========================================================================== |
| // 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. |
| extern "C" [[gnu::visibility("default")]] void __cxa_throw( |
| 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(); |
| } |