Skip to content

[lldb] Implement delayed breakpoints#192971

Merged
felipepiovezan merged 14 commits into
mainfrom
users/felipepiovezan/delayed_bps_p3
May 1, 2026
Merged

[lldb] Implement delayed breakpoints#192971
felipepiovezan merged 14 commits into
mainfrom
users/felipepiovezan/delayed_bps_p3

Conversation

@felipepiovezan

@felipepiovezan felipepiovezan commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

This patch changes the Process class so that it delays physically enabling/disabling breakpoints until the process is about to resume/detach/be destroyed, potentially reducing the packets transmitted by batching all breakpoints together.

Most classes only need to know whether a breakpoint is "logically" enabled, as opposed to "physically" enabled (i.e. the remote server has actually enabled the breakpoint). However, lower level classes like derived Process classes, or StopInfo may actually need to know whether the breakpoint was physically enabled. As such, this commit also adds a "IsPhysicallyEnabled" API.

The following PRs are related to the MultiBreakpoint feature:

@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 33456 tests passed
  • 528 tests skipped

✅ The build succeeded and all tests passed.

@felipepiovezan felipepiovezan force-pushed the users/felipepiovezan/delayed_bps_p3 branch from c48d567 to 474bad5 Compare April 20, 2026 14:41
@felipepiovezan felipepiovezan force-pushed the users/felipepiovezan/delayed_bps_p2 branch from 8f4104e to 2070181 Compare April 22, 2026 14:22
@felipepiovezan felipepiovezan force-pushed the users/felipepiovezan/delayed_bps_p3 branch from 474bad5 to 9a7fcf5 Compare April 22, 2026 14:23
@felipepiovezan felipepiovezan force-pushed the users/felipepiovezan/delayed_bps_p2 branch from 2070181 to e180c10 Compare April 23, 2026 14:39
@felipepiovezan felipepiovezan force-pushed the users/felipepiovezan/delayed_bps_p3 branch from 9a7fcf5 to 35a2784 Compare April 23, 2026 14:41
felipepiovezan added a commit that referenced this pull request Apr 25, 2026
This describes the packet discussed in the RFC:
https://discourse.llvm.org/t/rfc-a-new-packet-to-set-remove-multiple-breakpoints/90623

The following PRs are related:

