Skip to content

Commit 0d8aed6

Browse files
authored
Library for getting stacktraces from arbitrary exceptions (#147)
1 parent 3de5aea commit 0d8aed6

10 files changed

Lines changed: 637 additions & 3 deletions

File tree

build/Jamfile.v2

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,15 @@ lib boost_stacktrace_windbg_cached
132132
#<link>shared:<define>BOOST_STACKTRACE_DYN_LINK=1
133133
;
134134

135-
boost-install boost_stacktrace_noop boost_stacktrace_backtrace boost_stacktrace_addr2line boost_stacktrace_basic boost_stacktrace_windbg boost_stacktrace_windbg_cached ;
135+
lib boost_stacktrace_from_exception
136+
: # sources
137+
../src/from_exception.cpp
138+
: # requirements
139+
<warnings>all
140+
<target-os>linux:<library>dl
141+
: # default build
142+
: # usage-requirements
143+
#<link>shared:<define>BOOST_STACKTRACE_DYN_LINK=1
144+
;
145+
146+
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 ;

doc/stacktrace.qbk

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,64 @@ Generic recommendation is to *avoid signal handlers! Use* platform specific ways
128128
]
129129

130130

131+
[endsect]
132+
133+
[section Stacktrace from arbitrary exception]
134+
135+
[warning At the moment the functionality is only available for some of the
136+
popular cxx run-times for POSIX systems. Make sure that your platform
137+
is supported by running some tests.
138+
]
139+
140+
The library provides a way to get stacktrace from an exception as if the
141+
stacktrace was captured at the point the exception was thrown. Works even if
142+
the exception was thrown from a third party binary library.
143+
144+
Link with
145+
`boost_stacktrace_from_exception` library (or just `LD_PRELOAD` it!) and call
146+
boost::stacktrace::stacktrace::from_current_exception() to get the trace:
147+
148+
```
149+
#include <iostream>
150+
#include <stdexcept>
151+
#include <string_view>
152+
#include <boost/stacktrace.hpp>
153+
154+
void foo(std::string_view key);
155+
void bar(std::string_view key);
156+
157+
int main() {
158+
try {
159+
foo("test1");
160+
bar("test2");
161+
} catch (const std::exception& exc) {
162+
boost::stacktrace::stacktrace trace = std::stacktrace::from_current_exception(); // <---
163+
std::cerr << "Caught exception: " << exc.what() << ", trace:\n" << trace;
164+
}
165+
}
166+
```
167+
168+
The output of the above sample may be the following:
169+
170+
```
171+
Caught exception: std::map::at, trace:
172+
0# get_data_from_config(std::string_view) at /home/axolm/basic.cpp:600
173+
1# bar(std::string_view) at /home/axolm/basic.cpp:6
174+
2# main at /home/axolm/basic.cpp:17
175+
```
176+
177+
With the above technique a developer can locate the source file and the function
178+
that has thrown the exception without a debugger help. it is especially useful
179+
for testing in containers (github CI, other CIs), where the developer has no
180+
direct access to the testing environment and reproducing the issue is
181+
complicated.
182+
183+
Note that linking with `boost_stacktrace_from_exception` may increase memory
184+
consumption of the application, as the exceptions now additionally store traces.
185+
186+
At runtime switch boost::stacktrace::this_thread::set_capture_stacktraces_at_throw()
187+
allows to disable/enable capturing and storing traces in exceptions.
188+
131189
[endsect]
132190

133191

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

346406
[endsect]
347407

include/boost/stacktrace.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
#endif
1414

1515
#include <boost/stacktrace/frame.hpp>
16-
#include <boost/stacktrace/stacktrace.hpp> // Actually already includes all the headers
16+
#include <boost/stacktrace/stacktrace.hpp>
1717
#include <boost/stacktrace/safe_dump_to.hpp>
18+
#include <boost/stacktrace/this_thread.hpp>
1819

1920
#endif // BOOST_STACKTRACE_HPP

include/boost/stacktrace/stacktrace.hpp

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@
3535

