[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 09:33:45 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/196336
>From c2bdbcd332b71a146b4b13d257ef0ac7bb144324 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 7 May 2026 17:33:30 +0100
Subject: [PATCH] [lldb][windows] add hidden frame recognizers
---
.../CPlusPlus/CPPLanguageRuntime.cpp | 73 +++++++++
lldb/source/Target/StackFrameRecognizer.cpp | 15 +-
.../cpp/msvcstl-internals-recognizer/Makefile | 3 +
.../TestMSVCSTLInternalsRecognizer.py | 148 ++++++++++++++++++
.../cpp/msvcstl-internals-recognizer/main.cpp | 9 ++
.../TestHiddenFrameMarkers.py | 6 -
6 files changed, 243 insertions(+), 11 deletions(-)
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..0f760de2c496d 100644
--- a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
@@ -109,6 +109,73 @@ class LibCXXFrameRecognizer : public StackFrameRecognizer {
}
};
+/// A frame recognizer that hides 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; }
+ };
+
+ // The MSVC demangler emits demangled names that include the return type
+ // (e.g. `void std::_Func_impl_no_alloc<...>::_Do_call`). Detect whether a
+ // function name belongs to the `std::` namespace, accounting for that
+ // optional return-type prefix.
+ static bool IsInStdNamespace(llvm::StringRef name) {
+ return name.starts_with("std::") || name.contains(" std::");
+ }
+
+public:
+ MSVCSTLFrameRecognizer()
+ // Examples of MSVC STL internals that should be hidden:
+ // void std::_Func_impl_no_alloc<`lambda...',void>::_Do_call
+ // void std::_Func_class<void>::operator()
+ // std::_Invoker_ret<std::_Unforced,1>::_Call<...>
+ // The regex is intentionally not anchored: the MSVC demangler may
+ // prepend a return type before the qualified name.
+ : 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 (IsInStdNamespace(
+ parent_sc.function->GetNameNoArguments().GetStringRef()))
+ return m_hidden_frame;
+
+ return {};
+ }
+};
+
CPPLanguageRuntime::CPPLanguageRuntime(Process *process)
: LanguageRuntime(process), m_itanium_runtime(process) {
if (process) {
@@ -118,6 +185,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/source/Target/StackFrameRecognizer.cpp b/lldb/source/Target/StackFrameRecognizer.cpp
index 3339195ddcbf0..b832e09df2439 100644
--- a/lldb/source/Target/StackFrameRecognizer.cpp
+++ b/lldb/source/Target/StackFrameRecognizer.cpp
@@ -146,11 +146,11 @@ StackFrameRecognizerManager::GetRecognizerForFrame(StackFrameSP frame) {
if (!module_sp)
return StackFrameRecognizerSP();
ConstString module_name = module_sp->GetFileSpec().GetFilename();
+ // We need either a Symbol or a Function to look up a recognizer. On Windows,
+ // when using PDB Symbol is not always populated even when a Function is.
const Symbol *symbol = symctx.symbol;
- if (!symbol)
+ if (!symbol && !symctx.function)
return StackFrameRecognizerSP();
- Address start_addr = symbol->GetAddress();
- Address current_addr = frame->GetFrameCodeAddress();
for (const auto &entry : m_recognizers) {
if (!entry.enabled)
@@ -174,9 +174,14 @@ StackFrameRecognizerManager::GetRecognizerForFrame(StackFrameSP frame) {
if (!entry.symbol_regexp->Execute(function_name.GetStringRef()))
continue;
- if (entry.first_instruction_only)
- if (start_addr != current_addr)
+ if (entry.first_instruction_only) {
+ // First-instruction matching requires a Symbol to know the function's
+ // entry address. Without one, we cannot apply this recognizer here.
+ if (!symbol)
continue;
+ if (symbol->GetAddress() != frame->GetFrameCodeAddress())
+ continue;
+ }
return entry.recognizer;
}
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..417b2780d0cd4
--- /dev/null
+++ b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py
@@ -0,0 +1,148 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+def _qualified_name_in_std(name):
+ """Return True if `name` is a function in the `std::` namespace.
+
+ Handles the MSVC demangler convention of prefixing the demangled name
+ with the return type (e.g. `void std::_Func_class<void>::operator()`).
+ """
+ if not name:
+ return False
+ return name.startswith("std::") or " std::" in name
+
+
+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."""
+ 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(
+ _qualified_name_in_std(name),
+ f"hidden frame #{i} '{name}' should be in 'std::' namespace",
+ )
+ parent = thread.GetFrameAtIndex(i + 1)
+ self.assertIsNotNone(parent)
+ parent_name = parent.GetFunctionName() or ""
+ self.assertTrue(
+ _qualified_name_in_std(parent_name),
+ 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;
+}
diff --git a/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py b/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py
index b3c6627641775..50e648befa65b 100644
--- a/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py
+++ b/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py
@@ -10,9 +10,6 @@
class HiddenFrameMarkerTest(TestBase):
@unicode_test
- @expectedFailureWindows(
- bugnumber="https://github.com/llvm/llvm-project/issues/191459"
- )
def test_hidden_frame_markers(self):
"""Test that hidden frame markers are rendered in backtraces"""
self.build()
@@ -52,9 +49,6 @@ def test_hidden_frame_markers(self):
)
@unicode_test
- @expectedFailureWindows(
- bugnumber="https://github.com/llvm/llvm-project/issues/191459"
- )
def test_nested_hidden_frame_markers(self):
"""Test that nested hidden frame markers are rendered in backtraces"""
self.build()
More information about the lldb-commits
mailing list