diff --git a/README.md b/README.md index b184852..1d04b7c 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,10 @@ and Windows including MinGW and Cygwin environments. The goal: Make stack traces - [Raw Traces](#raw-traces) - [Utilities](#utilities) - [Traced Exceptions](#traced-exceptions) + - [Wrapping std::exceptions](#wrapping-stdexceptions) - [Exception handling with cpptrace](#exception-handling-with-cpptrace) + - [Signal-Safe Tracing](#signal-safe-tracing) + - [Utility Types](#utility-types) - [Notable Library Configurations](#notable-library-configurations) - [Notes About the Library and Future Work](#notes-about-the-library-and-future-work) - [FAQ: What about C++23 ``?](#faq-what-about-c23-stacktrace) @@ -71,18 +74,24 @@ Cpptrace also provides exception types that store stack traces: #include void trace() { - throw cpptrace::exception(); + throw cpptrace::logic_error("This wasn't supposed to happen!"); } /* other stuff */ -// terminate called after throwing an instance of 'cpptrace::exception' -// what(): cpptrace::exception: +// terminate called after throwing an instance of 'cpptrace::logic_error' +// what(): This wasn't supposed to happen!: // Stack trace (most recent call first): // #0 0x00005641c715a1b6 in trace() at demo.cpp:9 // #1 0x00005641c715a229 in foo(int) at demo.cpp:16 // #2 0x00005641c715a2ba in main at demo.cpp:34 ``` +Additional notable features: + +- Utilities for demangling +- Utilities for catching `std::exception`s and wrapping them in traced exceptions +- Signal-safe stack tracing + ## CMake FetchContent Usage ```cmake @@ -131,12 +140,17 @@ direct access to frames as well as iterators. ```cpp namespace cpptrace { + // Some type sufficient for an instruction pointer, currently always an alias to std::uintptr_t + using frame_ptr = std::uintptr_t; + struct stacktrace_frame { - uintptr_t address; - std::uint_least32_t line; - std::uint_least32_t column; // Unknown column is represented with UINT_LEAST32_MAX + frame_ptr address; + // nullable represents a nullable integer. More docs later. + nullable line; + nullable column; std::string filename; std::string symbol; + bool is_inline; bool operator==(const stacktrace_frame& other) const; bool operator!=(const stacktrace_frame& other) const; std::string to_string() const; @@ -145,8 +159,9 @@ namespace cpptrace { struct stacktrace { std::vector frames; - static stacktrace current(std::uint_least32_t skip = 0); // here as a drop-in for std::stacktrace - static stacktrace current(std::uint_least32_t skip, std::uint_least32_t max_depth); + // here as a drop-in for std::stacktrace + static stacktrace current(std::size_t skip = 0); + static stacktrace current(std::size_t skip, std::size_t max_depth); void print() const; void print(std::ostream& stream) const; void print(std::ostream& stream, bool color) const; @@ -156,8 +171,8 @@ namespace cpptrace { /* operator<<(ostream, ..), std::format support, and iterators exist for this object */ }; - stacktrace generate_trace(std::uint_least32_t skip = 0); - stacktrace generate_trace(std::uint_least32_t skip, std::uint_least32_t max_depth); + stacktrace generate_trace(std::size_t skip = 0); + stacktrace generate_trace(std::size_t skip, std::size_t max_depth); } ``` @@ -172,22 +187,22 @@ namespace cpptrace { struct object_frame { std::string obj_path; std::string symbol; - uintptr_t raw_address = 0; - uintptr_t obj_address = 0; + frame_ptr raw_address; + frame_ptr obj_address; }; struct object_trace { std::vector frames; - static object_trace current(std::uint_least32_t skip = 0); - static object_trace current(std::uint_least32_t skip, std::uint_least32_t max_depth); + static object_trace current(std::size_t skip = 0); + static object_trace current(std::size_t skip, std::size_t max_depth); stacktrace resolve() const; void clear(); bool empty() const noexcept; /* iterators exist for this object */ }; - object_trace generate_object_trace(std::uint_least32_t skip = 0); - object_trace generate_object_trace(std::uint_least32_t skip, std::uint_least32_t max_depth); + object_trace generate_object_trace(std::size_t skip = 0); + object_trace generate_object_trace(std::size_t skip, std::size_t max_depth); } ``` @@ -201,9 +216,9 @@ Note it is important executables and shared libraries in memory aren't somehow u ```cpp namespace cpptrace { struct raw_trace { - std::vector frames; - static raw_trace current(std::uint_least32_t skip = 0); - static raw_trace current(std::uint_least32_t skip, std::uint_least32_t max_depth); + std::vector frames; + static raw_trace current(std::size_t skip = 0); + static raw_trace current(std::size_t skip, std::size_t max_depth); object_trace resolve_object_trace() const; stacktrace resolve() const; void clear(); @@ -211,8 +226,8 @@ namespace cpptrace { /* iterators exist for this object */ }; - raw_trace generate_raw_trace(std::uint_least32_t skip = 0); - raw_trace generate_raw_trace(std::uint_least32_t skip, std::uint_least32_t max_depth); + raw_trace generate_raw_trace(std::size_t skip = 0); + raw_trace generate_raw_trace(std::size_t skip, std::size_t max_depth); } ``` @@ -263,38 +278,60 @@ namespace cpptrace { ### Traced Exceptions -Cpptrace provides a set of exception classes that that generate stack traces when thrown. These exceptions generate -relatively lightweight raw traces and resolve symbols and line numbers lazily if and when requested. +Cpptrace provides an interface for a traced exceptions, `cpptrace::exception`, as well as a set of exception classes +that that generate stack traces when thrown. These exceptions generate relatively lightweight raw traces and resolve +symbols and line numbers lazily if and when requested. + +The basic interface is: +```cpp +namespace cpptrace { + class exception : public std::exception { + public: + virtual const char* what() const noexcept = 0; // The what string both the message and trace + virtual const char* message() const noexcept = 0; + virtual const stacktrace& trace() const noexcept = 0; + }; +} +``` + +There are two ways to go about traced exception objects: Traces can be resolved eagerly or lazily. Cpptrace provides the +basic implementation of exceptions as lazy exceptions. I hate to have anything about the implementation exposed in the +interface or type system but this seems to be the best way to do this. ```cpp namespace cpptrace { - // Traced exception class - // Extending classes should call the exception constructor with a skip value of 1. - class exception : public std::exception { + class lazy_exception : public exception { + mutable detail::lazy_trace_holder trace_holder; // basically std::variant, more docs later + mutable std::string what_string; protected: - explicit exception(std::uint_least32_t skip, std::uint_least32_t max_depth) noexcept; - explicit exception(std::uint_least32_t skip) noexcept; + explicit lazy_exception(std::size_t skip, std::size_t max_depth) noexcept; + explicit lazy_exception(std::size_t skip) noexcept; public: - explicit exception() noexcept; - virtual const char* what() const noexcept override; - // what(), but not a C-string. Performs lazy evaluation of the full what string. - virtual const std::string& get_what() const noexcept; - // Just the plain what() value without the stacktrace. This value is called by get_what() - // during lazy evaluation. - virtual const char* get_raw_what() const noexcept; - // Returns internal raw_trace - const raw_trace& get_raw_trace() const noexcept; - // Returns a resolved trace. Performs lazy evaluation. - const stacktrace& get_trace() const noexcept; + explicit lazy_exception() noexcept : lazy_exception(1) {} + const char* what() const noexcept override; + const char* message() const noexcept override; + const stacktrace& trace() const noexcept override; }; +} +``` - class exception_with_message : public exception { +`cpptrace::lazy_exception` can be freely thrown or overridden. Generally `message()` is the only field to override. + +Lastly cpptrace provides an exception class that takes a user-provided message, `cpptrace::exception_with_message`, as +well as a number of traced exception classes resembling ``: + +```cpp +namespace cpptrace { + class CPPTRACE_EXPORT exception_with_message : public lazy_exception { + mutable std::string user_message; protected: - explicit exception_with_message(std::string&& message_arg, std::uint_least32_t skip) noexcept; - explicit exception_with_message(std::string&& message_arg, std::uint_least32_t skip, std::uint_least32_t max_depth) noexcept; + explicit exception_with_message(std::string&& message_arg, std::size_t skip) noexcept; + explicit exception_with_message(std::string&& message_arg, std::size_t skip, std::size_t max_depth) noexcept; public: - explicit exception_with_message(std::string&& message_arg) noexcept; - virtual const char* get_raw_what() const noexcept override; + explicit exception_with_message(std::string&& message_arg) noexcept + : exception_with_message(std::move(message_arg), 1) {} + + const char* message() const noexcept override; }; // All stdexcept errors have analogs here. Same constructor as exception_with_message. @@ -310,15 +347,25 @@ namespace cpptrace { } ``` -## Exception handling with cpptrace +## Wrapping std::exceptions -To register a custom handler for `std::terminate` that prints a stack trace from a cpptrace exception and otherwise -behaves like the normal terminate handler. +Cpptrace exceptions can provide great information for user-controlled exceptions. For non-cpptrace::exceptions that may +originate outside of code you control, e.g. the standard library, cpptrace provides some wrapper utilities that can +rethrow these exceptions nested in traced cpptrace exceptions. The trace won't be perfect, the trace will start where +the rapper caught it, but these utilities can provide good diagnostic information. Unfortunately this is the best +solution for this problem, as far as I know. ```cpp -cpptrace::register_terminate_handler(); +std::vector foo = {1, 2, 3}; +CPPTRACE_WRAP_BLOCK( + foo.at(4) = 2; + foo.at(5)++; +); +std::cout<` is used for a nullable integer type. Internally the maximum value for `T` is used as a +sentinel. `std::optional` would be used if this library weren't c++11. But, `nullable` provides +an `std::optional`-like interface and it's less heavy-duty for this use than an `std::optional`. + +`detail::lazy_trace_holder` is a utility type for `lazy_exception` used in place of an +`std::variant`. + +```cpp +namespace cpptrace { + template::value, int>::type = 0> + struct nullable { + T raw_value; + nullable& operator=(T value) + bool has_value() const noexcept; + T& value() noexcept; + const T& value() const noexcept; + T value_or(T alternative) const noexcept; + void swap(nullable& other) noexcept; + void reset() noexcept; + bool operator==(const nullable& other) const noexcept; + bool operator!=(const nullable& other) const noexcept; + constexpr static nullable null() noexcept; // returns a null instance + }; + + namespace detail { + class lazy_trace_holder { + bool resolved; + union { + raw_trace trace; + stacktrace resolved_trace; + }; + public: + // constructors + lazy_trace_holder() : trace() {} + explicit lazy_trace_holder(raw_trace&& _trace); + explicit lazy_trace_holder(stacktrace&& _resolved_trace); + // logistics + lazy_trace_holder(const lazy_trace_holder& other); + lazy_trace_holder(lazy_trace_holder&& other) noexcept; + lazy_trace_holder& operator=(const lazy_trace_holder& other); + lazy_trace_holder& operator=(lazy_trace_holder&& other) noexcept; + ~lazy_trace_holder(); + // access + stacktrace& get_resolved_trace(); + const stacktrace& get_resolved_trace() const; // throws if not already resolved + private: + void clear(); + }; + } +} +``` + ## Notable Library Configurations - `CPPTRACE_STATIC=On/Off`: Create cpptrace as a static library. diff --git a/include/cpptrace/cpptrace.hpp b/include/cpptrace/cpptrace.hpp index f2089e4..0919958 100644 --- a/include/cpptrace/cpptrace.hpp +++ b/include/cpptrace/cpptrace.hpp @@ -31,6 +31,7 @@ namespace cpptrace { struct object_trace; struct stacktrace; + // Some type sufficient for an instruction pointer, currently always an alias to std::uintptr_t using frame_ptr = std::uintptr_t; struct CPPTRACE_EXPORT raw_trace { @@ -180,11 +181,13 @@ namespace cpptrace { #define CPPTRACE_PATH_MAX 4096 // safe tracing interface + // signal-safe CPPTRACE_EXPORT std::size_t safe_generate_raw_trace( frame_ptr* buffer, std::size_t size, std::size_t skip = 0 ); + // signal-safe CPPTRACE_EXPORT std::size_t safe_generate_raw_trace( frame_ptr* buffer, std::size_t size, @@ -198,6 +201,7 @@ namespace cpptrace { // To be called outside a signal handler. Not signal safe. object_frame resolve() const; }; + // signal-safe CPPTRACE_EXPORT void get_minimal_object_frame(frame_ptr address, minimal_object_frame* out); // utilities: @@ -225,9 +229,6 @@ namespace cpptrace { } namespace detail { - CPPTRACE_EXPORT bool should_absorb_trace_exceptions(); - CPPTRACE_EXPORT enum cache_mode get_cache_mode(); - // This is a helper utility, if the library weren't C++11 an std::variant would be used class CPPTRACE_EXPORT lazy_trace_holder { bool resolved; @@ -257,6 +258,7 @@ namespace cpptrace { // Interface for a traced exception object class CPPTRACE_EXPORT exception : public std::exception { public: + virtual const char* what() const noexcept = 0; virtual const char* message() const noexcept = 0; virtual const stacktrace& trace() const noexcept = 0; }; diff --git a/signal-safe-tracing.md b/signal-safe-tracing.md new file mode 100644 index 0000000..d396da1 --- /dev/null +++ b/signal-safe-tracing.md @@ -0,0 +1,203 @@ +# Signal-Safe Stack Tracing + +- [Overview](#overview) +- [Big-Picture](#big-picture) +- [API](#api) +- [Strategy](#strategy) +- [Technical Requirements](#technical-requirements) +- [Signal-Safe Tracing With `fork()` + `exec()`](#signal-safe-tracing-with-fork--exec) + - [In the main program](#in-the-main-program) + - [In the tracer program](#in-the-tracer-program) + +# Overview + +Signal-safe stack tracing is very useful for debugging application crashes, e.g. SIGSEGVs or +SIGTRAPs, but it's very difficult to do correctly and most implementations I see online do this +incorrectly. + +Signal-safe tracing is difficult because most methods for unwinding are not signal-safe, figuring +out what shared objects addresses are in is tricky to do in a signal-safe manner (`dladdr` isn't +safe), and then the symbol/line resolution process is pretty much impossible to do safely (parsing +dwarf will not be safe). + +# Big-Picture + +In order to do this full process safely the way to go is collecting basic information in the signal +handler and then either resolving later or handing that information to another process to resolve. + +It's not as simple as calling `cpptrace::generate_trace().print()` but this is what is needed to +really do this safely as far as I can tell. + +FAQ: What's the worst that could happen if you call `cpptrace::generate_trace().print()` from a +signal handler? In many cases you might be able to get away with it but you risk deadlocking or +memory corruption. + +# API + +Cpptrace provides APIs for generating raw trace information safely and then also safely resolving +those raw pointers to the most minimal object information needed to resolve later. + +```cpp +namespace cpptrace { + // signal-safe + std::size_t safe_generate_raw_trace(frame_ptr* buffer, std::size_t size, std::size_t skip = 0); + // signal-safe + std::size_t safe_generate_raw_trace(frame_ptr* buffer, std::size_t size, std::size_t skip, std::size_t max_depth); + + struct minimal_object_frame { + frame_ptr raw_address; + frame_ptr address_relative_to_object_base_in_memory; + char object_path[CPPTRACE_PATH_MAX + 1]; + object_frame resolve() const; // To be called outside a signal handler. Not signal safe. + }; + + // signal-safe + void get_minimal_object_frame(frame_ptr address, minimal_object_frame* out); +} +``` + +# Strategy + +Signal-safe tracing can be done three ways: +- In a signal handler, call `safe_generate_raw_trace` and then outside a signal handler + construct a `cpptrace:raw_trace` and resolve. +- In a signal handler, call `safe_generate_raw_trace`, then write `cpptrace::minimal_object_frame` + information to a file to be resolved later. +- In a signal handler, call `safe_generate_raw_trace`, `fork()` and `exec()` a process to handle the + resolution, pass `cpptrace::minimal_object_frame` information to that child through a pipe, and + wait for the child to exit. + +It's not as simple as calling `cpptrace::generate_trace().print()`, I know, but these are truly the +only ways to do this safely as far as I can tell. + +# Technical Requirements + +**Note:** Not all back-ends and platforms support these interfaces. If signal-safe unwinding isn't supported +`safe_generate_raw_trace` will just produce an empty trace and if object information can't be resolved in a signal-safe +way then `get_minimal_object_frame` will not populate fields beyond the `raw_address`. + +Currently the only back-end that can unwind safely is libunwind. Currently, the only way I know to get `dladdr`'s +information in a signal-safe manner is `_dl_find_object`, which doesn't exist on macos (or windows of course). If anyone +knows ways to do these safely on other platforms, I'd be much appreciative. + +# Signal-Safe Tracing With `fork()` + `exec()` + +Of the three strategies, `fork()` + `exec()`, is the most technically involved and the only way to resolve while the +signal handler is running. I think it's worthwhile to do a deep-dive into how to do this. + +In the source code, [`signal_demo.cpp`](signal_demo.cpp) and [`signal_tracer.cpp`](signal_tracer.cpp) provide a working +example for what is described here. + +## In the main program + +The main program handles most of the complexity for tracing from signal handlers: +- Collecting a raw trace +- Spawning a child process +- Resolving raw frame pointers to minimal object frames +- Sending that info to the other process + +A basic implementation is as follows: + +```cpp +#include +#include +#include +#include + +#include + +// This is just a utility I like, it makes the pipe API more expressive. +struct pipe_t { + union { + struct { + int read_end; + int write_end; + }; + int data[2]; + }; +}; + +void do_signal_safe_trace(cpptrace::frame_ptr* buffer, std::size_t size) { + // Setup pipe and spawn child + pipe_t input_pipe; + pipe(input_pipe.data); + const pid_t pid = fork(); + if(pid == -1) { return; /* Some error ocurred */ } + if(pid == 0) { // child + dup2(input_pipe.read_end, STDIN_FILENO); + close(input_pipe.read_end); + close(input_pipe.write_end); + execl("signal_tracer", "signal_tracer", nullptr); + _exit(1); + } + // Resolve to minimal_object_frames and write those to the pipe + for(std::size_t i = 0; i < count; i++) { + cpptrace::minimal_object_frame frame; + cpptrace::get_minimal_object_frame(buffer[i], &frame); + write(input_pipe.write_end, &frame, sizeof(frame)); + } + close(input_pipe.read_end); + close(input_pipe.write_end); + // Wait for child + waitpid(pid, nullptr, 0); +} + +void handler(int signo, siginfo_t* info, void* context) { + // Print basic message + const char* message = "SIGSEGV ocurred:\n"; + write(STDERR_FILENO, message, strlen(message)); + // Generate trace + constexpr std::size_t N = 100; + cpptrace::frame_ptr buffer[N]; + std::size_t count = cpptrace::safe_generate_raw_trace(buffer, N); + do_signal_safe_trace(buffer, N); + // Up to you if you want to exit or continue or whatever + _exit(1); +} + +int main() { + // Setup signal handler + struct sigaction action = { 0 }; + action.sa_flags = 0; + action.sa_sigaction = &handler; + if (sigaction(SIGSEGV, &action, NULL) == -1) { + perror("sigaction"); + } + + /// ... +} +``` + +## In the tracer program + +The tracer program is quite simple. It just has to read `cpptrace::minimal_object_frame`s from the pipe, resolve to +`cpptrace::object_frame`s, and resolve an `object_trace`. + +```cpp +#include +#include +#include + +#include + +int main() { + cpptrace::object_trace trace; + while(true) { + cpptrace::minimal_object_frame frame; + // fread used over read because a read() from a pipe might not read the full frame + std::size_t res = fread(&frame, sizeof(frame), 1, stdin); + if(res == 0) { + break; + } else if(res == -1) { + perror("Something went wrong while reading from the pipe"); + break; + } else if(res != 1) { + std::cerr<<"Something went wrong while reading from the pipe"<::null(), nullable::null(), "", "", false}; + + bool should_absorb_trace_exceptions(); + enum cache_mode get_cache_mode(); } }