Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lldb][libc++] Hide all libc++ implementation details from stacktraces #108870

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions libcxx/docs/UserDocumentation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,31 @@ Third-party Integrations

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

Debugging libc++ internals in LLDB
----------------------------------

LLDB hides the implementation details of libc++ by default.

E.g., when setting a breakpoint in a comparator passed to ``std::sort``, the
backtrace will read as

.. code-block::

(lldb) thread backtrace
* thread #1, name = 'a.out', stop reason = breakpoint 3.1
* frame #0: 0x000055555555520e a.out`my_comparator(a=1, b=8) at test-std-sort.cpp:6:3
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
frame #8: 0x000055555555531a a.out`main at test-std-sort.cpp:24:3

Note how the caller of ``my_comparator`` is shown as ``std::sort``. Looking at
the frame numbers, we can see that frames #1 until #6 were hidden. Those frames
represent internal implementation details such as ``__sort4`` and similar
utility functions.

To also show those implementation details, use ``thread backtrace -u``.
Alternatively, to disable those compact backtraces, use ``frame recognizer list``
and ``frame recognizer disable`` on the "libc++ frame recognizer".

Michael137 marked this conversation as resolved.
Show resolved Hide resolved
GDB Pretty printers for libc++
------------------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ char CPPLanguageRuntime::ID = 0;
/// A frame recognizer that is installed to hide libc++ implementation
/// details from the backtrace.
class LibCXXFrameRecognizer : public StackFrameRecognizer {
std::array<RegularExpression, 4> m_hidden_regex;
std::array<RegularExpression, 2> m_hidden_regex;
RecognizedStackFrameSP m_hidden_frame;

struct LibCXXHiddenFrame : public RecognizedStackFrame {
Expand All @@ -55,28 +55,17 @@ class LibCXXFrameRecognizer : public StackFrameRecognizer {
public:
LibCXXFrameRecognizer()
: m_hidden_regex{
// internal implementation details of std::function
// internal implementation details in the `std::` namespace
// std::__1::__function::__alloc_func<void (*)(), std::__1::allocator<void (*)()>, void ()>::operator()[abi:ne200000]
// std::__1::__function::__func<void (*)(), std::__1::allocator<void (*)()>, void ()>::operator()
// std::__1::__function::__value_func<void ()>::operator()[abi:ne200000]() const
RegularExpression{""
R"(^std::__[^:]*::)" // Namespace.
R"(__function::.*::operator\(\))"},
// internal implementation details of std::function in ABI v2
// std::__2::__function::__policy_invoker<void (int, int)>::__call_impl[abi:ne200000]<std::__2::__function::__default_alloc_func<int (*)(int, int), int (int, int)>>
RegularExpression{""
R"(^std::__[^:]*::)" // Namespace.
R"(__function::.*::__call_impl)"},
// internal implementation details of std::invoke
// std::__1::__invoke[abi:ne200000]<void (*&)()>
RegularExpression{
R"(^std::__[^:]*::)" // Namespace.
R"(__invoke)"},
// internal implementation details of std::invoke
// std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:ne200000]<void (*&)()>
RegularExpression{
R"(^std::__[^:]*::)" // Namespace.
R"(__invoke_void_return_wrapper<.*>::__call)"}
// std::__1::__invoke[abi:ne200000]<void (*&)()>
// std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:ne200000]<void (*&)()>
RegularExpression{R"(^std::__[^:]*::__)"},
// internal implementation details in the `std::ranges` namespace
// 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>
RegularExpression{R"(^std::__[^:]*::ranges::__)"},
},
m_hidden_frame(new LibCXXHiddenFrame()) {}

Expand All @@ -90,9 +79,26 @@ class LibCXXFrameRecognizer : public StackFrameRecognizer {
if (!sc.function)
return {};

// Check if we have a regex match
bool matches_regex = false;
for (RegularExpression &r : m_hidden_regex)
vogelsgesang marked this conversation as resolved.
Show resolved Hide resolved
if (r.Execute(sc.function->GetNameNoArguments()))
if (r.Execute(sc.function->GetNameNoArguments())) {
matches_regex = true;
break;
}

if (matches_regex) {
vogelsgesang marked this conversation as resolved.
Show resolved Hide resolved
// Only hide this frame if the immediate caller is also within libc++.
lldb::StackFrameSP parent_frame =
Michael137 marked this conversation as resolved.
Show resolved Hide resolved
frame_sp->GetThread()->GetStackFrameAtIndex(
frame_sp->GetFrameIndex() + 1);
const auto &parent_sc =
parent_frame->GetSymbolContext(lldb::eSymbolContextFunction);
if (parent_sc.function->GetNameNoArguments().GetStringRef().starts_with(
Michael137 marked this conversation as resolved.
Show resolved Hide resolved
"std::")) {
return m_hidden_frame;
}
vogelsgesang marked this conversation as resolved.
Show resolved Hide resolved
}

return {};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
CXX_SOURCES := main.cpp
USE_LIBCPP := 1
CXXFLAGS_EXTRAS := -std=c++17
CXXFLAGS_EXTRAS := -std=c++20

include Makefile.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil


class LibCxxInternalsRecognizerTestCase(TestBase):
NO_DEBUG_INFO_TESTCASE = True

@add_test_categories(["libc++"])
def test_frame_recognizer(self):
"""Test that implementation details of libc++ are hidden"""
self.build()
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "break here", lldb.SBFileSpec("main.cpp")
)

expected_parents = {
"sort_less(int, int)": ["::sort", "test_algorithms"],
# `std::ranges::sort` is implemented as an object of types `__sort`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There're a large number of such customization point objects (and niebloids, which will be respecified as CPOs soon, see P3136R0) since C++20. Should we invent some convention to recognize them uniformly?

Copy link
Member Author

@vogelsgesang vogelsgesang Sep 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that such a convention would be necessary. With the current heuristic ("never hide a frame which was immediately called from user code"), the end result to the user is fine, despite the debug info containing the __sort::operator() function name

# We never hide the frame of the entry-point into the standard library, even
# if the name starts with `__` which usually indicates an internal function.
"ranges_sort_less(int, int)": [
"ranges::__sort::operator()",
"test_algorithms",
],
# `ranges::views::transform` internally uses `std::invoke`, and that
# call also shows up in the stack trace
"view_transform(int)": [
"::invoke",
"ranges::transform_view",
"test_algorithms",
],
# Various types of `invoke` calls
"consume_number(int)": ["::invoke", "test_invoke"],
"invoke_add(int, int)": ["::invoke", "test_invoke"],
"Callable::member_function(int) const": ["::invoke", "test_invoke"],
"Callable::operator()(int) const": ["::invoke", "test_invoke"],
# Containers
"MyKey::operator<(MyKey const&) const": [
"less",
"::emplace",
"test_containers",
],
}
stop_set = set()
while process.GetState() != lldb.eStateExited:
fn = thread.GetFrameAtIndex(0).GetFunctionName()
stop_set.add(fn)
self.assertIn(fn, expected_parents.keys())
frame_id = 1
for expected_parent in expected_parents[fn]:
# Skip all hidden frames
while (
frame_id < thread.GetNumFrames()
and thread.GetFrameAtIndex(frame_id).IsHidden()
):
frame_id = frame_id + 1
# Expect the correct parent frame
self.assertIn(
expected_parent, thread.GetFrameAtIndex(frame_id).GetFunctionName()
)
frame_id = frame_id + 1
process.Continue()

# Make sure that we actually verified all intended scenarios
self.assertEqual(len(stop_set), len(expected_parents))
86 changes: 86 additions & 0 deletions lldb/test/API/lang/cpp/libcxx-internals-recognizer/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#include <algorithm>
#include <functional>
#include <map>
#include <ranges>
#include <vector>

bool sort_less(int a, int b) {
__builtin_printf("break here");
return a < b;
}

bool ranges_sort_less(int a, int b) {
__builtin_printf("break here");
return a < b;
}

int view_transform(int a) {
__builtin_printf("break here");
return a * a;
}

void test_algorithms() {
std::vector<int> vec{8, 1, 3, 2};

// The internal frames for `std::sort` should be hidden
std::sort(vec.begin(), vec.end(), sort_less);

// The internal frames for `ranges::sort` should be hidden
std::ranges::sort(vec.begin(), vec.end(), ranges_sort_less);

// Same for views
for (auto x : vec | std::ranges::views::transform(view_transform)) {
// no-op
}
}

void consume_number(int i) { __builtin_printf("break here"); }

int invoke_add(int i, int j) {
__builtin_printf("break here");
return i + j;
}

struct Callable {
Callable(int num) : num_(num) {}
void operator()(int i) const { __builtin_printf("break here"); }
void member_function(int i) const { __builtin_printf("break here"); }
int num_;
};

void test_invoke() {
// Invoke a void-returning function
std::invoke(consume_number, -9);

// Invoke a non-void-returning function
std::invoke(invoke_add, 1, 10);

// Invoke a member function
const Callable foo(314159);
std::invoke(&Callable::member_function, foo, 1);

// Invoke a function object
std::invoke(Callable(12), 18);
}

struct MyKey {
int x;
bool operator==(const MyKey &) const = default;
bool operator<(const MyKey &other) const {
__builtin_printf("break here");
return x < other.x;
}
};

void test_containers() {
std::map<MyKey, int> map;
map.emplace(MyKey{1}, 2);
map.emplace(MyKey{2}, 3);
}

int main() {
test_algorithms();
test_invoke();
test_containers();
return 0;
}

This file was deleted.

30 changes: 0 additions & 30 deletions lldb/test/API/lang/cpp/std-invoke-recognizer/main.cpp

This file was deleted.

Loading