Updating wording and docs arround the safe trace API

This commit is contained in:
Jeremy Rifkin 2024-11-14 22:07:32 -06:00
parent 88df27aa32
commit 5a9a4f314d
No known key found for this signature in database
GPG Key ID: 19AA8270105E8EB4
2 changed files with 33 additions and 22 deletions

View File

@ -670,17 +670,14 @@ 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.
Stack traces from signal handlers can provide very helpful information for debugging application crashes, e.g. from
SIGSEGV or SIGTRAP handlers. Signal handlers are really restrictive environments as your application could be
interrupted by a signal at any point, including in the middle of malloc or buffered IO or while holding a lock.
Doing a stack trace in a signal handler is possible but it requires a lot of care. This is difficult to do correctly
and most examples 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:
Cpptrace offers an API to walk the stack in a signal handler and produce a raw trace safely. The library also provides
an interface for producing a object frame safely:
```cpp
namespace cpptrace {
@ -688,7 +685,7 @@ namespace cpptrace {
std::size_t safe_generate_raw_trace(frame_ptr* buffer, std::size_t size, std::size_t skip, std::size_t max_depth);
struct safe_object_frame {
frame_ptr raw_address;
frame_ptr address_relative_to_object_start; // object base address must yet be added
frame_ptr address_relative_to_object_start;
char object_path[CPPTRACE_PATH_MAX + 1];
object_frame resolve() const; // To be called outside a signal handler. Not signal safe.
};
@ -697,6 +694,16 @@ namespace cpptrace {
}
```
It is not possible to resolve debug symbols safely in the process from a signal handler without heroic effort. In order
to produce a full trace there are three options:
1. Carefully save the object trace information to be resolved at a later time outside the signal handler
2. Write the object trace information to a file to be resolved later
3. Spawn a new process, communicate object trace information to that process, and have that process do the trace
resolution
For traces on segfaults, e.g., only options 2 and 3 are viable. For more information an implementation of approach 3,
see the comprehensive overview and demo at [signal-safe-tracing.md](docs/signal-safe-tracing.md).
> [!IMPORTANT]
> Currently signal-safe stack unwinding is only possible with `libunwind`, which must be
> [manually enabled](#library-back-ends). If signal-safe unwinding isn't supported, `safe_generate_raw_trace` will just
@ -712,9 +719,6 @@ namespace cpptrace {
> Calls to shared objects can be lazy-loaded where the first call to the shared object invokes non-signal-safe functions
> such as `malloc()`. To avoid this, call these routines in `main()` ahead of a signal handler to "warm up" the library.
Because signal-safe tracing is an involved process, I have written up a comprehensive overview of
what is involved at [signal-safe-tracing.md](docs/signal-safe-tracing.md).
## Utility Types
A couple utility types are used to provide the library with a good interface.

View File

@ -11,14 +11,21 @@
# 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.
Stack traces from signal handlers can provide very helpful information for debugging application crashes, e.g. from
SIGSEGV or SIGTRAP handlers. Signal handlers are really restrictive environments as your application could be
interrupted by a signal at any point, including in the middle of malloc or buffered IO or while holding a lock.
Doing a stack trace in a signal handler is possible but it requires a lot of care. This is difficult to do correctly
and most examples 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).
It is not possible to resolve debug symbols safely in the process from a signal handler without heroic effort. In order
to produce a full trace there are three options:
1. Carefully save the object trace information to be resolved at a later time outside the signal handler
2. Write the object trace information to a file to be resolved later
3. Spawn a new process, communicate object trace information to that process, and have that process do the trace
resolution
For traces on segfaults, e.g., only options 2 and 3 are viable. The this guide will go over approach 3 and the cpptrace
safe API.
# Big-Picture
@ -57,7 +64,7 @@ namespace cpptrace {
struct safe_object_frame {
frame_ptr raw_address;
frame_ptr address_relative_to_object_start; // object base address must yet be added
frame_ptr address_relative_to_object_start;
char object_path[CPPTRACE_PATH_MAX + 1];
object_frame resolve() const; // To be called outside a signal handler. Not signal safe.
};