diff --git a/cxx/lyra/cxa_throw.cpp b/cxx/lyra/cxa_throw.cpp index d3b4477..edc357a 100644 --- a/cxx/lyra/cxa_throw.cpp +++ b/cxx/lyra/cxa_throw.cpp @@ -19,6 +19,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #include @@ -46,30 +49,28 @@ void enableCxaThrowHookBacktraces(bool enable) { enableBacktraces.store(enable, std::memory_order_relaxed); } -[[gnu::noreturn]] __attribute__((annotate("dynamic_fn_ptr"))) void ( - *original_cxa_throw)(void*, const std::type_info*, void (*)(void*)); - // We want to attach stack traces to C++ exceptions. Our API contract is that // calling lyra::getExceptionTrace on the exception_ptr for an exception should // return the stack trace for that exception. // -// We accomplish this by providing a hook for __cxa_throw, which creates an +// We accomplish this by providing a hook for __cxa_init_primary_exception or +// __cxa_throw (depending on the libc++ version), which creates an // ExceptionTraceHolder object (which captures the stack trace for the // exception), and creates a mapping from the pointer to the exception object -// (which is a parameter to __cxa_throw) to its ExceptionTraceHolder object. -// This mapping can then be queried by lyra::getExceptionTrace to get the stack -// trace for the exception. We have a custom exception destructor to destroy the -// trace object and call the original destructor for the exception object, and -// our __cxa_throw hook calls the original __cxa_throw to perform the actual -// exception throwing and passes this custom destructor. +// (which is a function parameter) to its ExceptionTraceHolder object. This +// mapping can then be queried by lyra::getExceptionTrace to get the stack trace +// for the exception. We have a custom exception destructor to destroy the trace +// object and call the original destructor for the exception object, and our +// hook calls the original function for the actual exception functionality and +// passes this custom destructor. // -// This works because __cxa_throw is only called when creating a new exception -// object (that has been freshly allocated via __cxa_allocate_exception), so at -// that point, we're able to capture the original stack trace for the exception. -// Even if that exception is later rethrown, we'll still maintain its original -// stack trace, assuming that std::current_exception creates a reference to the -// current exception instead of copying it (which is true for both libstdc++ and -// libc++), such that we can still look up the exception object via pointer. +// This works because the hooked function is only called when creating a new +// exception object, so at that point, we're able to capture the original stack +// trace for the exception. Even if that exception is later rethrown, we'll +// still maintain its original stack trace, assuming that std::current_exception +// creates a reference to the current exception instead of copying it (which is +// true for both libstdc++ and libc++), such that we can still look up the +// exception object via pointer. // // We don't have to worry about any pointer adjustments for the exception object // (e.g. for converting to or from a base class subobject pointer), because a @@ -122,19 +123,65 @@ void trace_destructor(void* exception_obj) { original_destructor(exception_obj); } } -} // namespace -[[noreturn]] void -cxa_throw(void* obj, const std::type_info* type, destructor_type destructor) { +// always_inline to avoid an unnecessary stack frame in the trace. +[[gnu::always_inline]] +void add_exception_trace(void* obj, destructor_type destructor) { if (enableBacktraces.load(std::memory_order_relaxed)) { std::lock_guard lock(*get_exception_state_map_mutex()); get_exception_state_map()->emplace( obj, ExceptionState{ExceptionTraceHolder(), destructor}); } +} +} // namespace + +#ifndef _WIN32 +#if _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION +__attribute__((annotate("dynamic_fn_ptr"))) static abi::__cxa_exception* ( + *original_cxa_init_primary_exception)( + void*, + std::type_info*, + destructor_type) = &abi::__cxa_init_primary_exception; + +abi::__cxa_exception* cxa_init_primary_exception( + void* obj, + std::type_info* type, + destructor_type destructor) { + add_exception_trace(obj, destructor); + return original_cxa_init_primary_exception(obj, type, trace_destructor); +} + +const HookInfo* getHookInfo() { + static const HookInfo info = { + .original = + reinterpret_cast(&original_cxa_init_primary_exception), + .replacement = reinterpret_cast(&cxa_init_primary_exception), + }; + return &info; +} +#else +[[gnu::noreturn]] +__attribute__((annotate("dynamic_fn_ptr"))) static void (*original_cxa_throw)( + void*, + std::type_info*, + destructor_type) = &abi::__cxa_throw; +[[noreturn]] void +cxa_throw(void* obj, std::type_info* type, destructor_type destructor) { + add_exception_trace(obj, destructor); original_cxa_throw(obj, type, trace_destructor); } +const HookInfo* getHookInfo() { + static const HookInfo info = { + .original = reinterpret_cast(&original_cxa_throw), + .replacement = reinterpret_cast(&cxa_throw), + }; + return &info; +} +#endif +#endif + const ExceptionTraceHolder* detail::getExceptionTraceHolder( std::exception_ptr ptr) { { diff --git a/cxx/lyra/lyra_exceptions.h b/cxx/lyra/lyra_exceptions.h index 8f702a9..ad1fa82 100644 --- a/cxx/lyra/lyra_exceptions.h +++ b/cxx/lyra/lyra_exceptions.h @@ -79,21 +79,23 @@ void ensureRegisteredTerminateHandler(); std::string toString(std::exception_ptr exceptionPointer); /** - * lyra's cxa_throw will delegate to the original cxa throw. That pointer must - * be set before lyra::cxa_throw is called. - * - * One example use would be to statically compile against something that - * overrides __cxa_throw. That would look something like: - * - * [[noreturn]] void __cxa_throw(void* obj, const std::type_info* type, void - * (*destructor) (void*)) { static auto initializer = lyra::original_cxa_throw = - * lookupOriginalCxaThrow(); lyra::cxa_throw(obj, type, destructor); - * } + * Lyra needs to hook either __cxa_init_primary_exception or __cxa_throw + * (depending on the libc++ version) in order to inject stack traces. Users are + * required to set up this hooking using the information in this struct: + * - original is a pointer to the address of the exception function to hook. + * This pointer should have the address of the unhooked function stored back + * to it (if the hooking mechanism produces it), so that Lyra can delegate to + * the original function. + * - replacement is the address of Lyra's replacement function. After the + * hooking, all callers to the original function should call the replacement + * function instead. */ -[[gnu::noreturn]] extern void ( - *original_cxa_throw)(void*, const std::type_info*, void (*)(void*)); -[[noreturn]] void -cxa_throw(void* obj, const std::type_info* type, void (*destructor)(void*)); +struct HookInfo { + void** original; + void* replacement; +}; + +const HookInfo* getHookInfo(); void enableCxaThrowHookBacktraces(bool enable);