3636
namespace boost { namespace stacktrace {
3737

38+
namespace impl {
39+
40+
#if defined(__GNUC__) && defined(__ELF__)
41+
42+
BOOST_NOINLINE BOOST_SYMBOL_VISIBLE __attribute__((weak))
43+
const char* current_exception_stacktrace() noexcept;
44+
45+
BOOST_NOINLINE BOOST_SYMBOL_VISIBLE __attribute__((weak))
46+
bool& ref_capture_stacktraces_at_throw() noexcept;
47+
48+
#endif
49+
50+
} // namespace impl
51+
3852
/// Class that on construction copies minimal information about call stack into its internals and provides access to that information.
3953
/// @tparam Allocator Allocator to use during stack capture.
4054
template <class Allocator>
@@ -65,7 +79,7 @@ class basic_stacktrace {
6579
}
6680

6781
BOOST_NOINLINE void init(std::size_t frames_to_skip, std::size_t max_depth) {
68-
BOOST_CONSTEXPR_OR_CONST std::size_t buffer_size = 128;
82+
constexpr std::size_t buffer_size = 128;
6983
if (!max_depth) {
7084
return;
7185
}
@@ -340,6 +354,44 @@ class basic_stacktrace {
340354

341355
return ret;
342356
}
357+
358+
/// Returns a basic_stacktrace object containing a stacktrace captured at
359+
/// the point where the currently handled exception was thrown by its
360+
/// initial throw-expression (i.e. not a rethrow), or an empty
361+
/// basic_stacktrace object if:
362+
///
363+
/// - the `boost_stacktrace_from_exception` library is not linked to the
364+
/// current binary, or
365+
/// - the initialization of stacktrace failed, or
366+
/// - stacktrace captures are not enabled for the throwing thread, or
367+
/// - no exception is being handled, or
368+
/// - due to implementation-defined reasons.
369+
///
370+
/// `alloc` is passed to the constructor of the stacktrace object.
371+
/// Rethrowing an exception using a throw-expression with no operand does
372+
/// not alter the captured stacktrace.
373+
///
374+
/// Implements https://wg21.link/p2370r1
375+
static basic_stacktrace<Allocator> from_current_exception(const allocator_type& alloc = allocator_type()) noexcept {
376+
// Matches the constant from implementation
377+
constexpr std::size_t kStacktraceDumpSize = 4096;
378+
379+
const char* trace = nullptr;
380+
#if defined(__GNUC__) && defined(__ELF__)
381+
if (impl::current_exception_stacktrace) {
382+
trace = impl::current_exception_stacktrace();
383+
}
384+
#endif
385+
386+
if (trace) {
387+
try {
388+
return basic_stacktrace<Allocator>::from_dump(trace, kStacktraceDumpSize, alloc);
389+
} catch (const std::exception&) {
390+
// ignore
391+
}
392+
}
393+
return basic_stacktrace<Allocator>{0, 0, alloc};
394+
}
343395
};
344396

345397
/// @brief Compares stacktraces for less, order is platform dependent.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright Antony Polukhin, 2023-2024.
2+
//
3+
// Distributed under the Boost Software License, Version 1.0. (See
4+
// accompanying file LICENSE_1_0.txt or copy at
5+
// http://www.boost.org/LICENSE_1_0.txt)
6+
7+
#ifndef BOOST_STACKTRACE_THIS_THREAD_HPP
8+
#define BOOST_STACKTRACE_THIS_THREAD_HPP
9+
10+
#include <boost/config.hpp>
11+
#ifdef BOOST_HAS_PRAGMA_ONCE
12+
# pragma once
13+
#endif
14+
15+
#include <boost/stacktrace/stacktrace.hpp>
16+
17+
namespace boost { namespace stacktrace { namespace this_thread {
18+
19+
/// @brief Invoking the function with the enable parameter equal to `true`
20+
/// enables capturing of stacktraces by the current thread of execution at
21+
/// exception object construction if the `boost_stacktrace_from_exception`
22+
/// library is linked to the current binary; disables otherwise.
23+
///
24+
/// Implements https://wg21.link/p2370r1
25+
inline void set_capture_stacktraces_at_throw(bool enable = true) noexcept {
26+
#if defined(__GNUC__) && defined(__ELF__)
27+
if (impl::ref_capture_stacktraces_at_throw) {
28+
impl::ref_capture_stacktraces_at_throw() = enable;
29+
}
30+
#endif
31+
(void)enable;
32+
}
33+
34+
/// @return whether the capturing of stacktraces by the current thread of
35+
/// execution is enabled and
36+
/// boost::stacktrace::basic_stacktrace::from_current_exception may return a
37+
/// non empty stacktrace.
38+
///
39+
/// Returns true if set_capture_stacktraces_at_throw(false) was not called
40+
/// and the `boost_stacktrace_from_exception` is linked to the current binary.
41+
///
42+
/// Implements https://wg21.link/p2370r1
43+
inline bool get_capture_stacktraces_at_throw() noexcept {
44+
#if defined(__GNUC__) && defined(__ELF__)
45+
if (impl::ref_capture_stacktraces_at_throw) {
46+
return impl::ref_capture_stacktraces_at_throw();
47+
}
48+
#endif
49+
return false;
50+
}
51+
52+
}}} // namespace boost::stacktrace::this_thread
53+
54+
#endif // BOOST_STACKTRACE_THIS_THREAD_HPP

src/exception_headers.h

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright Antony Polukhin, 2023-2024.
2+
//
3+
// Distributed under the Boost Software License, Version 1.0. (See
4+
// accompanying file LICENSE_1_0.txt or copy at
5+
// http://www.boost.org/LICENSE_1_0.txt)
6+
7+
#include <stddef.h>
8+
9+
#if defined(__x86_64__) || defined(_M_X64) || defined(__MINGW32__)
10+
#define BOOST_STACKTRACE_ALWAYS_STORE_IN_PADDING 1
11+
#else
12+
#define BOOST_STACKTRACE_ALWAYS_STORE_IN_PADDING 0
13+
#endif
14+
15+
16+
extern "C" {
17+
18+
// Developer note: helper to experiment with layouts of different
19+
// exception headers https://godbolt.org/z/rrcdPbh1P
20+
21+
// https://github.com/llvm/llvm-project/blob/b3dd14ce07f2750ae1068fe62abbf2f3bd2cade8/libcxxabi/src/cxa_exception.h
22+
struct cxa_exception_begin_llvm {
23+
const char* reserve;
24+
size_t referenceCount;
25+
};
26+
27+
static cxa_exception_begin_llvm* exception_begin_llvm_ptr(void* ptr) {
28+
size_t kExceptionBeginOffset = (
29+
sizeof(void*) == 8 ? 128 : 80
30+
);
31+
return (cxa_exception_begin_llvm*)((char*)ptr - kExceptionBeginOffset);
32+
}
33+
34+
// https://github.com/gcc-mirror/gcc/blob/5d2a360f0a541646abb11efdbabc33c6a04de7ee/libstdc%2B%2B-v3/libsupc%2B%2B/unwind-cxx.h#L100
35+
struct cxa_exception_begin_gcc {
36+
size_t referenceCount;
37+
const char* reserve;
38+
};
39+
40+
static cxa_exception_begin_gcc* exception_begin_gcc_ptr(void* ptr) {
41+
size_t kExceptionBeginOffset = (
42+
sizeof(void*) == 8 ? 128 : 96
43+
);
44+
return (cxa_exception_begin_gcc*)((char*)ptr - kExceptionBeginOffset);
45+
}
46+
47+
static void* get_current_exception_raw_ptr(void* exc_ptr) {
48+
// https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libstdc%2B%2B-v3/libsupc%2B%2B/eh_ptr.cc#L147
49+
return *static_cast<void**>(exc_ptr);
50+
}
51+
52+
} // extern "C"
53+
54+

0 commit comments

Comments
 (0)