https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/196336
>From 29e85dd3171c81214d99e97dd187a6cd842cf3e7 Mon Sep 17 00:00:00 2001 From: Charles Zablit <[email protected]> Date: Thu, 7 May 2026 15:50:06 +0100 Subject: [PATCH 1/2] [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; +} >From be7cf5e4bf5240f206a0acb55e3b288a4e7d517b Mon Sep 17 00:00:00 2001 From: Charles Zablit <[email protected]> Date: Thu, 7 May 2026 17:12:18 +0100 Subject: [PATCH 2/2] fixup! [lldb][windows] add hidden frame recognizers --- .../CPlusPlus/CPPLanguageRuntime.cpp | 22 +++++++++++----- .../TestMSVCSTLInternalsRecognizer.py | 26 +++++++++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp index f22a970f04302..bb5ee15ee3ee9 100644 --- a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp +++ b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp @@ -122,13 +122,23 @@ class MSVCSTLFrameRecognizer : public StackFrameRecognizer { 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: - // std::_Func_impl_no_alloc<`lambda...',void>::_Do_call - // std::_Func_class<void>::operator() + // void std::_Func_impl_no_alloc<`lambda...',void>::_Do_call + // void std::_Func_class<void>::operator() // std::_Invoker_ret<std::_Unforced,1>::_Call<...> - : m_hidden_regex(R"(^std::_[A-Z])"), + // 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"; } @@ -158,8 +168,8 @@ class MSVCSTLFrameRecognizer : public StackFrameRecognizer { parent_frame_sp->GetSymbolContext(lldb::eSymbolContextFunction); if (!parent_sc.function) return {}; - if (parent_sc.function->GetNameNoArguments().GetStringRef().starts_with( - "std::")) + if (IsInStdNamespace( + parent_sc.function->GetNameNoArguments().GetStringRef())) return m_hidden_frame; return {}; @@ -177,7 +187,7 @@ CPPLanguageRuntime::CPPLanguageRuntime(Process *process) process->GetTarget().GetFrameRecognizerManager().AddRecognizer( StackFrameRecognizerSP(new MSVCSTLFrameRecognizer()), {}, - std::make_shared<RegularExpression>("^std::_[A-Z]"), + std::make_shared<RegularExpression>("std::_[A-Z]"), /*mangling_preference=*/Mangled::ePreferDemangledWithoutArguments, /*first_instruction_only=*/false); diff --git a/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py index de282b180a4f5..787dfa0b09df2 100644 --- a/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py +++ b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py @@ -4,6 +4,17 @@ 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 @@ -38,14 +49,14 @@ def test_outermost_std_frame_visible(self): continue name = frame.GetFunctionName() or "" self.assertTrue( - name.startswith("std::_"), - f"hidden frame #{i} '{name}' should start with 'std::_'", + _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( - parent_name.startswith("std::"), + _qualified_name_in_std(parent_name), f"hidden frame #{i} '{name}' has non-std parent '{parent_name}'", ) @@ -100,7 +111,9 @@ def test_up_down(self): break end_idx = frame.GetFrameID() self.assertEqual(frame.GetFunctionName(), "main") - self.assertLess(up_steps, end_idx - start_idx, "expected skipped frames going up") + self.assertLess( + up_steps, end_idx - start_idx, "expected skipped frames going up" + ) # Walk back down to `target`. start_idx = frame.GetFrameID() @@ -113,7 +126,9 @@ def test_up_down(self): break end_idx = frame.GetFrameID() self.assertIn("target", frame.GetFunctionName()) - self.assertLess(down_steps, start_idx - end_idx, "expected skipped frames going down") + self.assertLess( + down_steps, start_idx - end_idx, "expected skipped frames going down" + ) @skipUnlessWindows def test_user_lambda_not_hidden(self): @@ -132,3 +147,4 @@ def test_user_lambda_not_hidden(self): ) return self.fail("did not find a user lambda frame in the backtrace") + _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
