Skip to content

Commit

Permalink
Library for getting stacktraces from arbitrary exceptions (#147)
Browse files Browse the repository at this point in the history
  • Loading branch information
apolukhin authored Feb 7, 2024
1 parent 3de5aea commit 0d8aed6
Show file tree
Hide file tree
Showing 10 changed files with 637 additions and 3 deletions.
13 changes: 12 additions & 1 deletion build/Jamfile.v2
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,15 @@ lib boost_stacktrace_windbg_cached
#<link>shared:<define>BOOST_STACKTRACE_DYN_LINK=1
;

boost-install boost_stacktrace_noop boost_stacktrace_backtrace boost_stacktrace_addr2line boost_stacktrace_basic boost_stacktrace_windbg boost_stacktrace_windbg_cached ;
lib boost_stacktrace_from_exception
: # sources
../src/from_exception.cpp
: # requirements
<warnings>all
<target-os>linux:<library>dl
: # default build
: # usage-requirements
#<link>shared:<define>BOOST_STACKTRACE_DYN_LINK=1
;

boost-install boost_stacktrace_noop boost_stacktrace_backtrace boost_stacktrace_addr2line boost_stacktrace_basic boost_stacktrace_windbg boost_stacktrace_windbg_cached boost_stacktrace_from_exception ;
60 changes: 60 additions & 0 deletions doc/stacktrace.qbk
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,64 @@ Generic recommendation is to *avoid signal handlers! Use* platform specific ways
]


[endsect]

[section Stacktrace from arbitrary exception]

