My best async signal safe attempt so far
Let me know if it is not actually safe. I could not yet find a way to show line numbers.
stacktrace_on_signal_safe.cpp
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#define TRACE_MAX 1024
void handler(int sig) {
(void)sig;
void *array[TRACE_MAX];
size_t size;
const char msg[] = "failed with a signal\n";
size = backtrace(array, TRACE_MAX);
if(write(STDERR_FILENO, msg, sizeof(msg))){};
backtrace_symbols_fd(array, size, STDERR_FILENO);
_Exit(1);
}
void my_func_2(void) {
*((int*)0) = 1;
}
void my_func_1(double f) {
(void)f;
my_func_2();
}
void my_func_1(int i) {
(void)i;
my_func_2();
}
int main() {
/* Make a dummy call to `backtrace` to load libgcc because man backrace says:
* * backtrace() and backtrace_symbols_fd() don't call malloc() explicitly, but they are part of libgcc, which gets loaded dynamically when first used. Dynamic loading usually triggers a call to mal‐
* loc(3). If you need certain calls to these two functions to not allocate memory (in signal handlers, for example), you need to make sure libgcc is loaded beforehand.
*/
void *dummy[1];
backtrace(dummy, 1);
signal(SIGSEGV, handler);
my_func_1(1);
}
Compile and run:
g++ -ggdb3 -O2 -std=c++11 -Wall -Wextra -pedantic -rdynamic -o stacktrace_on_signal_safe.out stacktrace_on_signal_safe.cpp
./stacktrace_on_signal_safe.out
-rdynamic
is needed to get the function names:
failed with a signal
./stacktrace_on_signal_safe.out(_Z7handleri+0x6e)[0x56239398928e]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f04b1459520]
./stacktrace_on_signal_safe.out(main+0x38)[0x562393989118]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f04b1440d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f04b1440e40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x562393989155]
We can then pipe it to c++filt
to demangle:
./stacktrace_on_signal_safe.out |& c++filt
giving:
failed with a signal
/stacktrace_on_signal_safe.out(handler(int)+0x6e)[0x55b6df43f28e]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f40d4167520]
./stacktrace_on_signal_safe.out(main+0x38)[0x55b6df43f118]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f40d414ed90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f40d414ee40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x55b6df43f155]
Several levels are missing due to optimizations, with -O0
we get a fuller:
/stacktrace_on_signal_safe.out(handler(int)+0x76)[0x55d39b68325f]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f4d8ffdd520]
./stacktrace_on_signal_safe.out(my_func_2()+0xd)[0x55d39b6832bb]
./stacktrace_on_signal_safe.out(my_func_1(int)+0x14)[0x55d39b6832f1]
./stacktrace_on_signal_safe.out(main+0x4a)[0x55d39b68333e]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f4d8ffc4d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f4d8ffc4e40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x55d39b683125]
Line numbers are not present, but we can get them with addr2line
. This requires building without -rdynamic
:
g++ -ggdb3 -O0 -std=c++23 -Wall -Wextra -pedantic -o stacktrace_on_signal_safe.out stacktrace_on_signal_safe.cpp
./stacktrace_on_signal_safe.out |& sed -r 's/.*\(//;s/\).*//' | addr2line -C -e stacktrace_on_signal_safe.out -f
producing:
??
??:0
handler(int)
/home/ciro/stacktrace_on_signal_safe.cpp:14
??
??:0
my_func_2()
/home/ciro/stacktrace_on_signal_safe.cpp:22
my_func_1(i
/home/ciro/stacktrace_on_signal_safe.cpp:33
main
/home/ciro/stacktrace_on_signal_safe.cpp:45
??
??:0
??
??:0
_start
??:?
awk
parses the +<addr>
numbers out o the non -rdynamic
output:
./stacktrace_on_signal_safe.out(+0x125f)[0x55984828825f]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f8644a1e520]
./stacktrace_on_signal_safe.out(+0x12bb)[0x5598482882bb]
./stacktrace_on_signal_safe.out(+0x12f1)[0x5598482882f1]
./stacktrace_on_signal_safe.out(+0x133e)[0x55984828833e]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f8644a05d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f8644a05e40]
./stacktrace_on_signal_safe.out(+0x1125)[0x559848288125]
If you also want to print the actual signal number to stdout, here's an async signal safe implementation int to string: Print int from signal handler using write or async-safe functions since printf
is not.
Tested on Ubuntu 22.04.
C++23 <stacktrace>
Like many other answers, this section ignores async signal safe aspects of the problem, which could lead your code to deadlock on crash, which could be serious. We can only hope one day the C++ standard will add a boost::stacktrace::safe_dump_to
-like function to solve this once and for all.
This will be the generally superior C++ stacktrace option moving forward as mentioned at: How to print a stack trace whenever a certain function is called as it shows line numbers and does demangling for us automatically.
stacktrace_on_signal.cpp
#include <stacktrace>
#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void handler(int sig) {
(void)sig;
/* De-register this signal in the hope of avoiding infinite loops
* if asyns signal unsafe things fail later on. But can likely still deadlock. */
signal(sig, SIG_DFL);
// std::stacktrace::current
std::cout << std::stacktrace::current();
// C99 async signal safe version of exit().
_Exit(1);
}
void my_func_2(void) {
*((int*)0) = 1;
}
void my_func_1(double f) {
(void)f;
my_func_2();
}
void my_func_1(int i) {
(void)i;
my_func_2();
}
int main() {
signal(SIGSEGV, handler);
my_func_1(1);
}
Compile and run:
g++ -ggdb3 -O2 -std=c++23 -Wall -Wextra -pedantic -o stacktrace_on_signal.out stacktrace_on_signal.cpp -lstdc++_libbacktrace
./stacktrace_on_signal.out
Output on GCC 12.1 compiled from source, Ubuntu 22.04:
0# handler(int) at /home/ciro/stacktrace_on_signal.cpp:11
1# at :0
2# my_func_2() at /home/ciro/stacktrace_on_signal.cpp:16
3# at :0
4# at :0
5# at :0
6#
I think it missed my_func_1
due to optimization being turned on, and there is in general nothing we can do about that AFAIK. With -O0
instead it is better:
0# handler(int) at /home/ciro/stacktrace_on_signal.cpp:11
1# at :0
2# my_func_2() at /home/ciro/stacktrace_on_signal.cpp:16
3# my_func_1(int) at /home/ciro/stacktrace_on_signal.cpp:26
4# at /home/ciro/stacktrace_on_signal.cpp:31
5# at :0
6# at :0
7# at :0
8#
but not sure why main
didn't show up there.
backtrace_simple
https://github.com/gcc-mirror/gcc/blob/releases/gcc-12.1.0/libstdc%2B%2B-v3/src/libbacktrace/backtrace-supported.h.in#L45 mentions that backtrace_simple
is safe:
/* BACKTRACE_USES_MALLOC will be #define'd as 1 if the backtrace
library will call malloc as it works, 0 if it will call mmap
instead. This may be used to determine whether it is safe to call
the backtrace functions from a signal handler. In general this
only applies to calls like backtrace and backtrace_pcinfo. It does
not apply to backtrace_simple, which never calls malloc. It does
not apply to backtrace_print, which always calls fprintf and
therefore malloc. */
but it does not appear very convenient for usage, mostly an internal tool.
std::basic_stacktrace
This is what std::stacktrace
is based on according to: https://en.cppreference.com/w/cpp/utility/basic_stacktrace
It has an allocator parameter which cppreference describes as:
Support for custom allocators is provided for using basic_stacktrace on a hot path or in embedded environments. Users can allocate stacktrace_entry objects on the stack or in some other place, where appropriate.
so I wonder if basic_stacktrace
is itself async signal safe, and if it wouldn't be possible to make a version of std::stacktrace
that is also with a custom allocator, e.g. either something that:
- writes to a file on disk like
boost::stacktrace::safe_dump_to
- or writes to some pre-alocated stack buffer with some maximum size
https://apolukhin.github.io/papers/stacktrace_r1.html might be the proposal that got in, mentions:
Note about signal safety: this proposal does not attempt to provide a signal-safe solution for capturing and decoding stacktraces. Such functionality currently is not implementable on some of the popular platforms. However, the paper attempts to provide extensible solution, that may be made signal safe some day by providing a signal safe allocator and changing the stacktrace implementation details.
Just getting the core dump instead?
The core dump allows you to inspect memory with GDB: How do I analyze a program's core dump file with GDB when it has command-line parameters? so it is more powerful than just having the trace.
Just make sure you enable it properly, notably on Ubuntu 22.04 you need:
echo 'core' | sudo tee /proc/sys/kernel/core_pattern
or to learn to use apport, see also: https://askubuntu.com/questions/1349047/where-do-i-find-core-dump-files-and-how-do-i-view-and-analyze-the-backtrace-st/1442665#1442665