-
-
Notifications
You must be signed in to change notification settings - Fork 14
Implement a cross-platform basic stack trace printer #2418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
07a49cd
276d7c1
a114a3c
81cab0b
7231f93
a01d39d
90cfaec
b5cfad0
8bcd1d7
98a923b
f6d429c
6cc703f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME stacktrace | ||
| PRIVATE_HEADERS | ||
| SOURCES stacktrace.cc stacktrace_posix.h stacktrace_windows.h) | ||
|
|
||
| if(UNIX) | ||
| target_link_libraries(sourcemeta_core_stacktrace PRIVATE ${CMAKE_DL_LIBS}) | ||
| target_link_options(sourcemeta_core_stacktrace INTERFACE | ||
| "$<$<PLATFORM_ID:Linux>:-rdynamic>" | ||
| "$<$<PLATFORM_ID:Darwin>:-rdynamic>") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||
| elseif(WIN32) | ||
| target_link_libraries(sourcemeta_core_stacktrace PRIVATE dbghelp) | ||
| endif() | ||
|
|
||
| if(SOURCEMETA_CORE_INSTALL) | ||
| sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME stacktrace) | ||
| endif() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| #ifndef SOURCEMETA_CORE_STACKTRACE_H_ | ||
| #define SOURCEMETA_CORE_STACKTRACE_H_ | ||
|
|
||
| #ifndef SOURCEMETA_CORE_STACKTRACE_EXPORT | ||
| #include <sourcemeta/core/stacktrace_export.h> | ||
| #endif | ||
|
|
||
| /// @defgroup stacktrace Stacktrace | ||
| /// @brief A collection of utilities for interacting with stack traces. | ||
| /// | ||
| /// This functionality is included as follows: | ||
| /// | ||
| /// ```cpp | ||
| /// #include <sourcemeta/core/stacktrace.h> | ||
| /// ``` | ||
|
|
||
| namespace sourcemeta::core { | ||
|
|
||
| /// @ingroup stacktrace | ||
| /// | ||
| /// Install a process-wide handler that prints a stack trace on fatal signals. | ||
| /// For example: | ||
| /// | ||
| /// ```cpp | ||
| /// #include <sourcemeta/core/stacktrace.h> | ||
| /// | ||
| /// auto main() -> int { | ||
| /// sourcemeta::core::stacktrace_on_crash(); | ||
| /// // ... rest of the program | ||
| /// } | ||
| /// ``` | ||
| SOURCEMETA_CORE_STACKTRACE_EXPORT | ||
| auto stacktrace_on_crash() -> void; | ||
|
|
||
| /// @ingroup stacktrace | ||
| /// | ||
| /// Print the current backtrace. Safe to call from any thread, but not from a | ||
| /// signal handler. For example: | ||
| /// | ||
| /// ```cpp | ||
| /// #include <sourcemeta/core/stacktrace.h> | ||
| /// | ||
| /// sourcemeta::core::stacktrace(); | ||
| /// ``` | ||
| SOURCEMETA_CORE_STACKTRACE_EXPORT | ||
| auto stacktrace() -> void; | ||
|
|
||
| } // namespace sourcemeta::core | ||
|
|
||
| #endif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #include <sourcemeta/core/stacktrace.h> | ||
|
|
||
| #if defined(_WIN32) | ||
| #include "stacktrace_windows.h" | ||
| #elif defined(__unix__) || defined(__APPLE__) | ||
| #include "stacktrace_posix.h" | ||
| #else | ||
| #error "sourcemeta::core::stacktrace has no implementation for this platform" | ||
| #endif |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,241 @@ | ||||||
| #ifndef SOURCEMETA_CORE_STACKTRACE_POSIX_H_ | ||||||
| #define SOURCEMETA_CORE_STACKTRACE_POSIX_H_ | ||||||
|
|
||||||
| #include <sourcemeta/core/stacktrace.h> | ||||||
|
|
||||||
| #include <array> // std::array | ||||||
| #include <atomic> // std::atomic | ||||||
| #include <csignal> // sigaction, struct sigaction, SIG*, raise | ||||||
| #include <cstddef> // std::size_t | ||||||
| #include <cstdint> // std::uintptr_t | ||||||
| #include <cstring> // std::strlen | ||||||
| #include <initializer_list> // std::initializer_list | ||||||
|
|
||||||
| #include <dlfcn.h> // dladdr, Dl_info | ||||||
| #include <execinfo.h> // backtrace | ||||||
| #include <sys/ucontext.h> // ucontext_t | ||||||
| #include <unistd.h> // write, getpid, STDERR_FILENO | ||||||
|
|
||||||
| namespace { | ||||||
|
|
||||||
| constexpr int maximum_frames{128}; | ||||||
| constexpr std::size_t hex_buffer_size{2 + (sizeof(std::uintptr_t) * 2)}; | ||||||
| constexpr std::size_t decimal_buffer_size{24}; | ||||||
|
|
||||||
| auto raw_write(int file_descriptor, const char *data, std::size_t size) | ||||||
| -> void { | ||||||
| [[maybe_unused]] const auto result{::write(file_descriptor, data, size)}; | ||||||
| } | ||||||
|
|
||||||
| auto write_text(int file_descriptor, const char *text) -> void { | ||||||
| raw_write(file_descriptor, text, std::strlen(text)); | ||||||
| } | ||||||
|
|
||||||
| auto write_hex(int file_descriptor, std::uintptr_t value) -> void { | ||||||
| constexpr const char *digits{"0123456789abcdef"}; | ||||||
| std::array<char, hex_buffer_size> buffer{{'0', 'x'}}; | ||||||
| std::size_t index{2}; | ||||||
| if (value == 0) { | ||||||
| buffer[index++] = '0'; | ||||||
| } else { | ||||||
| std::array<char, sizeof(std::uintptr_t) * 2> temporary{}; | ||||||
| std::size_t length{0}; | ||||||
| while (value != 0) { | ||||||
| temporary[length++] = digits[value & 0xF]; | ||||||
| value >>= 4; | ||||||
| } | ||||||
| while (length > 0) { | ||||||
| buffer[index++] = temporary[--length]; | ||||||
| } | ||||||
| } | ||||||
| raw_write(file_descriptor, buffer.data(), index); | ||||||
| } | ||||||
|
|
||||||
| auto write_decimal(int file_descriptor, unsigned long value) -> void { | ||||||
| std::array<char, decimal_buffer_size> buffer{}; | ||||||
| std::size_t length{0}; | ||||||
| if (value == 0) { | ||||||
| buffer[length++] = '0'; | ||||||
| } else { | ||||||
| std::array<char, decimal_buffer_size> temporary{}; | ||||||
| std::size_t temporary_length{0}; | ||||||
| while (value != 0) { | ||||||
| temporary[temporary_length++] = static_cast<char>('0' + (value % 10)); | ||||||
| value /= 10; | ||||||
| } | ||||||
| while (temporary_length > 0) { | ||||||
| buffer[length++] = temporary[--temporary_length]; | ||||||
| } | ||||||
| } | ||||||
| raw_write(file_descriptor, buffer.data(), length); | ||||||
| } | ||||||
|
|
||||||
| auto write_frame(int file_descriptor, int frame_index, void *address) -> void { | ||||||
| Dl_info information{}; | ||||||
| const int resolved{::dladdr(address, &information)}; | ||||||
|
|
||||||
| write_text(file_descriptor, "#"); | ||||||
| write_decimal(file_descriptor, static_cast<unsigned long>(frame_index)); | ||||||
| write_text(file_descriptor, " "); | ||||||
| write_hex(file_descriptor, reinterpret_cast<std::uintptr_t>(address)); | ||||||
| write_text(file_descriptor, " "); | ||||||
|
|
||||||
| const char *symbol_name{(resolved != 0 && information.dli_sname != nullptr) | ||||||
| ? information.dli_sname | ||||||
| : "<unknown>"}; | ||||||
| write_text(file_descriptor, symbol_name); | ||||||
|
|
||||||
| if (resolved != 0 && information.dli_saddr != nullptr) { | ||||||
| const auto offset{static_cast<std::uintptr_t>( | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This computes an offset via pointer subtraction on unrelated addresses, which is undefined behavior in C++. Use Prompt for AI agents |
||||||
| reinterpret_cast<char *>(address) - | ||||||
| reinterpret_cast<char *>(information.dli_saddr))}; | ||||||
| write_text(file_descriptor, " +"); | ||||||
| write_hex(file_descriptor, offset); | ||||||
| } | ||||||
|
|
||||||
| if (resolved != 0 && information.dli_fname != nullptr) { | ||||||
| write_text(file_descriptor, "\n in "); | ||||||
| write_text(file_descriptor, information.dli_fname); | ||||||
| } | ||||||
| write_text(file_descriptor, "\n"); | ||||||
| } | ||||||
|
|
||||||
| __attribute__((noinline)) auto write_backtrace(int file_descriptor, | ||||||
| int frames_to_skip, | ||||||
| void *crash_pc = nullptr) | ||||||
| -> void { | ||||||
| std::array<void *, maximum_frames> frames{}; | ||||||
| const int captured{::backtrace(frames.data(), maximum_frames)}; | ||||||
|
|
||||||
| int frame_index{0}; | ||||||
| void *suppress_saddr{nullptr}; | ||||||
| if (crash_pc != nullptr) { | ||||||
| write_frame(file_descriptor, frame_index, crash_pc); | ||||||
| frame_index = frame_index + 1; | ||||||
| Dl_info crash_information{}; | ||||||
| if (::dladdr(crash_pc, &crash_information) != 0 && | ||||||
| crash_information.dli_saddr != nullptr) { | ||||||
| suppress_saddr = crash_information.dli_saddr; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| for (int index{frames_to_skip}; index < captured; ++index) { | ||||||
| void *address{frames[static_cast<std::size_t>(index)]}; | ||||||
| if (suppress_saddr != nullptr) { | ||||||
| Dl_info frame_information{}; | ||||||
| if (::dladdr(address, &frame_information) != 0 && | ||||||
| frame_information.dli_saddr == suppress_saddr) { | ||||||
| suppress_saddr = nullptr; | ||||||
| continue; | ||||||
| } | ||||||
| } | ||||||
| write_frame(file_descriptor, frame_index, address); | ||||||
| frame_index = frame_index + 1; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| auto extract_crash_pc(void *context) -> void * { | ||||||
| if (context == nullptr) { | ||||||
| return nullptr; | ||||||
| } | ||||||
| [[maybe_unused]] const auto *user_context{ | ||||||
| static_cast<const ucontext_t *>(context)}; | ||||||
| std::uintptr_t program_counter{0}; | ||||||
| #if defined(__APPLE__) && defined(__aarch64__) | ||||||
| program_counter = user_context->uc_mcontext->__ss.__pc; | ||||||
| #elif defined(__APPLE__) && defined(__x86_64__) | ||||||
| program_counter = user_context->uc_mcontext->__ss.__rip; | ||||||
| #elif defined(__linux__) && defined(__aarch64__) | ||||||
| program_counter = user_context->uc_mcontext.pc; | ||||||
| #elif defined(__linux__) && defined(__x86_64__) | ||||||
| program_counter = | ||||||
| static_cast<std::uintptr_t>(user_context->uc_mcontext.gregs[16]); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. src/lang/stacktrace/stacktrace_posix.h:151-152: Using Severity: low 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Hardcoding Prompt for AI agents
Suggested change
|
||||||
| #endif | ||||||
| // NOLINTNEXTLINE(performance-no-int-to-ptr) | ||||||
| return reinterpret_cast<void *>(program_counter); | ||||||
| } | ||||||
|
|
||||||
| constexpr const char *separator{"========================================" | ||||||
| "========================================\n"}; | ||||||
|
|
||||||
| std::atomic<bool> crash_handler_installed{false}; | ||||||
|
|
||||||
| extern "C" __attribute__((visibility("default"))) auto | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||||||
| crash_handler(int signal_number, siginfo_t * /*info*/, void *context) -> void { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. src/lang/stacktrace/stacktrace_posix.h:164: Because Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||||||
| const int file_descriptor{STDERR_FILENO}; | ||||||
| write_text(file_descriptor, "\n"); | ||||||
| write_text(file_descriptor, separator); | ||||||
| write_text(file_descriptor, "signal: "); | ||||||
| write_decimal(file_descriptor, static_cast<unsigned long>(signal_number)); | ||||||
| write_text(file_descriptor, " ("); | ||||||
| const char *signal_name{"UNKNOWN"}; | ||||||
| switch (signal_number) { | ||||||
| case SIGSEGV: | ||||||
| signal_name = "SIGSEGV"; | ||||||
| break; | ||||||
| case SIGABRT: | ||||||
| signal_name = "SIGABRT"; | ||||||
| break; | ||||||
| case SIGFPE: | ||||||
| signal_name = "SIGFPE"; | ||||||
| break; | ||||||
| case SIGBUS: | ||||||
| signal_name = "SIGBUS"; | ||||||
| break; | ||||||
| case SIGILL: | ||||||
| signal_name = "SIGILL"; | ||||||
| break; | ||||||
| default: | ||||||
| break; | ||||||
| } | ||||||
| write_text(file_descriptor, signal_name); | ||||||
| write_text(file_descriptor, ")\n"); | ||||||
| write_text(file_descriptor, "pid: "); | ||||||
| write_decimal(file_descriptor, static_cast<unsigned long>(::getpid())); | ||||||
| write_text(file_descriptor, "\n\n"); | ||||||
| write_backtrace(file_descriptor, /*frames_to_skip=*/1, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. src/lang/stacktrace/stacktrace_posix.h:196: Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: The crash signal handler executes non-async-signal-safe routines ( Prompt for AI agents |
||||||
| extract_crash_pc(context)); | ||||||
| write_text(file_descriptor, separator); | ||||||
|
|
||||||
| struct sigaction default_action{}; | ||||||
| default_action.sa_handler = SIG_DFL; | ||||||
| sigemptyset(&default_action.sa_mask); | ||||||
| ::sigaction(signal_number, &default_action, nullptr); | ||||||
| ::raise(signal_number); | ||||||
| } | ||||||
|
|
||||||
| } // namespace | ||||||
|
|
||||||
| namespace sourcemeta::core { | ||||||
|
|
||||||
| // NOLINTNEXTLINE(misc-definitions-in-headers) | ||||||
| __attribute__((visibility("default"))) auto stacktrace_on_crash() -> void { | ||||||
| bool expected{false}; | ||||||
| if (!crash_handler_installed.compare_exchange_strong(expected, true)) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| struct sigaction action{}; | ||||||
| action.sa_sigaction = &crash_handler; | ||||||
| action.sa_flags = static_cast<int>(SA_SIGINFO | SA_RESETHAND | SA_NODEFER); | ||||||
| sigemptyset(&action.sa_mask); | ||||||
|
|
||||||
| for (const int signal_number : {SIGSEGV, SIGABRT, SIGFPE, SIGBUS, SIGILL}) { | ||||||
| ::sigaction(signal_number, &action, nullptr); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // NOLINTNEXTLINE(misc-definitions-in-headers) | ||||||
| __attribute__((noinline, visibility("default"))) auto stacktrace() -> void { | ||||||
| const int file_descriptor{STDERR_FILENO}; | ||||||
| write_text(file_descriptor, separator); | ||||||
| write_text(file_descriptor, "pid: "); | ||||||
| write_decimal(file_descriptor, static_cast<unsigned long>(::getpid())); | ||||||
| write_text(file_descriptor, "\n\n"); | ||||||
| write_backtrace(file_descriptor, /*frames_to_skip=*/1); | ||||||
| write_text(file_descriptor, separator); | ||||||
| } | ||||||
|
|
||||||
| } // namespace sourcemeta::core | ||||||
|
|
||||||
| #endif | ||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/lang/stacktrace/CMakeLists.txt:9:
-rdynamicis an ELF/GNU-style flag and is often rejected or ignored by Mach-O toolchains, so enabling it forPLATFORM_ID:Darwinmay break macOS linking or provide no effect.Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.