Updated documentation, added documentation for new stuff, and updated cpptrace.hpp a bit

This commit is contained in:
Jeremy 2023-11-20 22:14:13 -06:00
parent cd59ab5478
commit 7c49e64ba6
No known key found for this signature in database
GPG Key ID: B4C8300FEC395042
4 changed files with 410 additions and 53 deletions

249
README.md
View File

@ -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 `<stacktrace>`?](#faq-what-about-c23-stacktrace)
@ -71,18 +74,24 @@ Cpptrace also provides exception types that store stack traces:
#include <cpptrace/cpptrace.hpp>
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<T> represents a nullable integer. More docs later.
nullable<std::uint32_t> line;
nullable<std::uint32_t> 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<stacktrace_frame> 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<object_frame> 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<uintptr_t> 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<frame_ptr> 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<raw_trace, stacktrace>, 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 `<stdexcept>`:
```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<int> foo = {1, 2, 3};
CPPTRACE_WRAP_BLOCK(
foo.at(4) = 2;
foo.at(5)++;
);
std::cout<<CPPTRACE_WRAP(foo.at(12))<<std::endl;
```
## Exception handling with cpptrace
Working with cpptrace exceptions in your code:
```cpp
try {
@ -326,13 +373,115 @@ try {
} catch(cpptrace::exception& e) {
// Prints the exception info and stack trace, conditionally enabling color codes depending on
// whether stderr is a terminal
std::cerr << "Error: " << e.get_raw_what() << '\n';
e.get_trace().print(std::cerr, cpptrace::isatty(cpptrace::stderr_fileno));
std::cerr << "Error: " << e.message() << '\n';
e.trace().print(std::cerr, cpptrace::isatty(cpptrace::stderr_fileno));
} catch(std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
}
```
Additionally cpptrace provides a custom `std::terminate` handler that prints a stack trace from a cpptrace exception and otherwise behaves like the normal terminate handler and prints the stack trace involved in reaching `std::terminate`.
The stack trace to `std::terminate` may be helpful or it may not, it depends on the implementation, but often if an
implementation can't find an appropriate `catch` while unwinding it will jump directly to `std::terminate` giving
good information.
To register this custom handler:
```cpp
cpptrace::register_terminate_handler();
```
## Signal-Safe Tracing
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.
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()`, though you might be able to get
away with that, but this is what is needed to really do this safely.
The safe API is as follows:
```cpp
namespace cpptrace {
std::size_t safe_generate_raw_trace(frame_ptr* buffer, std::size_t size, std::size_t skip = 0);
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.
};
void get_minimal_object_frame(frame_ptr address, minimal_object_frame* out);
}
```
**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`.
Because signal-safe tracing is an involved process, I have written up a comprehensive overview of
what is involved at [signal-safe-tracing.md](signal-safe-tracing.md).
## Utility Types
A couple utility types are used to provide the library with a good interface.
`nullable<T>` 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<T>` 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<raw_trace, stacktrace>`.
```cpp
namespace cpptrace {
template<typename T, typename std::enable_if<std::is_integral<T>::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.

View File

@ -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;
};

203
signal-safe-tracing.md Normal file
View File

@ -0,0 +1,203 @@
# Signal-Safe Stack Tracing <!-- omit in toc -->
- [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 <sys/wait.h>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <cpptrace/cpptrace.hpp>
// 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 <cstdio>
#include <iostream>
#include <unistd.h>
#include <cpptrace/cpptrace.hpp>
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"<<res<<" "<<std::endl;
break;
} else {
trace.frames.push_back(frame.resolve());
}
}
trace.resolve().print();
}
```

View File

@ -56,6 +56,9 @@ namespace detail {
}
static const stacktrace_frame null_frame {0, nullable<uint32_t>::null(), nullable<uint32_t>::null(), "", "", false};
bool should_absorb_trace_exceptions();
enum cache_mode get_cache_mode();
}
}