* [[lldb] Propose MultiBreakpoint extension to GDB
Remote](#192910)
* [[debugserver] Implement
MultiBreakpoint](#192914)
* [[lldb-server][NFC] Factor out code handling breakpoint
packets](#192915)
* [[lldb-server] Implement support for MultiBreakpoint
packet](#192919)
* [[lldb][GDBRemote] Parse MultiBreakpoint+
capability](#192962)
* [[lldb][NFC] Move BreakpointSite::IsEnabled/SetEnabled into
Process](#192964)
* [[lldb] Implement delayed
breakpoints](#192971)
* [[lldb] Override UpdateBreakpointSites in ProcessGDBRemote to use
MultiBreakpoint](#192988)
llvm-upstreamsync Bot pushed a commit to qualcomm/cpullvm-toolchain that referenced this pull request Apr 25, 2026
…92910)

This describes the packet discussed in the RFC:
https://discourse.llvm.org/t/rfc-a-new-packet-to-set-remove-multiple-breakpoints/90623

The following PRs are related:

* [[lldb] Propose MultiBreakpoint extension to GDB
Remote](llvm/llvm-project#192910)
* [[debugserver] Implement
MultiBreakpoint](llvm/llvm-project#192914)
* [[lldb-server][NFC] Factor out code handling breakpoint
packets](llvm/llvm-project#192915)
* [[lldb-server] Implement support for MultiBreakpoint
packet](llvm/llvm-project#192919)
* [[lldb][GDBRemote] Parse MultiBreakpoint+
capability](llvm/llvm-project#192962)
* [[lldb][NFC] Move BreakpointSite::IsEnabled/SetEnabled into
Process](llvm/llvm-project#192964)
* [[lldb] Implement delayed
breakpoints](llvm/llvm-project#192971)
* [[lldb] Override UpdateBreakpointSites in ProcessGDBRemote to use
MultiBreakpoint](llvm/llvm-project#192988)
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request Apr 25, 2026
…92910)

This describes the packet discussed in the RFC:
https://discourse.llvm.org/t/rfc-a-new-packet-to-set-remove-multiple-breakpoints/90623

The following PRs are related:

* [[lldb] Propose MultiBreakpoint extension to GDB
Remote](llvm/llvm-project#192910)
* [[debugserver] Implement
MultiBreakpoint](llvm/llvm-project#192914)
* [[lldb-server][NFC] Factor out code handling breakpoint
packets](llvm/llvm-project#192915)
* [[lldb-server] Implement support for MultiBreakpoint
packet](llvm/llvm-project#192919)
* [[lldb][GDBRemote] Parse MultiBreakpoint+
capability](llvm/llvm-project#192962)
* [[lldb][NFC] Move BreakpointSite::IsEnabled/SetEnabled into
Process](llvm/llvm-project#192964)
* [[lldb] Implement delayed
breakpoints](llvm/llvm-project#192971)
* [[lldb] Override UpdateBreakpointSites in ProcessGDBRemote to use
MultiBreakpoint](llvm/llvm-project#192988)
cpullvm-upstream-sync Bot pushed a commit to navaneethshan/cpullvm-toolchain-1 that referenced this pull request Apr 25, 2026
…92910)

This describes the packet discussed in the RFC:
https://discourse.llvm.org/t/rfc-a-new-packet-to-set-remove-multiple-breakpoints/90623

The following PRs are related:

* [[lldb] Propose MultiBreakpoint extension to GDB
Remote](llvm/llvm-project#192910)
* [[debugserver] Implement
MultiBreakpoint](llvm/llvm-project#192914)
* [[lldb-server][NFC] Factor out code handling breakpoint
packets](llvm/llvm-project#192915)
* [[lldb-server] Implement support for MultiBreakpoint
packet](llvm/llvm-project#192919)
* [[lldb][GDBRemote] Parse MultiBreakpoint+
capability](llvm/llvm-project#192962)
* [[lldb][NFC] Move BreakpointSite::IsEnabled/SetEnabled into
Process](llvm/llvm-project#192964)
* [[lldb] Implement delayed
breakpoints](llvm/llvm-project#192971)
* [[lldb] Override UpdateBreakpointSites in ProcessGDBRemote to use
MultiBreakpoint](llvm/llvm-project#192988)
@felipepiovezan felipepiovezan force-pushed the users/felipepiovezan/delayed_bps_p2 branch from e180c10 to 3794843 Compare April 27, 2026 07:55
@felipepiovezan felipepiovezan force-pushed the users/felipepiovezan/delayed_bps_p3 branch from 35a2784 to 84d12c8 Compare April 27, 2026 08:02
@felipepiovezan felipepiovezan marked this pull request as ready for review April 27, 2026 08:02
@llvmbot llvmbot added the lldb label Apr 27, 2026
@llvmbot

llvmbot commented Apr 27, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-lldb

Author: Felipe de Azevedo Piovezan (felipepiovezan)

Changes

This patch changes the Process class so that it delays physically enabling/disabling breakpoints until the process is about to resume/detach/be destroyed, potentially reducing the packets transmitted by batching all breakpoints together.

Most classes only need to know whether a breakpoint is "logically" enabled, as opposed to "physically" enabled (i.e. the remote server has actually enabled the breakpoint). However, lower level classes like derived Process classes, or StopInfo may actually need to know whether the breakpoint was physically enabled. As such, this commit also adds a "IsPhysicallyEnabled" API.

The following PRs are related to the MultiBreakpoint feature:


Patch is 20.73 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/192971.diff

9 Files Affected:

  • (modified) lldb/include/lldb/Target/Process.h (+30-1)
  • (modified) lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp (+2-2)
  • (modified) lldb/source/Plugins/Process/Utility/StopInfoMachException.cpp (+5-3)
  • (modified) lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp (+6-6)
  • (modified) lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp (+1-1)
  • (modified) lldb/source/Plugins/Process/scripted/ScriptedThread.cpp (+1-1)
  • (modified) lldb/source/Target/Process.cpp (+95-8)
  • (modified) lldb/source/Target/TargetProperties.td (+5)
  • (modified) lldb/source/Target/ThreadPlanStepOverBreakpoint.cpp (+4-2)
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index f90c11813ba48..88e9ac4bc802f 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -114,6 +114,7 @@ class ProcessProperties : public Properties {
   Args GetAlwaysRunThreadNames() const;
   FollowForkMode GetFollowForkMode() const;
   bool TrackMemoryCacheChanges() const;
+  bool GetUseDelayedBreakpoints() const;
 
 protected:
   Process *m_process; // Can be nullptr for global ProcessProperties
@@ -2246,6 +2247,9 @@ class Process : public std::enable_shared_from_this<Process>,
   // Process Breakpoints
   size_t GetSoftwareBreakpointTrapOpcode(BreakpointSite *bp_site);
 
+  enum class BreakpointAction { Enable, Disable };
+
+protected:
   virtual Status EnableBreakpointSite(BreakpointSite *bp_site) {
     return Status::FromErrorStringWithFormatv(
         "error: {0} does not support enabling breakpoints", GetPluginName());
@@ -2256,6 +2260,14 @@ class Process : public std::enable_shared_from_this<Process>,
         "error: {0} does not support disabling breakpoints", GetPluginName());
   }
 
+  virtual llvm::Error UpdateBreakpointSites(
+      const std::map<lldb::BreakpointSiteSP, BreakpointAction> &site_to_action);
+
+public:
+  Status ExecuteBreakpointSiteAction(BreakpointSite &site,
+                                     Process::BreakpointAction action,
+                                     bool force_now = false);
+
   // This is implemented completely using the lldb::Process API. Subclasses
   // don't need to implement this function unless the standard flow of read
   // existing opcode, write breakpoint opcode, verify breakpoint opcode doesn't
@@ -2280,7 +2292,8 @@ class Process : public std::enable_shared_from_this<Process>,
   lldb::break_id_t CreateBreakpointSite(const lldb::BreakpointLocationSP &owner,
                                         bool use_hardware);
 
-  Status DisableBreakpointSiteByID(lldb::user_id_t break_id);
+  Status DisableBreakpointSiteByID(lldb::user_id_t break_id,
+                                   bool force_now = false);
 
   Status EnableBreakpointSiteByID(lldb::user_id_t break_id);
 
@@ -2293,6 +2306,8 @@ class Process : public std::enable_shared_from_this<Process>,
                                            lldb::user_id_t constituent_id,
                                            lldb::BreakpointSiteSP &bp_site_sp);
 
+  bool IsBreakpointSitePhysicallyEnabled(BreakpointSite &site);
+
   // Process Watchpoints (optional)
   virtual Status EnableWatchpoint(lldb::WatchpointSP wp_sp, bool notify = true);
 
@@ -3541,6 +3556,20 @@ void PruneThreadPlans();
   /// GetExtendedCrashInformation.
   StructuredData::DictionarySP m_crash_info_dict_sp;
 
+  struct DelayedBreakpointCache {
+    void Enqueue(lldb::BreakpointSiteSP site, BreakpointAction action);
+    void RemoveSite(lldb::BreakpointSiteSP site) {
+      m_site_to_action.erase(site);
+    }
+    void Clear() { m_site_to_action.clear(); }
+
+    std::map<lldb::BreakpointSiteSP, BreakpointAction> m_site_to_action;
+  };
+
+  DelayedBreakpointCache m_delayed_breakpoints;
+
+  llvm::Error FlushDelayedBreakpoints();
+
   size_t RemoveBreakpointOpcodesFromBuffer(lldb::addr_t addr, size_t size,
                                            uint8_t *buf) const;
 
diff --git a/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp b/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp
index 6b3354aad09e6..6166096a4e1d3 100644
--- a/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp
+++ b/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp
@@ -633,7 +633,7 @@ Status ProcessKDP::EnableBreakpointSite(BreakpointSite *bp_site) {
 
   if (m_comm.LocalBreakpointsAreSupported()) {
     Status error;
-    if (!IsBreakpointSiteEnabled(bp_site)) {
+    if (!IsBreakpointSitePhysicallyEnabled(*bp_site)) {
       if (m_comm.SendRequestBreakpoint(true, bp_site->GetLoadAddress())) {
         SetBreakpointSiteEnabled(*bp_site);
         bp_site->SetType(BreakpointSite::eExternal);
@@ -649,7 +649,7 @@ Status ProcessKDP::EnableBreakpointSite(BreakpointSite *bp_site) {
 Status ProcessKDP::DisableBreakpointSite(BreakpointSite *bp_site) {
   if (m_comm.LocalBreakpointsAreSupported()) {
     Status error;
-    if (IsBreakpointSiteEnabled(bp_site)) {
+    if (IsBreakpointSitePhysicallyEnabled(*bp_site)) {
       BreakpointSite::Type bp_type = bp_site->GetType();
       if (bp_type == BreakpointSite::eExternal) {
         if (m_destroy_in_process && m_comm.IsRunning()) {
diff --git a/lldb/source/Plugins/Process/Utility/StopInfoMachException.cpp b/lldb/source/Plugins/Process/Utility/StopInfoMachException.cpp
index 9951fe1d9e293..5fbc1f62a065f 100644
--- a/lldb/source/Plugins/Process/Utility/StopInfoMachException.cpp
+++ b/lldb/source/Plugins/Process/Utility/StopInfoMachException.cpp
@@ -636,7 +636,7 @@ StopInfoSP StopInfoMachException::CreateStopReasonWithMachException(
   addr_t pc = reg_ctx_sp->GetPC();
   BreakpointSiteSP bp_site_sp =
       process_sp->GetBreakpointSiteList().FindByAddress(pc);
-  if (bp_site_sp && process_sp->IsBreakpointSiteEnabled(bp_site_sp))
+  if (bp_site_sp && process_sp->IsBreakpointSitePhysicallyEnabled(*bp_site_sp))
     thread.SetThreadStoppedAtUnexecutedBP(pc);
 
   switch (exc_type) {
@@ -771,7 +771,8 @@ StopInfoSP StopInfoMachException::CreateStopReasonWithMachException(
       if (!bp_site_sp && reg_ctx_sp) {
         bp_site_sp = process_sp->GetBreakpointSiteList().FindByAddress(pc);
       }
-      if (bp_site_sp && process_sp->IsBreakpointSiteEnabled(bp_site_sp)) {
+      if (bp_site_sp &&
+          process_sp->IsBreakpointSitePhysicallyEnabled(*bp_site_sp)) {
         // We've hit this breakpoint, whether it was intended for this thread
         // or not.  Clear this in the Tread object so we step past it on resume.
         thread.SetThreadHitBreakpointSite();
@@ -865,7 +866,8 @@ bool StopInfoMachException::WasContinueInterrupted(Thread &thread) {
   // We have a hardware breakpoint -- this is the kernel bug.
   auto &bp_site_list = process_sp->GetBreakpointSiteList();
   for (auto &site : bp_site_list.Sites()) {
-    if (site->IsHardware() && process_sp->IsBreakpointSiteEnabled(site)) {
+    if (site->IsHardware() &&
+        process_sp->IsBreakpointSitePhysicallyEnabled(*site)) {
       LLDB_LOGF(log,
                 "Thread stopped with insn-step completed mach exception but "
                 "thread was not stepping; there is a hardware breakpoint set.");
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index 145d69d6b6d64..33d3713c948c4 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -1848,7 +1848,7 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
     addr_t pc = thread_sp->GetRegisterContext()->GetPC();
     BreakpointSiteSP bp_site_sp =
         thread_sp->GetProcess()->GetBreakpointSiteList().FindByAddress(pc);
-    if (bp_site_sp && IsBreakpointSiteEnabled(bp_site_sp))
+    if (bp_site_sp && IsBreakpointSitePhysicallyEnabled(*bp_site_sp))
       thread_sp->SetThreadStoppedAtUnexecutedBP(pc);
 
     if (exc_type != 0) {
@@ -2032,7 +2032,7 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
           // BreakpointSites in any other location, but we can't know for
           // sure what happened so it's a reasonable default.
           if (bp_site_sp) {
-            if (IsBreakpointSiteEnabled(bp_site_sp))
+            if (IsBreakpointSitePhysicallyEnabled(*bp_site_sp))
               thread_sp->SetThreadHitBreakpointSite();
 
             if (bp_site_sp->ValidForThisThread(*thread_sp)) {
@@ -3429,7 +3429,7 @@ Status ProcessGDBRemote::EnableBreakpointSite(BreakpointSite *bp_site) {
             site_id, (uint64_t)addr);
 
   // Breakpoint already exists and is enabled
-  if (IsBreakpointSiteEnabled(bp_site)) {
+  if (IsBreakpointSitePhysicallyEnabled(*bp_site)) {
     LLDB_LOGF(log,
               "ProcessGDBRemote::EnableBreakpointSite (size_id = %" PRIu64
               ") address = 0x%" PRIx64 " -- SUCCESS (already enabled)",
@@ -3450,7 +3450,7 @@ Status ProcessGDBRemote::DisableBreakpointSite(BreakpointSite *bp_site) {
             ") addr = 0x%8.8" PRIx64,
             site_id, (uint64_t)addr);
 
-  if (!IsBreakpointSiteEnabled(bp_site)) {
+  if (!IsBreakpointSitePhysicallyEnabled(*bp_site)) {
     LLDB_LOGF(log,
               "ProcessGDBRemote::DisableBreakpointSite (site_id = %" PRIu64
               ") addr = 0x%8.8" PRIx64 " -- SUCCESS (already disabled)",
@@ -6112,7 +6112,7 @@ void ProcessGDBRemote::DidForkSwitchSoftwareBreakpoints(
 
   GetBreakpointSiteList().ForEach([this, enable, entry_addr,
                                    log](BreakpointSite *bp_site) {
-    if (IsBreakpointSiteEnabled(bp_site) &&
+    if (IsBreakpointSitePhysicallyEnabled(*bp_site) &&
         (bp_site->GetType() == BreakpointSite::eSoftware ||
          bp_site->GetType() == BreakpointSite::eExternal)) {
       // During expression evaluation, retain the expression-return trap
@@ -6136,7 +6136,7 @@ void ProcessGDBRemote::DidForkSwitchSoftwareBreakpoints(
 void ProcessGDBRemote::DidForkSwitchHardwareTraps(bool enable) {
   if (m_gdb_comm.SupportsGDBStoppointPacket(eBreakpointHardware)) {
     GetBreakpointSiteList().ForEach([this, enable](BreakpointSite *bp_site) {
-      if (IsBreakpointSiteEnabled(bp_site) &&
+      if (IsBreakpointSitePhysicallyEnabled(*bp_site) &&
           bp_site->GetType() == BreakpointSite::eHardware) {
         m_gdb_comm.SendGDBStoppointTypePacket(
             eBreakpointHardware, enable, bp_site->GetLoadAddress(),
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
index b0718b771295b..c254a6841b707 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
@@ -265,7 +265,7 @@ size_t ScriptedProcess::DoWriteMemory(lldb::addr_t vm_addr, const void *buf,
 Status ScriptedProcess::EnableBreakpointSite(BreakpointSite *bp_site) {
   assert(bp_site != nullptr);
 
-  if (IsBreakpointSiteEnabled(bp_site)) {
+  if (IsBreakpointSitePhysicallyEnabled(*bp_site)) {
     return {};
   }
 
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
index aacfcc29e5f97..d11d134295579 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
@@ -316,7 +316,7 @@ bool ScriptedThread::CalculateStopInfo() {
     ProcessSP proc = GetProcess();
     if (BreakpointSiteSP bp_site_sp =
             proc->GetBreakpointSiteList().FindByAddress(pc))
-      if (proc->IsBreakpointSiteEnabled(bp_site_sp))
+      if (proc->IsBreakpointSitePhysicallyEnabled(*bp_site_sp))
         SetThreadStoppedAtUnexecutedBP(pc);
   }
 
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index f9b6c4a79dd09..1a2323f5de136 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -79,6 +79,17 @@ using namespace lldb;
 using namespace lldb_private;
 using namespace std::chrono;
 
+void Process::DelayedBreakpointCache::Enqueue(lldb::BreakpointSiteSP site,
+                                              BreakpointAction action) {
+  auto [previous, inserted] = m_site_to_action.insert({site, action});
+  // New site or already enqueued for the same action
+  if (inserted || previous->second == action)
+    return;
+  // Previously enqueued for the opposite action, don't update the site.
+  m_site_to_action.erase(previous);
+  assert(site->m_enabled == (action == BreakpointAction::Enable));
+}
+
 class ProcessOptionValueProperties
     : public Cloneable<ProcessOptionValueProperties, OptionValueProperties> {
 public:
@@ -322,6 +333,12 @@ bool ProcessProperties::GetStopOnExec() const {
       idx, g_process_properties[idx].default_uint_value != 0);
 }
 
+bool ProcessProperties::GetUseDelayedBreakpoints() const {
+  const uint32_t idx = ePropertyUseDelayedBreakpoints;
+  return GetPropertyAtIndexAs<bool>(
+      idx, g_process_properties[idx].default_uint_value != 0);
+}
+
 std::chrono::seconds ProcessProperties::GetUtilityExpressionTimeout() const {
   const uint32_t idx = ePropertyUtilityExpressionTimeout;
   uint64_t value = GetPropertyAtIndexAs<uint64_t>(
@@ -1546,12 +1563,12 @@ Process::GetBreakpointSiteList() const {
 
 void Process::DisableAllBreakpointSites() {
   m_breakpoint_site_list.ForEach([this](BreakpointSite *bp_site) -> void {
-    DisableBreakpointSite(bp_site);
+    ExecuteBreakpointSiteAction(*bp_site, BreakpointAction::Disable);
   });
 }
 
 Status Process::ClearBreakpointSiteByID(lldb::user_id_t break_id) {
-  Status error(DisableBreakpointSiteByID(break_id));
+  Status error(DisableBreakpointSiteByID(break_id, /*force_now=*/true));
 
   if (error.Success())
     m_breakpoint_site_list.Remove(break_id);
@@ -1559,12 +1576,14 @@ Status Process::ClearBreakpointSiteByID(lldb::user_id_t break_id) {
   return error;
 }
 
-Status Process::DisableBreakpointSiteByID(lldb::user_id_t break_id) {
+Status Process::DisableBreakpointSiteByID(lldb::user_id_t break_id,
+                                          bool force_now) {
   Status error;
   BreakpointSiteSP bp_site_sp = m_breakpoint_site_list.FindByID(break_id);
   if (bp_site_sp) {
     if (IsBreakpointSiteEnabled(bp_site_sp))
-      error = DisableBreakpointSite(bp_site_sp.get());
+      error = ExecuteBreakpointSiteAction(*bp_site_sp,
+                                          BreakpointAction::Disable, force_now);
   } else {
     error = Status::FromErrorStringWithFormat(
         "invalid breakpoint site ID: %" PRIu64, break_id);
@@ -1573,12 +1592,34 @@ Status Process::DisableBreakpointSiteByID(lldb::user_id_t break_id) {
   return error;
 }
 
+Status Process::ExecuteBreakpointSiteAction(BreakpointSite &site,
+                                            BreakpointAction action,
+                                            bool force_now) {
+  if (!force_now && GetUseDelayedBreakpoints()) {
+    m_delayed_breakpoints.Enqueue(site.shared_from_this(), action);
+    return Status();
+  }
+
+  auto site_sp = site.shared_from_this();
+  m_delayed_breakpoints.RemoveSite(site_sp);
+
+  switch (action) {
+  case BreakpointAction::Enable:
+    return EnableBreakpointSite(site_sp.get());
+  case BreakpointAction::Disable:
+    return DisableBreakpointSite(site_sp.get());
+  }
+
+  llvm_unreachable("Unhandled BreakpointAction");
+}
+
 Status Process::EnableBreakpointSiteByID(lldb::user_id_t break_id) {
   Status error;
   BreakpointSiteSP bp_site_sp = m_breakpoint_site_list.FindByID(break_id);
   if (bp_site_sp) {
     if (!IsBreakpointSiteEnabled(bp_site_sp))
-      error = EnableBreakpointSite(bp_site_sp.get());
+      error =
+          ExecuteBreakpointSiteAction(*bp_site_sp, BreakpointAction::Enable);
   } else {
     error = Status::FromErrorStringWithFormat(
         "invalid breakpoint site ID: %" PRIu64, break_id);
@@ -1588,12 +1629,21 @@ Status Process::EnableBreakpointSiteByID(lldb::user_id_t break_id) {
 
 bool Process::IsBreakpointSiteEnabled(const lldb::BreakpointSiteSP &site) {
   assert(site);
-  return site->m_enabled;
+  auto it = m_delayed_breakpoints.m_site_to_action.find(site);
+  // If no actions are delayed, use the current state of the site.
+  if (it == m_delayed_breakpoints.m_site_to_action.end())
+    return site->m_enabled;
+
+  return it->second == BreakpointAction::Enable;
 }
 
 bool Process::IsBreakpointSiteEnabled(BreakpointSite *site) {
   assert(site);
-  return site->m_enabled;
+  return IsBreakpointSiteEnabled(site->shared_from_this());
+}
+
+bool Process::IsBreakpointSitePhysicallyEnabled(BreakpointSite &site) {
+  return site.m_enabled;
 }
 
 static bool ShouldShowError(Process &process) {
@@ -1654,6 +1704,31 @@ static addr_t ComputeConstituentLoadAddress(BreakpointLocation &constituent,
   return resolved_address.GetOpcodeLoadAddress(&target);
 }
 
+llvm::Error Process::FlushDelayedBreakpoints() {
+  // Clear the cache in m_delayed_breakpoints so it can't affect the actual
+  // enabling of breakpoints. For
+  // example, if `EnableSoftwareBreakpoint` is called outside of
+  // FlushDelayedBreakpoints, it needs to check the delayed breakpoints and
+  // possibly early return. However, when called from FlushDelayedBreakpoints,
+  // the queue better be empty so that no early returns take place.
+  auto site_to_action = std::move(m_delayed_breakpoints.m_site_to_action);
+
+  auto error = UpdateBreakpointSites(site_to_action);
+  return error;
+}
+
+llvm::Error Process::UpdateBreakpointSites(
+    const std::map<lldb::BreakpointSiteSP, BreakpointAction> &site_to_action) {
+  llvm::Error error = llvm::Error::success();
+  for (auto [site, action] : site_to_action) {
+    Status new_error = action == BreakpointAction::Enable
+                           ? EnableBreakpointSite(site.get())
+                           : DisableBreakpointSite(site.get());
+    error = llvm::joinErrors(std::move(error), new_error.takeError());
+  }
+  return error;
+}
+
 lldb::break_id_t
 Process::CreateBreakpointSite(const BreakpointLocationSP &constituent,
                               bool use_hardware) {
@@ -1698,7 +1773,8 @@ void Process::RemoveConstituentFromBreakpointSite(
   if (num_constituents == 0) {
     // Don't try to disable the site if we don't have a live process anymore.
     if (IsAlive())
-      DisableBreakpointSite(bp_site_sp.get());
+      ExecuteBreakpointSiteAction(*bp_site_sp, BreakpointAction::Disable,
+                                  /*force_now=*/true);
     m_breakpoint_site_list.RemoveByAddress(bp_site_sp->GetLoadAddress());
   }
 }
@@ -3441,6 +3517,9 @@ Status Process::PrivateResume() {
             "Process::PrivateResume PreResumeActions failed, not resuming.");
       } else {
         m_mod_id.BumpResumeID();
+        if (auto E = FlushDelayedBreakpoints())
+          LLDB_LOG_ERROR(log, std::move(E),
+                         "Failed to update some delayed breakpoints: {0}");
         error = DoResume(direction);
         if (error.Success()) {
           DidResume();
@@ -3647,6 +3726,10 @@ Status Process::Detach(bool keep_stopped) {
 
     m_thread_list.DiscardThreadPlans();
     DisableAllBreakpointSites();
+    if (auto error = FlushDelayedBreakpoints())
+      LLDB_LOG_ERROR(
+          GetLog(LLDBLog::Process), std::move(error),
+          "Failed to update some delayed breakpoints during detach: {0}");
 
     error = DoDetach(keep_stopped);
     if (error.Success()) {
@@ -3716,6 +3799,10 @@ Status Process::DestroyImpl(bool force_kill) {
       // doing this now.
       m_thread_list.DiscardThreadPlans();
       DisableAllBreakpointSites();
+      if (auto error = FlushDelayedBreakpoints())
+        LLDB_LOG_ERROR(
+            GetLog(LLDBLog::Process), std::move(error),
+            "Failed to update some delayed breakpoints during destroy: {0}");
     }
 
     error = DoDestroy();
diff --git a/lldb/source/Target/TargetProperties.td b/lldb/source/Target/TargetProperties.td
index 223a12e059258..0a46125594ab6 100644
--- a/lldb/source/Target/TargetProperties.td
+++ b/lldb/source/Target/TargetProperties.td
@@ -319,6 +319,11 @@ let Definition = "process", Path = "target.process" in {
         Desc<"A list of thread names. Threads with any of these names will "
              "always be resumed when the process resumes, even when other "
              "threads are suspended during single-stepping operations.">;
+  def UseDelayedBreakpoints
+      : Property<"use-delayed-breakpoints", "Boolean">,
+        DefaultTrue,
+        Desc<"Specify whether to delay setting breakpoints until the process "
+             "is about to resume.">;
 }
 
 let Definition = "platform", Path = "platform" in {
diff --git a/lldb/source/Target/ThreadPlanStepOverBreakpoint.cpp b/lldb/source/Target/ThreadPlanStepOverBreakpoint.cpp
index 597577c208f8b..b215b4be76602 100644
--- a/lldb/source/Target/ThreadPlanStepOverBreakpoint.cpp
+++ b/lldb/source/Target/ThreadPlanStepOverBreakpoint.cpp
@@ -121,7 +121,8 @@ bool ThreadPlanStepOverBreakpoint::DoWillResume(StateType resume_state,
     BreakpointSiteSP bp_site_sp(
         m_process.GetBreakpointSiteList().FindByAddress(m_breakpoint_a...
[truncated]

@DavidSpickett DavidSpickett left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a release note about this but it's fine to wait until you've landed your stack and you know what to include.

Comment thread lldb/include/lldb/Target/Process.h Outdated
}
void Clear() { m_site_to_action.clear(); }

std::map<lldb::BreakpointSiteSP, BreakpointAction> m_site_to_action;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have ordering requirements for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, we do. Iteration order must not depend on pointer values.
I wanted to use a SetVector here, but this data structure requires some boilerplate to work with shared pointers, and it also is not great when deletions are frequent (which we expect to be the case with breakpoints). I will add a custom comparison functor.

// FlushDelayedBreakpoints, it needs to check the delayed breakpoints and
// possibly early return. However, when called from FlushDelayedBreakpoints,
// the queue better be empty so that no early returns take place.
auto site_to_action = std::move(m_delayed_breakpoints.m_site_to_action);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly is the purpose of move here?

I am wary of a use after move with m_site_to_action later. std::map might be ok with this but I don't know if it has to be the case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objects must be left in an usable state after a move, but I agree this is a bit too subtle. The purpose of the move is to clean the container / safely allow for iteration (with the rationale above). I can throw in an extra .clear() there.

Comment thread lldb/source/Target/TargetProperties.td Outdated
: Property<"use-delayed-breakpoints", "Boolean">,
DefaultTrue,
Desc<"Specify whether to delay setting breakpoints until the process "
"is about to resume.">;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any exceptions to this at this time?

We talked about hardware breaks for example. I'm not sure whether we need that exception actually but if you've implemented it already it should be noted here.

And on that topic, users could turn off this setting when hardware resources are a concern couldn't they.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this says "setting" which sounds like one half of the story. "setting or removing", "updating", something like that. Updating sounds a bit like it only works for ones that already exist. So some "X an Y-ing" is probably better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any exceptions to this at this time?

We do, like when the BreakpointSite is created for the first time. But these are very low level explanations that I really would want to avoid providing in the help string.... I'll reword this to say "may be delayed" instead of "is delayed".

And on that topic, users could turn off this setting when hardware resources are a concern couldn't they.

yes!

I'm not sure whether we need that exception actually but if you've implemented it already it should be noted here.

I haven't implemented that kind of exception yet. If we feel like we should do it together with this patch, I can have a look now. One idea @jimingham had was to check if we have already reached the number of hardware breakpoints the hardware allows, as we should, in theory, be able to calculate that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this says "setting" which sounds like one half of the story.

true, will update to "updating".

google-bazel-bot Bot pushed a commit to google-bazel-bot/llvm-project that referenced this pull request Apr 27, 2026
This implements the packet as described in
llvm#192910

The following PRs are related to the MultiBreakpoint feature:

* llvm#192910
* llvm#192914
* llvm#192915
* llvm#192919
* llvm#192962
* llvm#192964
* llvm#192971
* llvm#192988
felipepiovezan added a commit that referenced this pull request Apr 27, 2026
This commit extracts the code handling breakpoint packets into a helper
function that can be used by a future implementation of the
MultiBreakpointPacket.

It is meant to be purely NFC.

There are two functions handling breakpoint packets (`handle_Z` and
`handle_z`) with a lot of repeated code. This commit did not attempt to
merge the two, as that would make the diff much larger due to subtle
differences in the error message produced by the two. The only
deduplication done is in the code processing a GDBStoppointType, where a
helper struct (`BreakpointKind`) and function
(`std::optional<BreakpointKind> getBreakpointKind(GDBStoppointType
stoppoint_type)`) was created.

The following PRs are related to the MultiBreakpoint feature:

* #192910
* #192914
* #192915
* #192919
* #192962
* #192964
* #192971
* #192988
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request Apr 27, 2026
@felipepiovezan felipepiovezan merged commit 776ee6f into main May 1, 2026
11 checks passed
@felipepiovezan felipepiovezan deleted the users/felipepiovezan/delayed_bps_p3 branch May 1, 2026 06:52
cpullvm-upstream-sync Bot pushed a commit to navaneethshan/cpullvm-toolchain-1 that referenced this pull request May 1, 2026
This patch changes the Process class so that it delays *physically*
enabling/disabling breakpoints until the process is about to
resume/detach/be destroyed, potentially reducing the packets transmitted
by batching all breakpoints together.

Most classes only need to know whether a breakpoint is "logically"
enabled, as opposed to "physically" enabled (i.e. the remote server has
actually enabled the breakpoint). However, lower level classes like
derived Process classes, or StopInfo may actually need to know whether
the breakpoint was physically enabled. As such, this commit also adds a
"IsPhysicallyEnabled" API.

The following PRs are related to the MultiBreakpoint feature:

* llvm/llvm-project#192910
* llvm/llvm-project#192914
* llvm/llvm-project#192915
* llvm/llvm-project#192919
* llvm/llvm-project#192962
* llvm/llvm-project#192964
* llvm/llvm-project#192971
* llvm/llvm-project#192988
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request May 1, 2026
This patch changes the Process class so that it delays *physically*
enabling/disabling breakpoints until the process is about to
resume/detach/be destroyed, potentially reducing the packets transmitted
by batching all breakpoints together.

Most classes only need to know whether a breakpoint is "logically"
enabled, as opposed to "physically" enabled (i.e. the remote server has
actually enabled the breakpoint). However, lower level classes like
derived Process classes, or StopInfo may actually need to know whether
the breakpoint was physically enabled. As such, this commit also adds a
"IsPhysicallyEnabled" API.

The following PRs are related to the MultiBreakpoint feature:

* llvm/llvm-project#192910
* llvm/llvm-project#192914
* llvm/llvm-project#192915
* llvm/llvm-project#192919
* llvm/llvm-project#192962
* llvm/llvm-project#192964
* llvm/llvm-project#192971
* llvm/llvm-project#192988
llvm-upstreamsync Bot pushed a commit to qualcomm/cpullvm-toolchain that referenced this pull request May 1, 2026
This patch changes the Process class so that it delays *physically*
enabling/disabling breakpoints until the process is about to
resume/detach/be destroyed, potentially reducing the packets transmitted
by batching all breakpoints together.

Most classes only need to know whether a breakpoint is "logically"
enabled, as opposed to "physically" enabled (i.e. the remote server has
actually enabled the breakpoint). However, lower level classes like
derived Process classes, or StopInfo may actually need to know whether
the breakpoint was physically enabled. As such, this commit also adds a
"IsPhysicallyEnabled" API.

The following PRs are related to the MultiBreakpoint feature:

* llvm/llvm-project#192910
* llvm/llvm-project#192914
* llvm/llvm-project#192915
* llvm/llvm-project#192919
* llvm/llvm-project#192962
* llvm/llvm-project#192964
* llvm/llvm-project#192971
* llvm/llvm-project#192988
@slydiman

slydiman commented May 1, 2026

Copy link
Copy Markdown
Contributor

https://lab.llvm.org/buildbot/#/builders/211/builds/7890
It seems a lot of API tests on Windows x86_64 failed with an exception

Error output:
error: Expression execution was interrupted: Exception 0xc0000005 encountered at address 0x7ff6b7cd76df: Access violation reading location 0xffffffffffffffff.
The process has been returned to the state before expression evaluation.

and shell test

FAIL: lldb-shell::expressions.test
[view all 92 lines](https://lab.llvm.org/buildbot/#/builders/211/builds/7890/steps/10/logs/FAIL__lldb-shell__expressions_test) 
# | <<<<<<
# |             .
# |             .
# |             .
# |            19:  20 } 
# |            20: (lldb) expression result 
# |            21: (char) $0 = '\x1c' 
# |            22: (lldb) expression N0::N1::sum(N0::N1::buf1, sizeof(N0::N1::buf1)) 
# |            23: (char) $1 = '\x1c' 
# |            24: (lldb) expression N1::sum(N1::buf1, sizeof(N1::buf1)) 
# | check:16'0                                                          X error: no match found
# |            25:   
# | check:16'0     ~~
# |            26: error: Expression execution was interrupted: Exception 0xc0000005 encountered at address 0x7ff6b94b3000: User-mode data execution prevention (DEP) violation at location 0x7ff6b94b3000. 
# | check:16'0

Please take a look.

@slydiman

slydiman commented May 1, 2026

Copy link
Copy Markdown
Contributor

Also look at this issue #191222.
It is probably connected in some way.

@felipepiovezan

Copy link
Copy Markdown
Contributor Author

Also look at this issue #191222. It is probably connected in some way.

Yeah... I requested a rerun just in case. If it reproduces, I will probably disable delayed breakpoints on windows, since this seems to be a known problem there

@felipepiovezan

felipepiovezan commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

#195241

@labath

labath commented May 4, 2026

Copy link
Copy Markdown
Contributor

I think there may be a problem with this patch (and that the windows fix maybe just papers over the issue).

Let me illustrate what I mean this with the following packet log. I'm sorry for the length -- I tried to remove the really boring parts, but I didn't want to leave out too much context. The most interesting lines are marked with <===.

lldb             <   1> send packet: +
lldb             <  19> send packet: $QStartNoAckMode#b0
lldb             <   1> read packet: +
lldb             <   6> read packet: $OK#9a
lldb             <   1> send packet: +
lldb             < 104> send packet: $qSupported:xmlRegisters=i386,arm,mips,arc;multiprocess+;fork-events
+;vfork-events+;swbreak+;hwbreak+#cd
lldb             < 278> read packet: $PacketSize=131072;QStartNoAckMode+;qEcho+;native-signals+;QThreadSu
ffixSupported+;QListThreadsInStopReply+;qXfer:features:read+;QNonStop+;jMultiBreakpoint+;QPassSignals+;qX
fer:auxv:read+;qXfer:libraries-svr4:read+;qXfer:siginfo:read+;multiprocess+;fork-events+;vfork-events+#6c
lldb             <  26> send packet: $QThreadSuffixSupported#e4
lldb             <   6> read packet: $OK#9a
lldb             <  27> send packet: $QListThreadsInStopReply#21
lldb             <   6> read packet: $OK#9a
lldb             <  13> send packet: $qHostInfo#9b
lldb             < 338> read packet: $triple:7838365f36342d2d6c696e75782d676e75;ptrsize:8;distribution_id
:67656e746f6f;watchpoint_exceptions_received:after;endian:little;os_version:6.18.18;os_build:362e31382e31
382d67656e746f6f;os_kernel:233120534d5020505245454d50545f44594e414d494320546875204170722020322030393a3131
3a303320434553542032303236;hostname:756e696d617472697830;#f6
<snip>
lldb             <  29> send packet: $vRun;2f746d702f612e6f7574#f3
lldb             < 611> read packet: $T13thread:pe3f8.e3f8;name:a.out;threads:e3f8;thread-pcs:00000000004
027c0;00:0000000000000000;01:0000000000000000;02:0000000000000000;03:0000000000000000;04:0000000000000000
;05:0000000000000000;06:0000000000000000;07:f0d8ffffff7f0000;08:0000000000000000;09:0000000000000000;0a:0
000000000000000;0b:0000000000000000;0c:0000000000000000;0d:0000000000000000;0e:0000000000000000;0f:000000
0000000000;10:c027400000000000;11:0202000000000000;12:3300000000000000;13:0000000000000000;14:00000000000
00000;15:2b00000000000000;16:0000000000000000;17:0000000000000000;18:0000000000000000;19:0000000000000000
;reason:signal;#09
lldb             <  16> send packet: $qProcessInfo#dc
lldb             < 170> read packet: $pid:e3f8;parent-pid:e3f2;real-uid:3e8;real-gid:3e8;effective-uid:3e
8;effective-gid:3e8;triple:7838365f36342d2d6c696e75782d676e75;ostype:linux;endian:little;ptrsize:8;#bf
<snip>
lldb             <  23> send packet: $qThreadStopInfoe3f8#31
lldb             < 611> read packet: $T13thread:pe3f8.e3f8;name:a.out;threads:e3f8;thread-pcs:00000000004
027c0;00:0000000000000000;01:0000000000000000;02:0000000000000000;03:0000000000000000;04:0000000000000000
;05:0000000000000000;06:0000000000000000;07:f0d8ffffff7f0000;08:0000000000000000;09:0000000000000000;0a:0
000000000000000;0b:0000000000000000;0c:0000000000000000;0d:0000000000000000;0e:0000000000000000;0f:000000
0000000000;10:c027400000000000;11:0202000000000000;12:3300000000000000;13:0000000000000000;14:00000000000
00000;15:2b00000000000000;16:0000000000000000;17:0000000000000000;18:0000000000000000;19:0000000000000000
;reason:signal;#09
lldb             <  15> send packet: $Z0,4137f0,1#78
lldb             <   6> read packet: $OK#9a
lldb             <  15> send packet: $Z0,40290c,1#75
lldb             <   6> read packet: $OK#9a
b-remote.async>  <  20> send packet: $vCont;c:pe3f8.-1#14
Process 58360 launched: '/tmp/a.out' (x86_64)
b-remote.async>  < 615> read packet: $T05thread:pe3f8.e3f8;name:a.out;threads:e3f8;thread-pcs:00000000004
0290c;00:10d8ffffff7f0000;01:08d9ffffff7f0000;02:1101000000000000;03:08d9ffffff7f0000;04:0100000000000000
;05:f8d8ffffff7f0000;06:e0d7ffffff7f0000;07:e0d7ffffff7f0000;08:0001000000000000;09:0400000000000000;0a:8
0914a0000000000;0b:0f00000000000000;0c:f8d8ffffff7f0000;0d:0200000000000000;0e:604d4a0000000000;0f:020000
0000000000;10:0c29400000000000;11:4602000000000000;12:3300000000000000;13:0000000000000000;14:00000000000
00000;15:2b00000000000000;16:c0134b0000000000;17:0000000000000000;18:0000000000000000;19:0000000000000000
;reason:breakpoint;#60
<snip>
Process 58360 stopped
* thread #1, name = 'a.out', stop reason = breakpoint 1.1
       frame #0: 0x000000000040290c a.out`main at a.c:3:28
   1   	int call_me() { return 47; }
   2   	
-> 3   	int main() { return call_me(); }
(lldb) expr -- call_me()
lldb             <  14> send packet: $_M1000,rwx#fa
lldb             <  20> read packet: $00007ffff7ff6000#58
lldb             <  13> send packet: $_M1000,rw#82
lldb             <  20> read packet: $00007ffff7ff5000#57
lldb             <  13> send packet: $_M1000,rx#83
lldb             <  20> read packet: $00007ffff7ff4000#56
lldb             <  12> send packet: $_M1000,r#0b
lldb             <  20> read packet: $00007ffff7ff3000#55
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff5000#ab
lldb             <  55> read packet: $start:7ffff7ff5000;size:1000;permissions:rw;flags:;#c7
lldb             <  22> send packet: $M7ffff7ff5000,1:00#db
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff4000#aa
lldb             <  55> read packet: $start:7ffff7ff4000;size:1000;permissions:rx;flags:;#c7
lldb             <  97> send packet: $M7ffff7ff4000,26:f30f1efa554889e55350488b1f48897df048b8f52840000000
0000ffd089034883c4085b5dc3#b5
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff3000#a9
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             <  22> send packet: $M7ffff7ff3000,1:00#d9
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff3010#aa
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             < 379> send packet: $M7ffff7ff3010,b3:00245f5f6c6c64625f66756e633a3a3078313a307835383a5f
5a3763616c6c5f6d6576005f5a3132245f5f6c6c64625f657870725076002e72656c612e6c74657874002e74657874002e636f6d6
d656e74005f5a47565a3132245f5f6c6c64625f657870725076453139245f5f6c6c64625f657870725f726573756c74002e6c6273
73002e6e6f74652e474e552d737461636b00245f5f6c6c64625f6d6f64756c65002e737472746162002e73796d74616200#d9
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff4040#ae
lldb             <  55> read packet: $start:7ffff7ff4000;size:1000;permissions:rx;flags:;#c7
lldb             <  22> send packet: $M7ffff7ff4040,1:00#de
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff30d0#dd
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             <  69> send packet: $M7ffff7ff30d0,18:130000000000000001000000040000000000000000000000#e
e
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff30f0#df
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             <  71> send packet: $M7ffff7ff30f0,19:00636c616e672076657273696f6e2032332e302e3067697400
#34
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff3110#ab
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             <  22> send packet: $M7ffff7ff3110,1:00#db
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff3120#ac
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             < 261> send packet: $M7ffff7ff3120,78:00000000000000000000000000000000000000000000000094
0000000400f1ff0000000000000000000000000000000052000000010005000000000000000000010000000000000024000000120
0030000000000000000002600000000000000010000001000000000000000000000000000000000000000#92
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff5020#ad
lldb             <  55> read packet: $start:7ffff7ff5000;size:1000;permissions:rw;flags:;#c7
lldb             <  34> send packet: $M7ffff7ff5020,7:00000000000000#23
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff5020#ad
lldb             <  55> read packet: $start:7ffff7ff5000;size:1000;permissions:rw;flags:;#c7
lldb             <  28> send packet: $M7ffff7ff5020,4:00000000#00
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff5010#ac
lldb             <  55> read packet: $start:7ffff7ff5000;size:1000;permissions:rw;flags:;#c7
lldb             <  36> send packet: $M7ffff7ff5010,8:2050fff7ff7f0000#dc
lldb             <   6> read packet: $OK#9a
lldb             <  35> send packet: $QSaveRegisterState;thread:e3f8;#84
lldb             <   5> read packet: $1#31
lldb             <  36> send packet: $P4=1050fff7ff7f0000;thread:e3f8;#77
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7fffffffd758#1d
lldb             <  76> read packet: $start:7ffffffdd000;size:22000;permissions:rw;flags:;name:5b73746163
6b5d;#d9
lldb             <  36> send packet: $M7fffffffd758,8:c027400000000000#34
lldb             <   6> read packet: $OK#9a
lldb             <  36> send packet: $P7=58d7ffffff7f0000;thread:e3f8;#eb
lldb             <   6> read packet: $OK#9a
lldb             <  37> send packet: $P10=0040fff7ff7f0000;thread:e3f8;#a2
lldb             <   6> read packet: $OK#9a
lldb             <  15> send packet: $Z0,4027c0,1#73                # <========= ENTRY BKTP SET
lldb             <   6> read packet: $OK#9a
lldb             <  20> send packet: $p10;thread:e3f8;#2f
lldb             <  20> read packet: $0040fff7ff7f0000#56
b-remote.async>  <  20> send packet: $vCont;c:pe3f8.-1#14
b-remote.async>  < 615> read packet: $T05thread:pe3f8.e3f8;name:a.out;threads:e3f8;thread-pcs:00000000004
027c0;00:2f00000000000000;01:08d9ffffff7f0000;02:1101000000000000;03:08d9ffffff7f0000;04:1050fff7ff7f0000
;05:f8d8ffffff7f0000;06:e0d7ffffff7f0000;07:60d7ffffff7f0000;08:0001000000000000;09:0400000000000000;0a:8
0914a0000000000;0b:0f00000000000000;0c:f8d8ffffff7f0000;0d:0200000000000000;0e:604d4a0000000000;0f:020000
0000000000;10:c027400000000000;11:0602000000000000;12:3300000000000000;13:0000000000000000;14:00000000000
00000;15:2b00000000000000;16:c0134b0000000000;17:0000000000000000;18:0000000000000000;19:0000000000000000
;reason:breakpoint;#fa
<snip>
intern-state     <  40> send packet: $QRestoreRegisterState:1;thread:e3f8;#44
intern-state     <   6> read packet: $OK#9a
<snip>
(int) $0 = 47
(lldb) expr -- call_me()
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff5000#ab
lldb             <  55> read packet: $start:7ffff7ff5000;size:1000;permissions:rw;flags:;#c7
lldb             <  22> send packet: $M7ffff7ff5000,1:00#db
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff4000#aa
lldb             <  55> read packet: $start:7ffff7ff4000;size:1000;permissions:rx;flags:;#c7
lldb             <  97> send packet: $M7ffff7ff4000,26:f30f1efa554889e55350488b1f48897df048b8f52840000000
0000ffd089034883c4085b5dc3#b5
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff3000#a9
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             <  22> send packet: $M7ffff7ff3000,1:00#d9
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff3010#aa
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             < 379> send packet: $M7ffff7ff3010,b3:00245f5f6c6c64625f66756e633a3a3078313a307835383a5f
5a3763616c6c5f6d6576005f5a3132245f5f6c6c64625f657870725076002e72656c612e6c74657874002e74657874002e636f6d6
d656e74005f5a47565a3132245f5f6c6c64625f657870725076453139245f5f6c6c64625f657870725f726573756c74002e6c6273
73002e6e6f74652e474e552d737461636b00245f5f6c6c64625f6d6f64756c65002e737472746162002e73796d74616200#d9
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff4040#ae
lldb             <  55> read packet: $start:7ffff7ff4000;size:1000;permissions:rx;flags:;#c7
lldb             <  22> send packet: $M7ffff7ff4040,1:00#de
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff30d0#dd
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             <  69> send packet: $M7ffff7ff30d0,18:130000000000000001000000040000000000000000000000#e
e
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff30f0#df
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             <  71> send packet: $M7ffff7ff30f0,19:00636c616e672076657273696f6e2032332e302e3067697400
#34
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff3110#ab
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             <  22> send packet: $M7ffff7ff3110,1:00#db
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff3120#ac
lldb             <  54> read packet: $start:7ffff7ff3000;size:1000;permissions:r;flags:;#4e
lldb             < 261> send packet: $M7ffff7ff3120,78:00000000000000000000000000000000000000000000000094
0000000400f1ff0000000000000000000000000000000052000000010005000000000000000000010000000000000024000000120
0030000000000000000002600000000000000010000001000000000000000000000000000000000000000#92
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff5020#ad
lldb             <  55> read packet: $start:7ffff7ff5000;size:1000;permissions:rw;flags:;#c7
lldb             <  34> send packet: $M7ffff7ff5020,7:00000000000000#23
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff5020#ad
lldb             <  55> read packet: $start:7ffff7ff5000;size:1000;permissions:rw;flags:;#c7
lldb             <  28> send packet: $M7ffff7ff5020,4:00000000#00
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7ffff7ff5010#ac
lldb             <  55> read packet: $start:7ffff7ff5000;size:1000;permissions:rw;flags:;#c7
lldb             <  36> send packet: $M7ffff7ff5010,8:2050fff7ff7f0000#dc
lldb             <   6> read packet: $OK#9a
lldb             <  35> send packet: $QSaveRegisterState;thread:e3f8;#84
lldb             <   5> read packet: $2#32
lldb             <  36> send packet: $P4=1050fff7ff7f0000;thread:e3f8;#77
lldb             <   6> read packet: $OK#9a
lldb             <  34> send packet: $qMemoryRegionInfo:7fffffffd758#1d
lldb             <  76> read packet: $start:7ffffffdd000;size:22000;permissions:rw;flags:;name:5b73746163
6b5d;#d9
lldb             <  36> send packet: $M7fffffffd758,8:c027400000000000#34
lldb             <   6> read packet: $OK#9a
lldb             <  36> send packet: $P7=58d7ffffff7f0000;thread:e3f8;#eb
lldb             <   6> read packet: $OK#9a
lldb             <  37> send packet: $P10=0040fff7ff7f0000;thread:e3f8;#a2
lldb             <   6> read packet: $OK#9a
lldb             <  15> send packet: $Z0,4027c0,1#73     # <====== ENTRY BKPT SET (AGAIN)
lldb             <   6> read packet: $OK#9a
lldb             <  20> send packet: $p10;thread:e3f8;#2f
lldb             <  20> read packet: $0040fff7ff7f0000#56
lldb             <  15> send packet: $z0,4027c0,1#93     # <======= ENTRY BKPT CLEARED
lldb             <   6> read packet: $OK#9a
b-remote.async>  <  20> send packet: $vCont;c:pe3f8.-1#14
b-remote.async>  < 615> read packet: $T05thread:pe3f8.e3f8;name:a.out;threads:e3f8;thread-pcs:00000000004
027c0;00:2f00000000000000;01:08d9ffffff7f0000;02:1101000000000000;03:08d9ffffff7f0000;04:1050fff7ff7f0000
;05:f8d8ffffff7f0000;06:e0d7ffffff7f0000;07:60d7ffffff7f0000;08:0001000000000000;09:0400000000000000;0a:8
0914a0000000000;0b:0f00000000000000;0c:f8d8ffffff7f0000;0d:0200000000000000;0e:604d4a0000000000;0f:020000
0000000000;10:c027400000000000;11:0602000000000000;12:3300000000000000;13:0000000000000000;14:00000000000
00000;15:2b00000000000000;16:c0134b0000000000;17:0000000000000000;18:0000000000000000;19:0000000000000000
;reason:breakpoint;#fa
<snip>
intern-state     <  40> send packet: $QRestoreRegisterState:2;thread:e3f8;#45
intern-state     <   6> read packet: $OK#9a
<snip>
(int) $1 = 47

This is a log of me evaluating two expressions. The problematic part is that during the evaluation of the first expression, we set the entry breakpoint (which is supposed to be hit when the expression gets evaluation) -- but this breakpoint doesn't get cleared. Then, when we evaluate the second expression, we set the breakpoint again -- and then we also clear it (presumably intending to clear the breakpoint from the first run).

Now, on lldb-server and debugserver this does not actually matter -- because they implement the set/clear breakpoint packets using a refcount. This means the breakpoint remains set when evaluating the expression -- as expected.

However, it's not clear to me that this is actually the expected way to implement these packets. The gdb protocol spec doesn't say much on the matter, but if we consider gdbserver to be a "reference implementation" of the spec, then this would point to it not being the case:

gdb) maintenance packet Z0,7ffff7fe3880,1
sending: Z0,7ffff7fe3880,1
[remote] Sending packet: $Z0,7ffff7fe3880,1#b7
[remote] Packet received: OK
received: "OK"
(gdb) maintenance packet Z0,7ffff7fe3880,1
sending: Z0,7ffff7fe3880,1
[remote] Sending packet: $Z0,7ffff7fe3880,1#b7
[remote] Packet received: OK
received: "OK"
(gdb) maintenance packet z0,7ffff7fe3880,1
sending: z0,7ffff7fe3880,1
[remote] Sending packet: $z0,7ffff7fe3880,1#d7
[remote] Packet received: E01
received: "E01"

Now, the reason I noticed this is because we have an (internal) stub, which implements these packets the "gdb way", which means that the entry breakpoint is not set when evaluating the second expression, and the expression ends up starting the binary all over again (which, predictably, blows up).

I'm not sure if this is a case for the windows implementation. Being in-process it doesn't use the same code as lldb-server, but I'm not completely sure in how it interacts with the rest of lldb. However, the failure mode is at least consistent with the behavior I've seen using our stub.

I've also looked at how qemu (a debug stub built for gdb that I have access to) implements this and -- to my surprise -- it does implement the reference counting behavior. I haven't looked at the source code, so I don't know if this is intentional or not (I can imagine this kind of behavior falling out "naturally" in some implementations).

Given all of this, I think we need to make a decision: do expect stub to support reference counted breakpoints or not?

The list of stubs which are currently known to not support this is not particularly compelling:

  • gdbserver (which is not particularly interesting, although people occasionally show up trying to use it)
  • our internal stub (which you don't need to care about, and I can fix relatively easily)
  • lldb windows in-process plugin (maybe)

However, I am worried about all the random debug stub implementations out there, potentially embedded in jtag probes or whatnot. I don't have access to these to verify, but I understand that people are using these with lldb, and these sound like the kind of things that would be hard to modify.

This is why my vote, whatever its worth, would be to fix this on the lldb side, and ideally revert this patch in the mean time. If it helps, I volunteer to rip out the reference counting code from within lldb-server to make this easier to test (and make sure it doesn't happen again).

@labath

labath commented May 4, 2026

Copy link
Copy Markdown
Contributor

Given all of this, I think we need to make a decision: do expect stub to support reference counted breakpoints or not?

This is why my vote, whatever its worth, would be to fix this on the lldb side, and ideally revert this patch in the mean time.

I've created #195652 for that, which we can submit if we come to some sort of a consensus.

Another issue I noticed when playing around with this is that the packet log for expression evaluation contains a lot of disable-enable breakpoint packet pairs:

ldb             <  21> send packet: $z0,7ffff7cb3290,1#cc
lldb             <   6> read packet: $OK#9a
lldb             <  21> send packet: $z0,7ffff7cb32e0,1#f8
lldb             <   6> read packet: $OK#9a
lldb             <  21> send packet: $z0,7ffff7cb1d50,1#f8
lldb             <   6> read packet: $OK#9a
lldb             <  21> send packet: $z0,555555555040,1#a4
lldb             <   6> read packet: $OK#9a
lldb             <  21> send packet: $Z0,7ffff7cb3290,1#ac
lldb             <   6> read packet: $OK#9a
lldb             <  21> send packet: $Z0,7ffff7cb32e0,1#d8
lldb             <   6> read packet: $OK#9a
lldb             <  21> send packet: $Z0,7ffff7cb1d50,1#d8
lldb             <   6> read packet: $OK#9a
b-remote.async>  <  21> send packet: $vCont;c:p11810.-1#d9

These correspond to internal breakpoints (which I guess are somehow re-enabled for every expression):

(lldb) breakpoint list -i
Current breakpoints:
Kind: shared-library-event
-1: names = {'_dl_debug_state', 'rtld_db_dlactivity', '__dl_rtld_db_dlactivity', 'r_debug_state', '_r_deb
ug_state', '_rtld_debug_state'}, language = c, module = ld-linux-x86-64.so.2, locations = 1, resolved = 1
, hit count = 2

  -1.1: where = ld-linux-x86-64.so.2`_dl_debug_state, address = 0x00007ffff7fc87e0, resolved, hit count =
 2 

Kind: c++ exception
-2: names = {'__cxa_throw', '__cxa_rethrow', '__cxa_allocate_exception'}, locations = 3 Options: disabled
 
  -2.1: where = libstdc++.so.6`__cxxabiv1::__cxa_throw(void *, std::type_info *, void (*)(void *)) at eh_
throw.cc:80:1, address = 0x00007ffff7cb3290, unresolved, hit count = 0 
  -2.2: where = libstdc++.so.6`__cxxabiv1::__cxa_rethrow() at eh_throw.cc:103:1, address = 0x00007ffff7cb
32e0, unresolved, hit count = 0 
  -2.3: where = libstdc++.so.6`__cxxabiv1::__cxa_allocate_exception(std::size_t) at eh_alloc.cc:395:1, ad
dress = 0x00007ffff7cb1d50, unresolved, hit count = 0 

This isn't necessarily a (correctness?) issue, but it strikes me as rather odd as the goal of this patch series is to reduce the number of breakpoint packets. And the patch contains code which seems to try to prevent these, so I'm wondering if this is actually working as intended.

@jasonmolenda

jasonmolenda commented May 4, 2026

Copy link
Copy Markdown
Contributor

@labath Felipe still needs to merge (this one final?) PR #192988 to have ProcessGDBRemote start sending MultiBreakpoint's; we are still doing the breakpoint enabling and disabling with separate packets right now. The behavior should be the same as the final goal, though: all breakpoint enabling/disabling is queued, until we go to resume the target, and then lldb sends all of them all together.

@jasonmolenda

Copy link
Copy Markdown
Contributor

Playing around with llvm.org main a little while watching packets, one thing I'm a little surprised by is that some breakpoint-set's are sent eagerly, some are delayed. At first, I saw hardware watchpoints were eager, which makes sense because there's a real possibility of them failing and maybe the developer will decide to use a software breakpoint (debugserver silently uses a software breakpoint, which is probably a bit unhelpful if you really needed hardware watchpoints)

But also breakpoint set -a <addr> will be sent eagerly. breakpoint set -l <lino> is enqueued.

Haven't looked at the sources closely to understand the differences, but I'm not sure I understand how we decide what is sent immediately vrs. enqueued yet. a minor detail, just noting it.

@jasonmolenda

Copy link
Copy Markdown
Contributor
(lldb) br s -l 10   ## a few lines into the main() function
(lldb) r

(lldb) p (int)isdigit('1')
(int) 1

We get a breakpoint on LC_MAIN (0x100000328) that the expression returns to, and a bunch of internal breakpoints are enabled

1777935527.466006994 <  18> send packet: $Z0,100000328,4#d4
1777935527.466072083 <   6> read packet: $OK#00
1777935527.466118097 <  23> send packet: $p20;thread:114e7e0;#c1
1777935527.466156960 <  20> read packet: $0080010001000000#00
1777935527.466211081 <  18> send packet: $Z0,18cdf6040,4#76   // __cxa_throw
1777935527.466286898 <   6> read packet: $OK#00
1777935527.466306925 <  18> send packet: $Z0,18cdf6dc4,4#dd  // __cxa_rethrow
1777935527.466382027 <   6> read packet: $OK#00
1777935527.466403961 <  18> send packet: $Z0,18cdf5f90,4#b0  // __cxa_allocate_exception
1777935527.466456890 <   6> read packet: $OK#00
1777935527.466485977 <  18> send packet: $Z0,18ca0a8c4,4#a3  // objc_exception_throw
1777935527.466559887 <   6> read packet: $OK#00
1777935527.466573954 <  18> send packet: $Z0,18ce4ec08,4#ab  // start_wqthread
1777935527.466636896 <   6> read packet: $OK#00
1777935527.466658115 <  18> send packet: $Z0,18ce4fd9c,4#e1  // _pthread_wqthread
1777935527.466711998 <   6> read packet: $OK#00
1777935527.466728926 <  18> send packet: $Z0,18ce53bd0,4#a5 // _pthread_start
1777935527.466780901 <   6> read packet: $OK#00
1777935527.466823101 <   5> send packet: $c#63
1777935527.468118906 < 872> read packet: $T05thread:114e7e0;threads:114e7e0;thread-pcs:100000328;[...]

If I do the expression again, I'll see all of these disabled and re-enabled

(lldb) p (int)isdigit('1')


1777935708.738998890 <  18> send packet: $Z0,100000328,4#d4         << a new LC_MAIN breakpoint
1777935708.739018917 <   6> read packet: $OK#00
1777935708.739065886 <  23> send packet: $p20;thread:114e7e0;#c1
1777935708.739106894 <  20> read packet: $0080010001000000#00
1777935708.739147902 <  18> send packet: $z0,18cdf6040,4#96
1777935708.739211082 <   6> read packet: $OK#00
1777935708.739228964 <  18> send packet: $z0,18cdf6dc4,4#fd
1777935708.739293098 <   6> read packet: $OK#00
1777935708.739319086 <  18> send packet: $z0,18cdf5f90,4#d0
1777935708.739386082 <   6> read packet: $OK#00
1777935708.739403009 <  18> send packet: $z0,18ca0a8c4,4#c3
1777935708.739480019 <   6> read packet: $OK#00
1777935708.739516973 <  18> send packet: $z0,100000328,4#f4       << The previous LC_MAIN breakpoint
1777935708.739542961 <   6> read packet: $OK#00
1777935708.739561081 <  18> send packet: $Z0,18cdf6040,4#76
1777935708.739625931 <   6> read packet: $OK#00
1777935708.739644051 <  18> send packet: $Z0,18cdf6dc4,4#dd
1777935708.739712000 <   6> read packet: $OK#00
1777935708.739732981 <  18> send packet: $Z0,18cdf5f90,4#b0
1777935708.739784956 <   6> read packet: $OK#00
1777935708.739809990 <  18> send packet: $Z0,18ca0a8c4,4#a3
1777935708.739881039 <   6> read packet: $OK#00
1777935708.739922047 <   5> send packet: $c#63
1777935708.741267920 < 872> read packet: $T05thread:114e7e0;threads:114e7e0;thread-pcs:100000328;00

I think what's happening is that the expression return breakpoint is set immediately, not enqueued. Notice how we send the Z0,100000328,4 and then read $pc (p20) for some reason -- we're in generic lldb logic here, not the "send down all enqueued breakpoints" loop. So we have an ordering problem -- the expr-return breakpoint is immediately inserted, but the disabling is enqueued. So on the second expression evaluation we have:

  1. Immediately insert expression-return-address breakpoint
  2. Send all enqueued disable-breakpoint/enable-breakpoint requests (including disabling the previous expression's return-address breakpoint)
  3. continue execution

If breakpoints are refcounted by the stub, this will work. But if it doesn't, we've deleted the expression-return breakpoint.

sorry this is probably exactly the same thing Pavel said, I was just figuring it out slowly for myself.

@jasonmolenda

Copy link
Copy Markdown
Contributor

If the expression-return breakpoint setting was enqueued instead of immediate (could this be the same reason that breakpoint set -a is immediately sent?), or if the clearing of that breakpoint was immediate instead of enqueued, we would be fine. But the mismatch between immediately-set and delayed-unset leads to this problem I think.

@jasonmolenda

jasonmolenda commented May 4, 2026

Copy link
Copy Markdown
Contributor

Just to try it out, I hacked debugserver to not refcount its breakpoints

diff --git a/lldb/tools/debugserver/source/DNBBreakpoint.h b/lldb/tools/debugserver/source/DNBBreakpoint.h
index b7c407905d0..8fbe69d2c70 100644
--- a/lldb/tools/debugserver/source/DNBBreakpoint.h
+++ b/lldb/tools/debugserver/source/DNBBreakpoint.h
@@ -91,11 +91,7 @@ public:
   void SetHardwareIndex(uint32_t hw_index) { m_hw_index = hw_index; }
   void Dump() const;
   uint32_t Retain() { return ++m_retain_count; }
-  uint32_t Release() {
-    if (m_retain_count == 0)
-      return 0;
-    return --m_retain_count;
-  }
+  uint32_t Release() { return 0; }
 
 private:
   uint32_t m_retain_count; // Each breakpoint is maintained by address and is

and the second expression evaluation in a row will fail.

@jasonmolenda

Copy link
Copy Markdown
Contributor

Yeah I re-read through Pavel's packet log, he's facing the same issue as the one I'm talking about.

@jasonmolenda

Copy link
Copy Markdown
Contributor

I saw hardware watchpoints were eager, [...] But also breakpoint set -a will be sent eagerly.

lol

   1761	  bool bp_from_address =
   1762	      constituent->GetBreakpoint().GetResolver()->GetResolverTy() ==
   1763	      BreakpointResolver::ResolverTy::AddressResolver;
   1764	  bool should_be_eager = use_hardware || bp_from_address;

I'm guessing the reason address breakpoints (which includes the expression-return breakpoint) are eager is because a user might put a breakpoint in unmapped memory by accident, and we'd like to show them an error message right away so they can correct the address. The removal of these cannot fail, so they're done non-eagerly.

I don't know if we should have some way of distinguishing between "a user specified address breakpoint" and something lldb has internally calculated so it can be delayed. Or if we want to check the list of enqueued breakpoint actions and see if any have the same address and making it non-eager, assuming that it is a valid address because we're already doing breakpoint things on it?

@jimingham

Copy link
Copy Markdown
Contributor

Jason's right about why we decided to make address breakpoints eager. I also want this because ThreadPlans tend to set address breakpoints in their ShouldStop, and if the breakpoint didn't work they would want to know in ShouldStop so they could make other plans.

For the most part, if a user wants lldb to calculate an address that you intend to use in a breakpoint you get the resolver to do it for you, and that wouldn't be a AddressResolver. Those are really only used when you have computed an Address by hand and want to break on that Address.

@jasonmolenda

jasonmolenda commented May 5, 2026

Copy link
Copy Markdown
Contributor

I think the tl;dr on this is that mixing eager breakpoints and delayed breakpoints can lead to chronological mistakes and bugs like these. I wonder if our solutions might look, generally, like

  1. If we have an eager breakpoint, and there are any enqueued delayed breakpoints, send the delayed breakpoints before we process the eager breakpoint.
  2. If we have an eager breakpoint, look at the addresses of the enqueued delayed breakpoints, and if they are a match, send the delayed breakpoints before we process the eager breakpoint.
  3. If we have an eager breakpoint, look if any enqueued delayed breakpoint is at the same address. If there is one, then we can assume that this breakpoint will succeed, we're already doing breakpoint-things there, and this can become a non-eager delayed breakpoint, and added to the end of the queue.
  4. Try to distinguish between a user-initiated by-address breakpoint (which may be at the wrong address, and fail) versus an lldb-initiated by-address breakpoint (which lldb is awesome so surely it is right and can be delayed, even if Jim isn't happy about it).

Right now when we have an eager breakpoint, we are sending two breakpoint requests: The eager-breakpoint right now, and then when we start execution, any delayed breakpoints in a multi-breakpoint packet. If we do (1), we're still sending two breakpoint requests, it's not any actual performance loss. I guess someone does an eager breakpoint (runs an expression) which flushes the delayed breakpoints - 2 packets. Then they do a step which sets a new breakpoint and continues - so when we resume we have to send a third breakpoint packet overall for this series. That might just be the cost of this, and being conservative.

Maybe that's the right approach (1), because it's the simplest and doesn't try to be fancier. We simply don't allow delayed breakpoints to exist when doing an eager breakpoint. And if we find we are losing performance in common cases by this approach, we might need to revisit it?

@labath

labath commented May 5, 2026

Copy link
Copy Markdown
Contributor

Thanks for checking this out, Jason.

(1) makes most sense to me as well. I'll just add that, if you want to reduce roundtrips, in principle it should possible to include the eager breakpoint request into the multi-packet flushing the delayed requests. The response includes individual error messages for each sub-request, so it should be possible to see whether the eager request failed or not.

@labath Felipe still needs to merge (this one final?) PR #192988 to have ProcessGDBRemote start sending MultiBreakpoint's; we are still doing the breakpoint enabling and disabling with separate packets right now. The behavior should be the same as the final goal, though: all breakpoint enabling/disabling is queued, until we go to resume the target, and then lldb sends all of them all together.

I understand that multi-packet is not enabled yet, but my point is that you don't need the multi packet to optimize this sort of sequence. The code can already see it is going to disable and reenable a breakpoint at the same address, so it could just cancel those two out locally. In fact, the code already tries to do that (Process::ExecuteBreakpointSiteAction), but it somehow doesn't work here. The matching logic is site-based, so I expect this will not catch the cases where a breakpoint deleted and a new one gets created in the same place, but I haven't checked if this is what's happening here or if there's some other issue in play.

This isn't a regression, since those packets would get sent even before this series, but I think you could call it a missed opportunity. Seeing the two packets back-to-back is sort of ridiculous and I can't help but wonder how much speedup you could get by eliminating these -- even without the new packet. Even if adding the packet is still worthwhile, this sort of thing could help with older stubs, which I believe is something that you care about.

I definitely haven't thought through all the implications, but an obvious idea is to change to logic to cluster breakpoint requests based on breakpoint address (as that is what ultimately gets sent), rather than the BreakpointSite pointer.

@jasonmolenda

Copy link
Copy Markdown
Contributor

I'll just add that, if you want to reduce roundtrips, in principle it should possible to include the eager breakpoint request into the multi-packet flushing the delayed requests.

Oh, of course, good point. I think we might need a short-term fix for this right away, and sending the extra packet right now to keep it simple might be good, but that's a better idea. Let's see what Felipe thinks.

@felipepiovezan

felipepiovezan commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for investigating this, everyone. Let me try to address some of the points mentioned here.

First, the efficiency point, which is not related to the main bug discussed.

The matching logic is site-based, so I expect this will not catch the cases where a breakpoint deleted and a new one gets created in the same place

This one is intentional because of a detail about how the BreakpointSite class is designed.

If we disable a BreakpointSite which had exactly one BreakpointLocation associated with it, the BreakpointSite is removed from the site list of the Process. This has always been the case. Now, if we re-enable a breakpoint on the same address, a new site will be created, because the code will look for a BreakpointSite in the site list with a matching address and find none. This has also always been the case.

The interaction with this patch is that now there will be two breakpoint operations queued for the same address (the disable, and then the enable). This is not problematic, but:

  1. It's not efficient, as Pavel points out.
  2. It makes it incorrect for us to "retry" operations. The follow-up PR had a fallback to retry individual components of MultiBreakpoint using "single" packets in case any of them failed. I am going to rip that code out, as it is both incorrect and we have to trust the stub to implement packets correctly.

You may point out that we could, in theory, search both the Process site list and the delayed breakpoint queue for a BreakpointSite with a matching address. If there was a BreakpointSite there already enqueued for disabling, simply move it back to the Process site list. This works, except for this case:

  1. BreakpointSite A is enabled
  2. Remove the BreakpointLocation associated with A.
  3. Create a BreakpointLocation at the same address, but make it a hardware breakpoint.

In this scenario, it would simply be incorrect to re-use the site and do nothing. A new site must be created. The first one must be disabled first.

Also worth pointing out that I believe LLDB, before this patch, doesn't handle well the case there you have multiple (non-hardware) breakpoints on the same address, and you try to create a new hardware breakpoint on the same address.

@felipepiovezan

Copy link
Copy Markdown
Contributor Author

If we have an eager breakpoint, and there are any enqueued delayed breakpoints, send the delayed breakpoints before we process the eager breakpoint.

Yeah, to me this makes the most sense as well. The whole eager business was a late addition, so it slipped through the cracks, but it is clearly wrong to not flush the cache while doing the eager breakpoint.

@felipepiovezan

Copy link
Copy Markdown
Contributor Author

I'll create a PR now.

@felipepiovezan

Copy link
Copy Markdown
Contributor Author

@labath can you test this? #195815

@labath

labath commented May 5, 2026

Copy link
Copy Markdown
Contributor

In this scenario, it would simply be incorrect to re-use the site and do nothing. A new site must be created. The first one must be disabled first.

That makes sense, but I don't think it prevents us from coalescing breakpoint requests of the same kind (I didn't mention that, but when I said that breakpoints could be clustered "based on breakpoint address", I meant that this should only apply within a breakpoint class. So like, if there's a z0,addr followed by a Z0,addr, we can just cancel them out (maybe?), but if there's a z0, addr followed by a Z1,addr, then we just send both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants