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