Skip to content

Commit 7e16571

Browse files
authored
[lldb][libc++] Hide all libc++ implementation details from stacktraces (#108870)
This commit changes the libc++ frame recognizer to hide implementation details of libc++ more aggressively. The applied heuristic is rather straightforward: We consider every function name starting with `__` as an implementation detail. This works pretty neatly for `std::invoke`, `std::function`, `std::sort`, `std::map::emplace` and many others. Also, this should align quite nicely with libc++'s general coding convention of using the `__` for their implementation details, thereby keeping the future maintenance effort low. However, this heuristic by itself does not work in 100% of the cases: E.g., `std::ranges::sort` is not a function, but an object with an overloaded `operator()`, which means that there is no actual call `std::ranges::sort` in the call stack. Instead, there is a `std::ranges::__sort::operator()` call. To make sure that we don't hide this stack frame, we never hide the frame which represents the entry point from user code into libc++ code
1 parent 2f22656 commit 7e16571

File tree

7 files changed

+211
-96
lines changed

7 files changed

+211
-96
lines changed

libcxx/docs/UserDocumentation.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,35 @@ Third-party Integrations
355355

356356
Libc++ provides integration with a few third-party tools.
357357

358+
Debugging libc++ internals in LLDB
359+
----------------------------------
360+
361+
LLDB hides the implementation details of libc++ by default.
362+
363+
E.g., when setting a breakpoint in a comparator passed to ``std::sort``, the
364+
backtrace will read as
365+
366+
.. code-block::
367+
368+
(lldb) thread backtrace
369+
* thread #1, name = 'a.out', stop reason = breakpoint 3.1
370+
* frame #0: 0x000055555555520e a.out`my_comparator(a=1, b=8) at test-std-sort.cpp:6:3
371+
frame #7: 0x0000555555555615 a.out`void std::__1::sort[abi:ne200000]<std::__1::__wrap_iter<int*>, bool (*)(int, int)>(__first=(item = 8), __last=(item = 0), __comp=(a.out`my_less(int, int) at test-std-sort.cpp:5)) at sort.h:1003:3
372+
frame #8: 0x000055555555531a a.out`main at test-std-sort.cpp:24:3
373+
374+
Note how the caller of ``my_comparator`` is shown as ``std::sort``. Looking at
375+
the frame numbers, we can see that frames #1 until #6 were hidden. Those frames
376+
represent internal implementation details such as ``__sort4`` and similar
377+
utility functions.
378+
379+
To also show those implementation details, use ``thread backtrace -u``.
380+
Alternatively, to disable those compact backtraces, use ``frame recognizer list``
381+
and ``frame recognizer disable`` on the "libc++ frame recognizer".
382+
383+
Futhermore, stepping into libc++ functions is disabled by default. This is controlled via the
384+
setting ``target.process.thread.step-avoid-regexp`` which defaults to ``^std::`` and can be
385+
disabled using ``settings set target.process.thread.step-avoid-regexp ""``.
386+
358387
GDB Pretty printers for libc++
359388
------------------------------
360389

lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ char CPPLanguageRuntime::ID = 0;
4545
/// A frame recognizer that is installed to hide libc++ implementation
4646
/// details from the backtrace.
4747
class LibCXXFrameRecognizer : public StackFrameRecognizer {
48-
std::array<RegularExpression, 4> m_hidden_regex;
48+
std::array<RegularExpression, 2> m_hidden_regex;
4949
RecognizedStackFrameSP m_hidden_frame;
5050

5151
struct LibCXXHiddenFrame : public RecognizedStackFrame {
@@ -55,28 +55,17 @@ class LibCXXFrameRecognizer : public StackFrameRecognizer {
5555
public:
5656
LibCXXFrameRecognizer()
5757
: m_hidden_regex{
58-
// internal implementation details of std::function
58+
// internal implementation details in the `std::` namespace
5959
// std::__1::__function::__alloc_func<void (*)(), std::__1::allocator<void (*)()>, void ()>::operator()[abi:ne200000]
6060
// std::__1::__function::__func<void (*)(), std::__1::allocator<void (*)()>, void ()>::operator()
6161
// std::__1::__function::__value_func<void ()>::operator()[abi:ne200000]() const
62-
RegularExpression{""
63-
R"(^std::__[^:]*::)" // Namespace.
64-
R"(__function::.*::operator\(\))"},
65-
// internal implementation details of std::function in ABI v2
6662
// std::__2::__function::__policy_invoker<void (int, int)>::__call_impl[abi:ne200000]<std::__2::__function::__default_alloc_func<int (*)(int, int), int (int, int)>>
67-
RegularExpression{""
68-
R"(^std::__[^:]*::)" // Namespace.
69-
R"(__function::.*::__call_impl)"},
70-
// internal implementation details of std::invoke
71-
// std::__1::__invoke[abi:ne200000]<void (*&)()>
72-
RegularExpression{
73-
R"(^std::__[^:]*::)" // Namespace.
74-
R"(__invoke)"},
75-
// internal implementation details of std::invoke
76-
// std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:ne200000]<void (*&)()>
77-
RegularExpression{
78-
R"(^std::__[^:]*::)" // Namespace.
79-
R"(__invoke_void_return_wrapper<.*>::__call)"}
63+
// std::__1::__invoke[abi:ne200000]<void (*&)()>
64+
// std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:ne200000]<void (*&)()>
65+
RegularExpression{R"(^std::__[^:]*::__)"},
66+
// internal implementation details in the `std::ranges` namespace
67+
// std::__1::ranges::__sort::__sort_fn_impl[abi:ne200000]<std::__1::__wrap_iter<int*>, std::__1::__wrap_iter<int*>, bool (*)(int, int), std::__1::identity>
68+
RegularExpression{R"(^std::__[^:]*::ranges::__)"},
8069
},
8170
m_hidden_frame(new LibCXXHiddenFrame()) {}
8271

@@ -90,9 +79,27 @@ class LibCXXFrameRecognizer : public StackFrameRecognizer {
9079
if (!sc.function)
9180
return {};
9281

93-
for (RegularExpression &r : m_hidden_regex)
94-
if (r.Execute(sc.function->GetNameNoArguments()))
82+
// Check if we have a regex match
83+
for (RegularExpression &r : m_hidden_regex) {
84+
if (!r.Execute(sc.function->GetNameNoArguments()))
85+
continue;
86+
87+
// Only hide this frame if the immediate caller is also within libc++.
88+
lldb::ThreadSP thread_sp = frame_sp->GetThread();
89+
if (!thread_sp)
90+
return {};
91+
lldb::StackFrameSP parent_frame_sp =
92+
thread_sp->GetStackFrameAtIndex(frame_sp->GetFrameIndex() + 1);
93+
if (!parent_frame_sp)
94+
return {};
95+
const auto &parent_sc =
96+
parent_frame_sp->GetSymbolContext(lldb::eSymbolContextFunction);
97+
if (!parent_sc.function)
98+
return {};
99+
if (parent_sc.function->GetNameNoArguments().GetStringRef().starts_with(
100+
"std::"))
95101
return m_hidden_frame;
102+
}
96103

97104
return {};
98105
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
CXX_SOURCES := main.cpp
22
USE_LIBCPP := 1
3-
CXXFLAGS_EXTRAS := -std=c++17
3+
CXXFLAGS_EXTRAS := -std=c++20
44

55
include Makefile.rules
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import lldb
2+
from lldbsuite.test.decorators import *
3+
from lldbsuite.test.lldbtest import *
4+
from lldbsuite.test import lldbutil
5+
6+
7+
class LibCxxInternalsRecognizerTestCase(TestBase):
8+
NO_DEBUG_INFO_TESTCASE = True
9+
10+
@add_test_categories(["libc++"])
11+
def test_frame_recognizer(self):
12+
"""Test that implementation details of libc++ are hidden"""
13+
self.build()
14+
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
15+
self, "break here", lldb.SBFileSpec("main.cpp")
16+
)
17+
18+
expected_parents = {
19+
"sort_less(int, int)": ["::sort", "test_algorithms"],
20+
# `std::ranges::sort` is implemented as an object of types `__sort`.
21+
# We never hide the frame of the entry-point into the standard library, even
22+
# if the name starts with `__` which usually indicates an internal function.
23+
"ranges_sort_less(int, int)": [
24+
"ranges::__sort::operator()",
25+
"test_algorithms",
26+
],
27+
# `ranges::views::transform` internally uses `std::invoke`, and that
28+
# call also shows up in the stack trace
29+
"view_transform(int)": [
30+
"::invoke",
31+
"ranges::transform_view",
32+
"test_algorithms",
33+
],
34+
# Various types of `invoke` calls
35+
"consume_number(int)": ["::invoke", "test_invoke"],
36+
"invoke_add(int, int)": ["::invoke", "test_invoke"],
37+
"Callable::member_function(int) const": ["::invoke", "test_invoke"],
38+
"Callable::operator()(int) const": ["::invoke", "test_invoke"],
39+
# Containers
40+
"MyKey::operator<(MyKey const&) const": [
41+
"less",
42+
"::emplace",
43+
"test_containers",
44+
],
45+
}
46+
stop_set = set()
47+
while process.GetState() != lldb.eStateExited:
48+
fn = thread.GetFrameAtIndex(0).GetFunctionName()
49+
stop_set.add(fn)
50+
self.assertIn(fn, expected_parents.keys())
51+
frame_id = 1
52+
for expected_parent in expected_parents[fn]:
53+
# Skip all hidden frames
54+
while (
55+
frame_id < thread.GetNumFrames()
56+
and thread.GetFrameAtIndex(frame_id).IsHidden()
57+
):
58+
frame_id = frame_id + 1
59+
# Expect the correct parent frame
60+
self.assertIn(
61+
expected_parent, thread.GetFrameAtIndex(frame_id).GetFunctionName()
62+
)
63+
frame_id = frame_id + 1
64+
process.Continue()
65+
66+
# Make sure that we actually verified all intended scenarios
67+
self.assertEqual(len(stop_set), len(expected_parents))
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#include <algorithm>
2+
#include <functional>
3+
#include <map>
4+
#include <ranges>
5+
#include <vector>
6+
7+
bool sort_less(int a, int b) {
8+
__builtin_printf("break here");
9+
return a < b;
10+
}
11+
12+
bool ranges_sort_less(int a, int b) {
13+
__builtin_printf("break here");
14+
return a < b;
15+
}
16+
17+
int view_transform(int a) {
18+
__builtin_printf("break here");
19+
return a * a;
20+
}
21+
22+
void test_algorithms() {
23+
std::vector<int> vec{8, 1, 3, 2};
24+
25+
// The internal frames for `std::sort` should be hidden
26+
std::sort(vec.begin(), vec.end(), sort_less);
27+
28+
// The internal frames for `ranges::sort` should be hidden
29+
std::ranges::sort(vec.begin(), vec.end(), ranges_sort_less);
30+
31+
// Same for views
32+
for (auto x : vec | std::ranges::views::transform(view_transform)) {
33+
// no-op
34+
}
35+
}
36+
37+
void consume_number(int i) { __builtin_printf("break here"); }
38+
39+
int invoke_add(int i, int j) {
40+
__builtin_printf("break here");
41+
return i + j;
42+
}
43+
44+
struct Callable {
45+
Callable(int num) : num_(num) {}
46+
void operator()(int i) const { __builtin_printf("break here"); }
47+
void member_function(int i) const { __builtin_printf("break here"); }
48+
int num_;
49+
};
50+
51+
void test_invoke() {
52+
// Invoke a void-returning function
53+
std::invoke(consume_number, -9);
54+
55+
// Invoke a non-void-returning function
56+
std::invoke(invoke_add, 1, 10);
57+
58+
// Invoke a member function
59+
const Callable foo(314159);
60+
std::invoke(&Callable::member_function, foo, 1);
61+
62+
// Invoke a function object
63+
std::invoke(Callable(12), 18);
64+
}
65+
66+
struct MyKey {
67+
int x;
68+
bool operator==(const MyKey &) const = default;
69+
bool operator<(const MyKey &other) const {
70+
__builtin_printf("break here");
71+
return x < other.x;
72+
}
73+
};
74+
75+
void test_containers() {
76+
std::map<MyKey, int> map;
77+
map.emplace(MyKey{1}, 2);
78+
map.emplace(MyKey{2}, 3);
79+
}
80+
81+
int main() {
82+
test_algorithms();
83+
test_invoke();
84+
test_containers();
85+
return 0;
86+
}

lldb/test/API/lang/cpp/std-invoke-recognizer/TestStdInvokeRecognizer.py

Lines changed: 0 additions & 44 deletions
This file was deleted.

lldb/test/API/lang/cpp/std-invoke-recognizer/main.cpp

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)