[warning At the moment the functionality is only available for some of the
popular cxx run-times for POSIX systems. 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 = std::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.

[endsect]


Expand Down Expand Up @@ -342,6 +400,8 @@ In order of helping and advising:
* Great thanks to Nat Goodspeed for requesting [classref boost::stacktrace::frame] like class.
* Great thanks to Niall Douglas for making an initial review, helping with some platforms and giving great hints on library design.
* Great thanks to all the library reviewers.
* Great thanks to Andrei Nekrashevich for prototyping the idea of stacktraces
from arbitrary exception in `libsfe`.

[endsect]

Expand Down
3 changes: 2 additions & 1 deletion include/boost/stacktrace.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
#endif

#include <boost/stacktrace/frame.hpp>
#include <boost/stacktrace/stacktrace.hpp> // Actually already includes all the headers
#include <boost/stacktrace/stacktrace.hpp>
#include <boost/stacktrace/safe_dump_to.hpp>
#include <boost/stacktrace/this_thread.hpp>

#endif // BOOST_STACKTRACE_HPP
54 changes: 53 additions & 1 deletion include/boost/stacktrace/stacktrace.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@

namespace boost { namespace stacktrace {

namespace impl {

#if defined(__GNUC__) && defined(__ELF__)

BOOST_NOINLINE BOOST_SYMBOL_VISIBLE __attribute__((weak))
const char* current_exception_stacktrace() noexcept;

BOOST_NOINLINE BOOST_SYMBOL_VISIBLE __attribute__((weak))
bool& ref_capture_stacktraces_at_throw() noexcept;

#endif

} // namespace impl

/// Class that on construction copies minimal information about call stack into its internals and provides access to that information.
/// @tparam Allocator Allocator to use during stack capture.
template <class Allocator>
Expand Down Expand Up @@ -65,7 +79,7 @@ class basic_stacktrace {
}

BOOST_NOINLINE void init(std::size_t frames_to_skip, std::size_t max_depth) {
BOOST_CONSTEXPR_OR_CONST std::size_t buffer_size = 128;
constexpr std::size_t buffer_size = 128;
if (!max_depth) {
return;
}
Expand Down Expand Up @@ -340,6 +354,44 @@ class basic_stacktrace {

return ret;
}

/// Returns a basic_stacktrace object containing a stacktrace captured at
/// the point where the currently handled exception was thrown by its
/// initial throw-expression (i.e. not a rethrow), or an empty
/// basic_stacktrace object if:
///
/// - the `boost_stacktrace_from_exception` library is not linked to the
/// current binary, or
/// - the initialization of stacktrace failed, or
/// - stacktrace captures are not enabled for the throwing thread, or
/// - no exception is being handled, or
/// - due to implementation-defined reasons.
///
/// `alloc` is passed to the constructor of the stacktrace object.
/// Rethrowing an exception using a throw-expression with no operand does
/// not alter the captured stacktrace.
///
/// Implements https://wg21.link/p2370r1
static basic_stacktrace<Allocator> from_current_exception(const allocator_type& alloc = allocator_type()) noexcept {
// Matches the constant from implementation
constexpr std::size_t kStacktraceDumpSize = 4096;

const char* trace = nullptr;
#if defined(__GNUC__) && defined(__ELF__)
if (impl::current_exception_stacktrace) {
trace = impl::current_exception_stacktrace();
}
#endif

if (trace) {
try {
return basic_stacktrace<Allocator>::from_dump(trace, kStacktraceDumpSize, alloc);
} catch (const std::exception&) {
// ignore
}
}
return basic_stacktrace<Allocator>{0, 0, alloc};
}
};

/// @brief Compares stacktraces for less, order is platform dependent.
Expand Down
54 changes: 54 additions & 0 deletions include/boost/stacktrace/this_thread.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright Antony Polukhin, 2023-2024.
//
// Distributed under the Boost Software License, Version 1.0. (See
// accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#ifndef BOOST_STACKTRACE_THIS_THREAD_HPP
#define BOOST_STACKTRACE_THIS_THREAD_HPP

#include <boost/config.hpp>
#ifdef BOOST_HAS_PRAGMA_ONCE
# pragma once
#endif

#include <boost/stacktrace/stacktrace.hpp>

namespace boost { namespace stacktrace { namespace this_thread {

/// @brief Invoking the function with the enable parameter equal to `true`
/// enables capturing of stacktraces by the current thread of execution at
/// exception object construction if the `boost_stacktrace_from_exception`
/// library is linked to the current binary; disables otherwise.
///
/// Implements https://wg21.link/p2370r1
inline void set_capture_stacktraces_at_throw(bool enable = true) noexcept {
#if defined(__GNUC__) && defined(__ELF__)
if (impl::ref_capture_stacktraces_at_throw) {
impl::ref_capture_stacktraces_at_throw() = enable;
}
#endif
(void)enable;
}

/// @return whether the capturing of stacktraces by the current thread of
/// execution is enabled and
/// boost::stacktrace::basic_stacktrace::from_current_exception may return a
/// non empty stacktrace.
///
/// Returns true if set_capture_stacktraces_at_throw(false) was not called
/// and the `boost_stacktrace_from_exception` is linked to the current binary.
///
/// Implements https://wg21.link/p2370r1
inline bool get_capture_stacktraces_at_throw() noexcept {
#if defined(__GNUC__) && defined(__ELF__)
if (impl::ref_capture_stacktraces_at_throw) {
return impl::ref_capture_stacktraces_at_throw();
}
#endif
return false;
}

}}} // namespace boost::stacktrace::this_thread

#endif // BOOST_STACKTRACE_THIS_THREAD_HPP
54 changes: 54 additions & 0 deletions src/exception_headers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright Antony Polukhin, 2023-2024.
//
// Distributed under the Boost Software License, Version 1.0. (See
// accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#include <stddef.h>

#if defined(__x86_64__) || defined(_M_X64) || defined(__MINGW32__)
#define BOOST_STACKTRACE_ALWAYS_STORE_IN_PADDING 1
#else
#define BOOST_STACKTRACE_ALWAYS_STORE_IN_PADDING 0
#endif


extern "C" {

// Developer note: helper to experiment with layouts of different
// exception headers https://godbolt.org/z/rrcdPbh1P

// https://github.com/llvm/llvm-project/blob/b3dd14ce07f2750ae1068fe62abbf2f3bd2cade8/libcxxabi/src/cxa_exception.h
struct cxa_exception_begin_llvm {
const char* reserve;
size_t referenceCount;
};

static cxa_exception_begin_llvm* exception_begin_llvm_ptr(void* ptr) {
size_t kExceptionBeginOffset = (
sizeof(void*) == 8 ? 128 : 80
);
return (cxa_exception_begin_llvm*)((char*)ptr - kExceptionBeginOffset);
}

// https://github.com/gcc-mirror/gcc/blob/5d2a360f0a541646abb11efdbabc33c6a04de7ee/libstdc%2B%2B-v3/libsupc%2B%2B/unwind-cxx.h#L100
struct cxa_exception_begin_gcc {
size_t referenceCount;
const char* reserve;
};

static cxa_exception_begin_gcc* exception_begin_gcc_ptr(void* ptr) {
size_t kExceptionBeginOffset = (
sizeof(void*) == 8 ? 128 : 96
);
return (cxa_exception_begin_gcc*)((char*)ptr - kExceptionBeginOffset);
}

static void* get_current_exception_raw_ptr(void* exc_ptr) {
// https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libstdc%2B%2B-v3/libsupc%2B%2B/eh_ptr.cc#L147
return *static_cast<void**>(exc_ptr);
}

} // extern "C"


Loading

0 comments on commit 0d8aed6

Please sign in to comment.