[Lldb-commits] [lldb] [lldb][windows] add hidden frame recognizers (PR #196336)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Thu May 7 07:50:40 PDT 2026
https://github.com/charles-zablit created https://github.com/llvm/llvm-project/pull/196336
None
>From 29e85dd3171c81214d99e97dd187a6cd842cf3e7 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 7 May 2026 15:50:06 +0100
Subject: [PATCH] [lldb][windows] add hidden frame recognizers
---
.../CPlusPlus/CPPLanguageRuntime.cpp | 63 ++++++++
.../cpp/msvcstl-internals-recognizer/Makefile | 3 +
.../TestMSVCSTLInternalsRecognizer.py | 134 ++++++++++++++++++
.../cpp/msvcstl-internals-recognizer/main.cpp | 9 ++
4 files changed, 209 insertions(+)
create mode 100644 lldb/test/API/lang/cpp/msvcstl-internals-recognizer/Makefile
create mode 100644 lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py
create mode 100644 lldb/test/API/lang/cpp/msvcstl-internals-recognizer/main.cpp
diff --git a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
index c517ec8611932..f22a970f04302 100644
--- a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
@@ -109,6 +109,63 @@ class LibCXXFrameRecognizer : public StackFrameRecognizer {
}
};
+/// A frame recognizer that is installed to hide MSVC STL implementation
+/// details from the backtrace. MSVC STL reserves identifiers beginning with an
+/// underscore followed by an uppercase letter (e.g. `_Func_class`) for
+/// implementation details, so frames whose function name starts with `std::_`
+/// followed by an uppercase letter are hidden when called from within `std::`.
+class MSVCSTLFrameRecognizer : public StackFrameRecognizer {
+ RegularExpression m_hidden_regex;
+ RecognizedStackFrameSP m_hidden_frame;
+
+ struct MSVCSTLHiddenFrame : public RecognizedStackFrame {
+ bool ShouldHide() override { return true; }
+ };
+
+public:
+ MSVCSTLFrameRecognizer()
+ // Examples of MSVC STL internals that should be hidden:
+ // std::_Func_impl_no_alloc<`lambda...',void>::_Do_call
+ // std::_Func_class<void>::operator()
+ // std::_Invoker_ret<std::_Unforced,1>::_Call<...>
+ : m_hidden_regex(R"(^std::_[A-Z])"),
+ m_hidden_frame(new MSVCSTLHiddenFrame()) {}
+
+ std::string GetName() override { return "MSVC STL frame recognizer"; }
+
+ lldb::RecognizedStackFrameSP
+ RecognizeFrame(lldb::StackFrameSP frame_sp) override {
+ if (!frame_sp)
+ return {};
+ const auto &sc = frame_sp->GetSymbolContext(lldb::eSymbolContextFunction);
+ if (!sc.function)
+ return {};
+
+ if (!m_hidden_regex.Execute(sc.function->GetNameNoArguments()))
+ return {};
+
+ // Only hide this frame if the immediate caller is also within MSVC STL.
+ // This keeps the outermost std-facing frame (the one called by user code)
+ // visible in the backtrace.
+ 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 {};
+ }
+};
+
CPPLanguageRuntime::CPPLanguageRuntime(Process *process)
: LanguageRuntime(process), m_itanium_runtime(process) {
if (process) {
@@ -118,6 +175,12 @@ CPPLanguageRuntime::CPPLanguageRuntime(Process *process)
/*mangling_preference=*/Mangled::ePreferDemangledWithoutArguments,
/*first_instruction_only=*/false);
+ process->GetTarget().GetFrameRecognizerManager().AddRecognizer(
+ StackFrameRecognizerSP(new MSVCSTLFrameRecognizer()), {},
+ std::make_shared<RegularExpression>("^std::_[A-Z]"),
+ /*mangling_preference=*/Mangled::ePreferDemangledWithoutArguments,
+ /*first_instruction_only=*/false);
+
RegisterVerboseTrapFrameRecognizer(*process);
}
}
diff --git a/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/Makefile b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules
diff --git a/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py
new file mode 100644
index 0000000000000..de282b180a4f5
--- /dev/null
+++ b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py
@@ -0,0 +1,134 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class MSVCSTLInternalsRecognizerTestCase(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def _run_to_target(self):
+ self.build()
+ return lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec("main.cpp")
+ )
+
+ @skipUnlessWindows
+ def test_frame_recognizer(self):
+ """At least one MSVC STL internal frame between `target` and `main`
+ should be hidden, but not all frames."""
+ (target, process, thread, bkpt) = self._run_to_target()
+
+ self.assertIn("target", thread.GetFrameAtIndex(0).GetFunctionName())
+
+ num_hidden = sum(1 for frame in thread.frames if frame.IsHidden())
+ self.assertGreater(num_hidden, 0)
+ self.assertLess(num_hidden, thread.GetNumFrames())
+
+ @skipUnlessWindows
+ def test_outermost_std_frame_visible(self):
+ """The outermost `std::*` frame (called directly by user code) must
+ stay visible — only frames whose immediate caller is also `std::*`
+ should be hidden."""
+ (target, process, thread, bkpt) = self._run_to_target()
+
+ for i in range(thread.GetNumFrames()):
+ frame = thread.GetFrameAtIndex(i)
+ if not frame.IsHidden():
+ continue
+ name = frame.GetFunctionName() or ""
+ self.assertTrue(
+ name.startswith("std::_"),
+ f"hidden frame #{i} '{name}' should start with 'std::_'",
+ )
+ parent = thread.GetFrameAtIndex(i + 1)
+ self.assertIsNotNone(parent)
+ parent_name = parent.GetFunctionName() or ""
+ self.assertTrue(
+ parent_name.startswith("std::"),
+ f"hidden frame #{i} '{name}' has non-std parent '{parent_name}'",
+ )
+
+ @skipUnlessWindows
+ def test_backtrace(self):
+ """`bt` hides MSVC STL internals; `bt -u` shows them."""
+ (target, process, thread, bkpt) = self._run_to_target()
+
+ self.expect(
+ "thread backtrace",
+ ordered=True,
+ substrs=["frame", "target", "frame", "main"],
+ )
+ self.expect(
+ "thread backtrace",
+ matching=False,
+ patterns=[r"frame.*std::_Func_impl", r"frame.*_Do_call"],
+ )
+ self.expect(
+ "thread backtrace -u",
+ ordered=True,
+ patterns=[r"frame.*target", r"frame.*std::_", r"frame.*main"],
+ )
+ self.expect(
+ "bt -u",
+ ordered=True,
+ patterns=[r"frame.*target", r"frame.*std::_", r"frame.*main"],
+ )
+ self.expect(
+ "thread backtrace --unfiltered",
+ ordered=True,
+ patterns=[r"frame.*target", r"frame.*std::_", r"frame.*main"],
+ )
+
+ @skipUnlessWindows
+ def test_up_down(self):
+ """`up` and `down` should skip past hidden MSVC STL frames."""
+ (target, process, thread, bkpt) = self._run_to_target()
+
+ frame = thread.selected_frame
+ self.assertIn("target", frame.GetFunctionName())
+ start_idx = frame.GetFrameID()
+
+ # Walk up until we hit `main`. The number of `up` invocations must be
+ # less than the raw frame distance, proving hidden frames were skipped.
+ up_steps = 0
+ for _ in range(thread.GetNumFrames()):
+ self.expect("up")
+ up_steps += 1
+ frame = thread.selected_frame
+ if frame.GetFunctionName() == "main":
+ break
+ end_idx = frame.GetFrameID()
+ self.assertEqual(frame.GetFunctionName(), "main")
+ self.assertLess(up_steps, end_idx - start_idx, "expected skipped frames going up")
+
+ # Walk back down to `target`.
+ start_idx = frame.GetFrameID()
+ down_steps = 0
+ for _ in range(thread.GetNumFrames()):
+ self.expect("down")
+ down_steps += 1
+ frame = thread.selected_frame
+ if "target" in (frame.GetFunctionName() or ""):
+ break
+ end_idx = frame.GetFrameID()
+ self.assertIn("target", frame.GetFunctionName())
+ self.assertLess(down_steps, start_idx - end_idx, "expected skipped frames going down")
+
+ @skipUnlessWindows
+ def test_user_lambda_not_hidden(self):
+ """The user's lambda wrapper (`main::<lambda_...>::operator()`)
+ is user code and must NOT be hidden, even though it sits among
+ `std::function` machinery."""
+ (target, process, thread, bkpt) = self._run_to_target()
+
+ for i in range(thread.GetNumFrames()):
+ frame = thread.GetFrameAtIndex(i)
+ name = frame.GetFunctionName() or ""
+ if "lambda" in name and "::operator()" in name:
+ self.assertFalse(
+ frame.IsHidden(),
+ f"user lambda frame '{name}' should not be hidden",
+ )
+ return
+ self.fail("did not find a user lambda frame in the backtrace")
diff --git a/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/main.cpp b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/main.cpp
new file mode 100644
index 0000000000000..b1328a1325c75
--- /dev/null
+++ b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/main.cpp
@@ -0,0 +1,9 @@
+#include <functional>
+
+static void target() { __builtin_printf("break here"); }
+
+int main() {
+ std::function<void()> fn = [] { target(); };
+ fn();
+ return 0;
+}
More information about the lldb-commits
mailing list