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 <[email protected]> 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() _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
