[Lldb-commits] [lldb] 7e16571 - [lldb][libc++] Hide all libc++ implementation details from stacktraces (#108870)

via lldb-commits lldb-commits at lists.llvm.org
Thu Oct 10 10:27:32 PDT 2024


Author: Adrian Vogelsgesang
Date: 2024-10-10T19:27:27+02:00
New Revision: 7e16571eb02e7e9da24fee45359e981af783d0d0

URL: https://github.com/llvm/llvm-project/commit/7e16571eb02e7e9da24fee45359e981af783d0d0
DIFF: https://github.com/llvm/llvm-project/commit/7e16571eb02e7e9da24fee45359e981af783d0d0.diff

LOG: [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

Added: 
    lldb/test/API/lang/cpp/libcxx-internals-recognizer/Makefile
    lldb/test/API/lang/cpp/libcxx-internals-recognizer/TestLibcxxInternalsRecognizer.py
    lldb/test/API/lang/cpp/libcxx-internals-recognizer/main.cpp

Modified: 
    libcxx/docs/UserDocumentation.rst
    lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp

Removed: 
    lldb/test/API/lang/cpp/std-invoke-recognizer/Makefile
    lldb/test/API/lang/cpp/std-invoke-recognizer/TestStdInvokeRecognizer.py
    lldb/test/API/lang/cpp/std-invoke-recognizer/main.cpp


################################################################################
diff  --git a/libcxx/docs/UserDocumentation.rst b/libcxx/docs/UserDocumentation.rst
index f5e55994aa7572..1db437ce58b95e 100644
--- a/libcxx/docs/UserDocumentation.rst
+++ b/libcxx/docs/UserDocumentation.rst
@@ -355,6 +355,35 @@ 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".
+
+Futhermore, stepping into libc++ functions is disabled by default. This is controlled via the
+setting ``target.process.thread.step-avoid-regexp`` which defaults to ``^std::`` and can be
+disabled using ``settings set target.process.thread.step-avoid-regexp ""``.
+
 GDB Pretty printers for libc++
 ------------------------------
 

diff  --git a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
index faa05e8f834ea1..e7ca3f655f237c 100644
--- a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
@@ -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 {
@@ -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()) {}
 
@@ -90,9 +79,27 @@ class LibCXXFrameRecognizer : public StackFrameRecognizer {
     if (!sc.function)
       return {};
 
-    for (RegularExpression &r : m_hidden_regex)
-      if (r.Execute(sc.function->GetNameNoArguments()))
+    // Check if we have a regex match
+    for (RegularExpression &r : m_hidden_regex) {
+      if (!r.Execute(sc.function->GetNameNoArguments()))
+        continue;
+
+      // Only hide this frame if the immediate caller is also within libc++.
+      lldb::ThreadSP thread_sp = frame_sp->GetThread();
+      if (!thread_sp)
+        return {};
+      lldb::StackFrameSP parent_frame_sp =
+          thread_sp->GetStackFrameAtIndex(frame_sp->GetFrameIndex() + 1);
+      if (!parent_frame_sp)
+        return {};
+      const auto &parent_sc =
+          parent_frame_sp->GetSymbolContext(lldb::eSymbolContextFunction);
+      if (!parent_sc.function)
+        return {};
+      if (parent_sc.function->GetNameNoArguments().GetStringRef().starts_with(
+              "std::"))
         return m_hidden_frame;
+    }
 
     return {};
   }

diff  --git a/lldb/test/API/lang/cpp/std-invoke-recognizer/Makefile b/lldb/test/API/lang/cpp/libcxx-internals-recognizer/Makefile
similarity index 68%
rename from lldb/test/API/lang/cpp/std-invoke-recognizer/Makefile
rename to lldb/test/API/lang/cpp/libcxx-internals-recognizer/Makefile
index 69014eb9c0f2eb..bb571299664934 100644
--- a/lldb/test/API/lang/cpp/std-invoke-recognizer/Makefile
+++ b/lldb/test/API/lang/cpp/libcxx-internals-recognizer/Makefile
@@ -1,5 +1,5 @@
 CXX_SOURCES := main.cpp
 USE_LIBCPP := 1
-CXXFLAGS_EXTRAS := -std=c++17
+CXXFLAGS_EXTRAS := -std=c++20
 
 include Makefile.rules

diff  --git a/lldb/test/API/lang/cpp/libcxx-internals-recognizer/TestLibcxxInternalsRecognizer.py b/lldb/test/API/lang/cpp/libcxx-internals-recognizer/TestLibcxxInternalsRecognizer.py
new file mode 100644
index 00000000000000..ad48208f21e502
--- /dev/null
+++ b/lldb/test/API/lang/cpp/libcxx-internals-recognizer/TestLibcxxInternalsRecognizer.py
@@ -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`.
+            # 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))

diff  --git a/lldb/test/API/lang/cpp/libcxx-internals-recognizer/main.cpp b/lldb/test/API/lang/cpp/libcxx-internals-recognizer/main.cpp
new file mode 100644
index 00000000000000..870301b0970439
--- /dev/null
+++ b/lldb/test/API/lang/cpp/libcxx-internals-recognizer/main.cpp
@@ -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;
+}

diff  --git a/lldb/test/API/lang/cpp/std-invoke-recognizer/TestStdInvokeRecognizer.py b/lldb/test/API/lang/cpp/std-invoke-recognizer/TestStdInvokeRecognizer.py
deleted file mode 100644
index dbe29610bf7982..00000000000000
--- a/lldb/test/API/lang/cpp/std-invoke-recognizer/TestStdInvokeRecognizer.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import lldb
-from lldbsuite.test.decorators import *
-from lldbsuite.test.lldbtest import *
-from lldbsuite.test import lldbutil
-
-
-class LibCxxStdFunctionRecognizerTestCase(TestBase):
-    NO_DEBUG_INFO_TESTCASE = True
-
-    @add_test_categories(["libc++"])
-    def test_frame_recognizer(self):
-        """Test that implementation details of `std::invoke` are hidden"""
-        self.build()
-        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
-            self, "break here", lldb.SBFileSpec("main.cpp")
-        )
-
-        stop_cnt = 0
-        while process.GetState() != lldb.eStateExited:
-            stop_cnt += 1
-            self.assertTrue(
-                any(
-                    f in thread.GetFrameAtIndex(0).GetFunctionName()
-                    for f in ["consume_number", "add", "Callable"]
-                )
-            )
-            # Skip all hidden frames
-            frame_id = 1
-            while (
-                frame_id < thread.GetNumFrames()
-                and thread.GetFrameAtIndex(frame_id).IsHidden()
-            ):
-                frame_id = frame_id + 1
-            # Expect `std::invoke` to be the direct parent
-            self.assertIn(
-                "::invoke", thread.GetFrameAtIndex(frame_id).GetFunctionName()
-            )
-            # And right above that, there should be the `main` frame
-            self.assertIn(
-                "main", thread.GetFrameAtIndex(frame_id + 1).GetFunctionName()
-            )
-            process.Continue()
-
-        self.assertEqual(stop_cnt, 4)

diff  --git a/lldb/test/API/lang/cpp/std-invoke-recognizer/main.cpp b/lldb/test/API/lang/cpp/std-invoke-recognizer/main.cpp
deleted file mode 100644
index bafbbd28386e8b..00000000000000
--- a/lldb/test/API/lang/cpp/std-invoke-recognizer/main.cpp
+++ /dev/null
@@ -1,30 +0,0 @@
-#include <functional>
-
-void consume_number(int i) { __builtin_printf("break here"); }
-
-int add(int i, int j) {
-  // 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_;
-};
-
-int main() {
-  // Invoke a void-returning function
-  std::invoke(consume_number, -9);
-
-  // Invoke a non-void-returning function
-  std::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);
-}


        


More information about the lldb-commits mailing list