...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
boost::stacktrace::stacktrace
contains methods for working with call-stack/backtraces/stacktraces. Here's
a small example:
#include <boost/stacktrace.hpp> // ... somewhere inside the `bar(int)` function that is called recursively: std::cout << boost::stacktrace::stacktrace();
In that example:
boost::stacktrace::
is the namespace that has all the classes and functions to work with
stacktraces
stacktrace()
is the default constructor call; constructor stores the current function
call sequence inside the stacktrace class.
Code from above will output something like this:
0# bar(int) at /path/to/source/file.cpp:70 1# bar(int) at /path/to/source/file.cpp:70 2# bar(int) at /path/to/source/file.cpp:70 3# bar(int) at /path/to/source/file.cpp:70 4# main at /path/to/main.cpp:93 5# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6 6# _start
Note | |
---|---|
By default the Stacktrace library is very conservative in methods to decode stacktrace. If your output does not look as fancy as in example from above, see section "Configuration and Build" for allowing advanced features of the library. |
Pretty often assertions provide not enough information to locate the problem. For example you can see the following message on out-of-range access:
../../../boost/array.hpp:123: T& boost::array<T, N>::operator[](boost::array<T, N>::size_type) [with T = int; long unsigned int N = 5ul]: Assertion '(i < N)&&("out of range")' failed. Aborted (core dumped)
That's not enough to locate the problem without debugger. There may be thousand code lines in real world examples and hundred places where that assertion could happen. Let's try to improve the assertions, and make them more informative:
// BOOST_ENABLE_ASSERT_DEBUG_HANDLER is defined for the whole project #include <stdexcept> // std::logic_error #include <iostream> // std::cerr #include <boost/stacktrace.hpp> namespace boost { inline void assertion_failed_msg(char const* expr, char const* msg, char const* function, char const* /*file*/, long /*line*/) { std::cerr << "Expression '" << expr << "' is false in function '" << function << "': " << (msg ? msg : "<...>") << ".\n" << "Backtrace:\n" << boost::stacktrace::stacktrace() << '\n'; std::abort(); } inline void assertion_failed(char const* expr, char const* function, char const* file, long line) { ::boost::assertion_failed_msg(expr, 0 /*nullptr*/, function, file, line); } } // namespace boost
We've defined the BOOST_ENABLE_ASSERT_DEBUG_HANDLER
macro for the whole project. Now all the BOOST_ASSERT
and BOOST_ASSERT_MSG
will
call our functions assertion_failed
and assertion_failed_msg
in case of failure. In assertion_failed_msg
we output information that was provided by the assertion macro and boost::stacktrace::stacktrace
:
Expression 'i < N' is false in function 'T& boost::array<T, N>::operator[](boost::array<T, N>::size_type) [with T = int; long unsigned int N = 5ul; boost::array<T, N>::reference = int&; boost::array<T, N>::size_type = long unsigned int]': out of range. Backtrace: 0# boost::assertion_failed_msg(char const*, char const*, char const*, char const*, long) at ../example/assert_handler.cpp:39 1# boost::array<int, 5ul>::operator[](unsigned long) at ../../../boost/array.hpp:124 2# bar(int) at ../example/assert_handler.cpp:17 3# foo(int) at ../example/assert_handler.cpp:25 4# bar(int) at ../example/assert_handler.cpp:17 5# foo(int) at ../example/assert_handler.cpp:25 6# main at ../example/assert_handler.cpp:54 7# 0x00007F991FD69F45 in /lib/x86_64-linux-gnu/libc.so.6 8# 0x0000000000401139
Now we do know the steps that led to the assertion and can find the error without debugger.
std::terminate
calls sometimes happen in programs.
Programmers usually wish to get as much information as possible on such incidents,
so having a stacktrace significantly improves debugging and fixing.
Here's how to write a terminate handler that dumps stacktrace:
#include <cstdlib> // std::abort #include <exception> // std::set_terminate #include <iostream> // std::cerr #include <boost/stacktrace.hpp> void my_terminate_handler() { try { std::cerr << boost::stacktrace::stacktrace(); } catch (...) {} std::abort(); }
Here's how to register it:
std::set_terminate(&my_terminate_handler);
Now we'll get the following output on std::terminate
call:
Previous run crashed: 0# my_terminate_handler(int) at ../example/terminate_handler.cpp:37 1# __cxxabiv1::__terminate(void (*)()) at ../../../../src/libstdc++-v3/libsupc++/eh_terminate.cc:48 2# 0x00007F3CE65E5901 in /usr/lib/x86_64-linux-gnu/libstdc++.so.6 3# bar(int) at ../example/terminate_handler.cpp:18 4# foo(int) at ../example/terminate_handler.cpp:22 5# bar(int) at ../example/terminate_handler.cpp:14 6# foo(int) at ../example/terminate_handler.cpp:22 7# main at ../example/terminate_handler.cpp:84 8# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6 9# 0x0000000000402209
Warning | |
---|---|
There's a temptation to write a signal handler that prints the stacktrace
on Signal handler is often invoked on a separate stack and trash is returned on attempt to get a trace! Generic recommendation is to avoid signal handlers! Use platform specific ways to store and decode core files. See "Theoretical async signal safety" for more info. |
Warning | |
---|---|
At the moment the functionality is only available for some of the popular C++ runtimes for POSIX systems with libbacktrace and for Windows. Make sure that your platform is supported by running some tests. |
The library provides a way to get stacktrace from an exception as if the stacktrace was captured at the point the exception was thrown. Works even if the exception was thrown from a third party binary library.
Link with boost_stacktrace_from_exception
library (or just LD_PRELOAD
it!) and call boost::stacktrace::stacktrace::from_current_exception() to
get the trace:
#include <iostream> #include <stdexcept> #include <string_view> #include <boost/stacktrace.hpp> void foo(std::string_view key); void bar(std::string_view key); int main() { try { foo("test1"); bar("test2"); } catch (const std::exception& exc) { boost::stacktrace::stacktrace trace = boost::stacktrace::stacktrace::from_current_exception(); // <--- std::cerr << "Caught exception: " << exc.what() << ", trace:\n" << trace; } }
The output of the above sample may be the following:
Caught exception: std::map::at, trace: 0# get_data_from_config(std::string_view) at /home/axolm/basic.cpp:600 1# bar(std::string_view) at /home/axolm/basic.cpp:6 2# main at /home/axolm/basic.cpp:17
With the above technique a developer can locate the source file and the function that has thrown the exception without a debugger help. it is especially useful for testing in containers (github CI, other CIs), where the developer has no direct access to the testing environment and reproducing the issue is complicated.
Note that linking with boost_stacktrace_from_exception
may increase memory consumption of the application, as the exceptions now
additionally store traces.
At runtime switch boost::stacktrace::this_thread::set_capture_stacktraces_at_throw() allows to disable/enable capturing and storing traces in exceptions.
To disable the boost_stacktrace_from_exception
library builds the boost.stacktrace.from_exception=off
option,
for example ./b2
boost.stacktrace.from_exception=off
.
You can provide more information along with exception by embedding stacktraces into the exception. There are many ways to do that, here's how to do that using Boost.Exception:
boost::error_info
typedef that holds the stacktrace:
#include <boost/stacktrace.hpp> #include <boost/exception/all.hpp> typedef boost::error_info<struct tag_stacktrace, boost::stacktrace::stacktrace> traced;
template <class E> void throw_with_trace(const E& e) { throw boost::enable_error_info(e) << traced(boost::stacktrace::stacktrace()); }
throw_with_trace(E);
instead of just throw
E;
:
if (i >= 4) throw_with_trace(std::out_of_range("'i' must be less than 4 in oops()")); if (i <= 0) throw_with_trace(std::logic_error("'i' must be greater than zero in oops()"));
try { foo(5); // testing assert handler } catch (const std::exception& e) { std::cerr << e.what() << '\n'; const boost::stacktrace::stacktrace* st = boost::get_error_info<traced>(e); if (st) { std::cerr << *st << '\n'; } }
Code from above will output:
'i' must not be greater than zero in oops() 0# void throw_with_trace<std::logic_error>(std::logic_error const&) at ../example/throwing_st.cpp:22 1# oops(int) at ../example/throwing_st.cpp:38 2# bar(int) at ../example/throwing_st.cpp:54 3# foo(int) at ../example/throwing_st.cpp:59 4# bar(int) at ../example/throwing_st.cpp:49 5# foo(int) at ../example/throwing_st.cpp:59 6# main at ../example/throwing_st.cpp:76 7# 0x00007FAC113BEF45 in /lib/x86_64-linux-gnu/libc.so.6 8# 0x0000000000402ED9
At some point arises a requirement to easily enable/disable stacktraces for a whole project. That could be easily achieved.
Just define BOOST_STACKTRACE_LINK for a whole project. Now you can enable/disable stacktraces by just linking with different libraries:
boost_stacktrace_noop
to disable backtracing
boost_stacktrace_*
libraries
See section "Configuration and Build" for more info.
boost::stacktrace::stacktrace
provides access to individual frames
of the stacktrace, so that you could save stacktrace information in your
own format. Consider the example, that saves only function addresses of each
frame:
#include <boost/stacktrace.hpp> #include <iostream> // std::cout namespace bs = boost::stacktrace; void dump_compact(const bs::stacktrace& st) { for (bs::frame frame: st) { std::cout << frame.address() << ','; } std::cout << std::endl; }
Code from above will output:
0x7fbcfd17f6b5,0x400d4a,0x400d61,0x400d61,0x400d61,0x400d61,0x400d77,0x400cbf,0x400dc0,0x7fbcfc82d830,0x400a79,
boost::stacktrace::frame
provides information about functions. You may construct that class from function
pointer and get the function name at runtime:
#include <signal.h> // ::signal #include <boost/stacktrace/frame.hpp> #include <iostream> // std::cerr #include <cstdlib> // std::exit void print_signal_handler_and_exit() { typedef void(*function_t)(int); function_t old_signal_function = ::signal(SIGSEGV, SIG_DFL); boost::stacktrace::frame f(old_signal_function); std::cout << f << std::endl; std::exit(0); }
Code from above will output:
my_signal_handler(int) at boost/libs/stacktrace/example/debug_function.cpp:21
You may override the behavior of default stacktrace output operator by defining
the macro from Boost.Config BOOST_USER_CONFIG
to point to a file like following:
#ifndef USER_CONFIG_HPP #define USER_CONFIG_HPP #include <boost/stacktrace/stacktrace_fwd.hpp> #include <iosfwd> namespace boost { namespace stacktrace { template <class CharT, class TraitsT, class Allocator> std::basic_ostream<CharT, TraitsT>& do_stream_st(std::basic_ostream<CharT, TraitsT>& os, const basic_stacktrace<Allocator>& bt); template <class CharT, class TraitsT> std::basic_ostream<CharT, TraitsT>& operator<<(std::basic_ostream<CharT, TraitsT>& os, const stacktrace& bt) { return do_stream_st(os, bt); } }} // namespace boost::stacktrace #endif // USER_CONFIG_HPP
Implementation of do_stream_st
may be the following:
namespace boost { namespace stacktrace { template <class CharT, class TraitsT, class Allocator> std::basic_ostream<CharT, TraitsT>& do_stream_st(std::basic_ostream<CharT, TraitsT>& os, const basic_stacktrace<Allocator>& bt) { const std::streamsize w = os.width(); const std::size_t frames = bt.size(); for (std::size_t i = 0; i < frames; ++i) { os.width(2); os << i; os.width(w); os << "# "; os << bt[i].name(); os << '\n'; } return os; } }} // namespace boost::stacktrace
Code from above will output:
Terminate called: 0# bar(int) 1# foo(int) 2# bar(int) 3# foo(int)