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/4] [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/4] 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") + >From 1aac73edec4ba585417f2cbc467774619e668c31 Mon Sep 17 00:00:00 2001 From: Charles Zablit <[email protected]> Date: Thu, 7 May 2026 17:23:27 +0100 Subject: [PATCH 3/4] --wip-- [skip ci] --- .../TestMSVCSTLInternalsRecognizer.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 787dfa0b09df2..efe2400261d84 100644 --- a/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py +++ b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py @@ -32,8 +32,28 @@ def test_frame_recognizer(self): self.assertIn("target", thread.GetFrameAtIndex(0).GetFunctionName()) + # Dump every frame's name and IsHidden() result so failures show + # exactly what the recognizer is seeing. + diagnostics = [] + for i in range(thread.GetNumFrames()): + f = thread.GetFrameAtIndex(i) + sym = f.GetSymbol() + sym_name = sym.GetName() if sym else None + sym_mangled = sym.GetMangledName() if sym else None + fn = f.GetFunction() + fn_name = fn.GetName() if fn else None + diagnostics.append( + f"#{i} hidden={f.IsHidden()} " + f"GetFunctionName={f.GetFunctionName()!r} " + f"function.GetName={fn_name!r} " + f"symbol.GetName={sym_name!r} " + f"symbol.GetMangledName={sym_mangled!r}" + ) + num_hidden = sum(1 for frame in thread.frames if frame.IsHidden()) - self.assertGreater(num_hidden, 0) + self.assertGreater( + num_hidden, 0, "no frames hidden. Frames seen:\n" + "\n".join(diagnostics) + ) self.assertLess(num_hidden, thread.GetNumFrames()) @skipUnlessWindows >From 1a3592a3c3fb09efa5a94293b1a443152e82525d Mon Sep 17 00:00:00 2001 From: Charles Zablit <[email protected]> Date: Thu, 7 May 2026 17:28:00 +0100 Subject: [PATCH 4/4] --wip-- [skip ci] --- lldb/source/Target/StackFrameRecognizer.cpp | 16 +++++++++----- .../TestMSVCSTLInternalsRecognizer.py | 22 +------------------ 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/lldb/source/Target/StackFrameRecognizer.cpp b/lldb/source/Target/StackFrameRecognizer.cpp index 3339195ddcbf0..7635ae1f03432 100644 --- a/lldb/source/Target/StackFrameRecognizer.cpp +++ b/lldb/source/Target/StackFrameRecognizer.cpp @@ -146,11 +146,12 @@ 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 some + // platforms (notably PDB on Windows) Symbol is not always populated even + // when a Function is — fall back to Function in that case. 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 +175,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/TestMSVCSTLInternalsRecognizer.py b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py index efe2400261d84..787dfa0b09df2 100644 --- a/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py +++ b/lldb/test/API/lang/cpp/msvcstl-internals-recognizer/TestMSVCSTLInternalsRecognizer.py @@ -32,28 +32,8 @@ def test_frame_recognizer(self): self.assertIn("target", thread.GetFrameAtIndex(0).GetFunctionName()) - # Dump every frame's name and IsHidden() result so failures show - # exactly what the recognizer is seeing. - diagnostics = [] - for i in range(thread.GetNumFrames()): - f = thread.GetFrameAtIndex(i) - sym = f.GetSymbol() - sym_name = sym.GetName() if sym else None - sym_mangled = sym.GetMangledName() if sym else None - fn = f.GetFunction() - fn_name = fn.GetName() if fn else None - diagnostics.append( - f"#{i} hidden={f.IsHidden()} " - f"GetFunctionName={f.GetFunctionName()!r} " - f"function.GetName={fn_name!r} " - f"symbol.GetName={sym_name!r} " - f"symbol.GetMangledName={sym_mangled!r}" - ) - num_hidden = sum(1 for frame in thread.frames if frame.IsHidden()) - self.assertGreater( - num_hidden, 0, "no frames hidden. Frames seen:\n" + "\n".join(diagnostics) - ) + self.assertGreater(num_hidden, 0) self.assertLess(num_hidden, thread.GetNumFrames()) @skipUnlessWindows _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
