Skip to content

Conversation

@daniilavdeev
Copy link
Contributor

This patch is the first in a series of patches that add software watchpoint support to LLDB.

The main purpose of this patch is to refactor watchpoint functionality in LLDB, making it easier to implement software watchpoint logic.

@llvmbot
Copy link
Member

llvmbot commented Sep 19, 2025

@llvm/pr-subscribers-lldb

Author: None (dlav-sc)

Changes

This patch is the first in a series of patches that add software watchpoint support to LLDB.

The main purpose of this patch is to refactor watchpoint functionality in LLDB, making it easier to implement software watchpoint logic.


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

15 Files Affected:

  • (modified) lldb/include/lldb/Breakpoint/Watchpoint.h (+132-26)
  • (modified) lldb/include/lldb/Interpreter/OptionGroupWatchpoint.h (+1)
  • (modified) lldb/include/lldb/Target/Target.h (+5-3)
  • (modified) lldb/include/lldb/lldb-enumerations.h (+2)
  • (modified) lldb/source/API/SBTarget.cpp (+2-2)
  • (modified) lldb/source/API/SBValue.cpp (+2-2)
  • (modified) lldb/source/Breakpoint/Watchpoint.cpp (+208-106)
  • (modified) lldb/source/Commands/CommandObjectWatchpoint.cpp (+32-20)
  • (modified) lldb/source/Interpreter/OptionGroupWatchpoint.cpp (+10-2)
  • (modified) lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp (+34-23)
  • (modified) lldb/source/Target/StopInfo.cpp (+81-176)
  • (modified) lldb/source/Target/Target.cpp (+159-75)
  • (modified) lldb/source/Target/Thread.cpp (+64-55)
  • (modified) lldb/test/API/commands/watchpoints/hello_watchlocation/TestWatchLocation.py (+1-1)
  • (modified) lldb/test/API/commands/watchpoints/watchpoint_commands/TestWatchpointCommands.py (+1-1)
diff --git a/lldb/include/lldb/Breakpoint/Watchpoint.h b/lldb/include/lldb/Breakpoint/Watchpoint.h
index ca48a0114888a..62435fe7dcce2 100644
--- a/lldb/include/lldb/Breakpoint/Watchpoint.h
+++ b/lldb/include/lldb/Breakpoint/Watchpoint.h
@@ -15,6 +15,7 @@
 #include "lldb/Breakpoint/StoppointSite.h"
 #include "lldb/Breakpoint/WatchpointOptions.h"
 #include "lldb/Symbol/CompilerType.h"
+#include "lldb/Target/Process.h"
 #include "lldb/Target/Target.h"
 #include "lldb/Utility/UserID.h"
 #include "lldb/lldb-private.h"
@@ -58,37 +59,92 @@ class Watchpoint : public std::enable_shared_from_this<Watchpoint>,
     const WatchpointEventData &operator=(const WatchpointEventData &) = delete;
   };
 
+  // Make sure watchpoint is properly disabled and subsequently enabled while
+  // performing watchpoint actions.
+  class WatchpointSentry {
+  public:
+    WatchpointSentry(lldb::ProcessSP p_sp, lldb::WatchpointSP w_sp)
+        : process_sp(p_sp), watchpoint_sp(w_sp) {
+      lldbassert(process_sp && watchpoint_sp && "Ill-formed WatchpointSentry!");
+
+      constexpr bool notify = false;
+      watchpoint_sp->TurnOnEphemeralMode();
+      process_sp->DisableWatchpoint(watchpoint_sp, notify);
+      process_sp->AddPreResumeAction(SentryPreResumeAction, this);
+    }
+
+    void DoReenable() {
+      bool was_disabled = watchpoint_sp->IsDisabledDuringEphemeralMode();
+      watchpoint_sp->TurnOffEphemeralMode();
+      constexpr bool notify = false;
+      if (was_disabled) {
+        process_sp->DisableWatchpoint(watchpoint_sp, notify);
+      } else {
+        process_sp->EnableWatchpoint(watchpoint_sp, notify);
+      }
+    }
+
+    ~WatchpointSentry() {
+      DoReenable();
+      process_sp->ClearPreResumeAction(SentryPreResumeAction, this);
+    }
+
+    static bool SentryPreResumeAction(void *sentry_void) {
+      WatchpointSentry *sentry = static_cast<WatchpointSentry *>(sentry_void);
+      sentry->DoReenable();
+      return true;
+    }
+
+  private:
+    lldb::ProcessSP process_sp;
+    lldb::WatchpointSP watchpoint_sp;
+  };
+
   Watchpoint(Target &target, lldb::addr_t addr, uint32_t size,
-             const CompilerType *type, bool hardware = true);
+             const CompilerType *type, lldb::WatchpointMode mode);
 
   ~Watchpoint() override;
 
   bool IsEnabled() const;
 
-  // This doesn't really enable/disable the watchpoint.   It is currently just
-  // for use in the Process plugin's {Enable,Disable}Watchpoint, which should
-  // be used instead.
+  // This doesn't really enable/disable the watchpoint.   It is currently
+  // just for use in the Process plugin's {Enable,Disable}Watchpoint, which
+  // should be used instead.
   void SetEnabled(bool enabled, bool notify = true);
 
-  bool IsHardware() const override;
+  bool IsHardware() const override {
+    return m_mode == lldb::eWatchpointModeHardware;
+  }
 
   bool ShouldStop(StoppointCallbackContext *context) override;
 
-  bool WatchpointRead() const;
-  bool WatchpointWrite() const;
-  bool WatchpointModify() const;
+  bool WatchpointRead() const { return m_watch_type & LLDB_WATCH_TYPE_READ; }
+  bool WatchpointWrite() const { return m_watch_type & LLDB_WATCH_TYPE_WRITE; }
+  bool WatchpointModify() const {
+    return m_watch_type & LLDB_WATCH_TYPE_MODIFY;
+  }
+
   uint32_t GetIgnoreCount() const;
   void SetIgnoreCount(uint32_t n);
   void SetWatchpointType(uint32_t type, bool notify = true);
   void SetDeclInfo(const std::string &str);
-  std::string GetWatchSpec();
+  std::string GetWatchSpec() const;
   void SetWatchSpec(const std::string &str);
+
+  // This function determines whether we should report a watchpoint value
+  // change. Specifically, it checks the watchpoint condition (if present),
+  // ignore count and so on.
+  //
+  // \param[in] exe_ctx This should represent the current execution context
+  // where execution stopped. It's used only for watchpoint condition
+  // evaluation.
+  //
+  // \return Returns true if we should report a watchpoint hit.
   bool WatchedValueReportable(const ExecutionContext &exe_ctx);
 
   // Snapshot management interface.
   bool IsWatchVariable() const;
   void SetWatchVariable(bool val);
-  bool CaptureWatchedValue(const ExecutionContext &exe_ctx);
 
   /// \struct WatchpointVariableContext
   /// \brief Represents the context of a watchpoint variable.
@@ -124,7 +180,7 @@ class Watchpoint : public std::enable_shared_from_this<Watchpoint>,
       void *baton, lldb_private::StoppointCallbackContext *context,
       lldb::user_id_t break_id, lldb::user_id_t break_loc_id);
 
-  void GetDescription(Stream *s, lldb::DescriptionLevel level);
+  void GetDescription(Stream *s, lldb::DescriptionLevel level) const;
   void Dump(Stream *s) const override;
   bool DumpSnapshots(Stream *s, const char *prefix = nullptr) const;
   void DumpWithLevel(Stream *s, lldb::DescriptionLevel description_level) const;
@@ -186,26 +242,72 @@ class Watchpoint : public std::enable_shared_from_this<Watchpoint>,
 
   bool IsDisabledDuringEphemeralMode();
 
-  const CompilerType &GetCompilerType() { return m_type; }
+  CompilerType GetCompilerType() const;
 
 private:
   friend class Target;
   friend class WatchpointList;
-  friend class StopInfoWatchpoint; // This needs to call UndoHitCount()
+
+  lldb::ValueObjectSP CalculateWatchedValue() const;
+
+  void CaptureWatchedValue(lldb::ValueObjectSP new_value_sp) {
+    m_old_value_sp = m_new_value_sp;
+    m_new_value_sp = new_value_sp;
+  }
+
+  bool CheckWatchpointCondition(const ExecutionContext &exe_ctx) const;
+
+  // This class facilitates retrieving a watchpoint's watched value.
+  //
+  // It's used by both hardware and software watchpoints to access
+  // values stored in the process memory.
+  //
+  // To retrieve the value located in the memory, the value's memory address
+  // and its CompilerType are required. ExecutionContext in this case should
+  // contain information about current process, so CalculateWatchedValue
+  // function first of all create ExecutionContext from the process of m_target.
+  class AddressWatchpointCalculateStrategy {
+  public:
+    AddressWatchpointCalculateStrategy(Target &target, lldb::addr_t addr,
+                                       uint32_t size, const CompilerType *type)
+        : m_target{target}, m_addr{addr},
+          m_type{CreateCompilerType(target, size, type)} {}
+
+    lldb::ValueObjectSP CalculateWatchedValue() const;
+
+    CompilerType GetCompilerType() const { return m_type; };
+
+  private:
+    static CompilerType CreateCompilerType(Target &target, uint32_t size,
+                                           const CompilerType *type) {
+      if (type && type->IsValid())
+        return *type;
+      // If we don't have a known type, then we force it to unsigned int of the
+      // right size.
+      return DeriveCompilerType(target, size);
+    }
+
+    static CompilerType DeriveCompilerType(Target &target, uint32_t size);
+
+    Target &m_target;
+    lldb::addr_t m_addr;
+    CompilerType m_type;
+  };
+
+  using WatchpointCalculateStrategy =
+      std::variant<AddressWatchpointCalculateStrategy>;
 
   void ResetHistoricValues() {
     m_old_value_sp.reset();
     m_new_value_sp.reset();
   }
 
-  void UndoHitCount() { m_hit_counter.Decrement(); }
-
   Target &m_target;
-  bool m_enabled;           // Is this watchpoint enabled
-  bool m_is_hardware;       // Is this a hardware watchpoint
-  bool m_is_watch_variable; // True if set via 'watchpoint set variable'.
-  bool m_is_ephemeral;      // True if the watchpoint is in the ephemeral mode,
-                            // meaning that it is
+  bool m_enabled;              // Is this watchpoint enabled
+  lldb::WatchpointMode m_mode; // Is this hardware or software watchpoint
+  bool m_is_watch_variable;    // True if set via 'watchpoint set variable'.
+  bool m_is_ephemeral; // True if the watchpoint is in the ephemeral mode,
+                       // meaning that it is
   // undergoing a pair of temporary disable/enable actions to avoid recursively
   // triggering further watchpoint events.
   uint32_t m_disabled_count; // Keep track of the count that the watchpoint is
@@ -213,15 +315,19 @@ class Watchpoint : public std::enable_shared_from_this<Watchpoint>,
   // At the end of the ephemeral mode when the watchpoint is to be enabled
   // again, we check the count, if it is more than 1, it means the user-
   // supplied actions actually want the watchpoint to be disabled!
-  uint32_t m_watch_read : 1, // 1 if we stop when the watched data is read from
-      m_watch_write : 1,     // 1 if we stop when the watched data is written to
-      m_watch_modify : 1;    // 1 if we stop when the watched data is changed
+  uint32_t m_watch_type;
   uint32_t m_ignore_count;      // Number of times to ignore this watchpoint
-  std::string m_decl_str;       // Declaration information, if any.
-  std::string m_watch_spec_str; // Spec for the watchpoint.
+  std::string m_watch_spec_str; // Spec for the watchpoint. It is optional for a
+                                // hardware watchpoint, in which it is used only
+                                // for dumping, but required for a software
+                                // watchpoint calculation
+  WatchpointCalculateStrategy m_calculate_strategy;
+  std::string m_decl_str; // Declaration information, if any.
   lldb::ValueObjectSP m_old_value_sp;
   lldb::ValueObjectSP m_new_value_sp;
-  CompilerType m_type;
+  lldb::ValueObjectSP
+      m_previous_hit_value_sp; // Used in software watchpoints to ensure proper
+                               // ignore count behavior
   Status m_error; // An error object describing errors associated with this
                   // watchpoint.
   WatchpointOptions m_options; // Settable watchpoint options, which is a
diff --git a/lldb/include/lldb/Interpreter/OptionGroupWatchpoint.h b/lldb/include/lldb/Interpreter/OptionGroupWatchpoint.h
index 527a2612b189b..67d5f5661d828 100644
--- a/lldb/include/lldb/Interpreter/OptionGroupWatchpoint.h
+++ b/lldb/include/lldb/Interpreter/OptionGroupWatchpoint.h
@@ -44,6 +44,7 @@ class OptionGroupWatchpoint : public OptionGroup {
   WatchType watch_type;
   OptionValueUInt64 watch_size;
   bool watch_type_specified;
+  lldb::WatchpointMode watch_mode;
   lldb::LanguageType language_type;
 
 private:
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 14a09f29094d5..bce2f178d4799 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -796,9 +796,11 @@ class Target : public std::enable_shared_from_this<Target>,
                                       bool resolve_indirect_symbols);
 
   // Use this to create a watchpoint:
-  lldb::WatchpointSP CreateWatchpoint(lldb::addr_t addr, size_t size,
-                                      const CompilerType *type, uint32_t kind,
-                                      Status &error);
+  lldb::WatchpointSP CreateWatchpointByAddress(lldb::addr_t addr, size_t size,
+                                               const CompilerType *type,
+                                               uint32_t kind,
+                                               lldb::WatchpointMode mode,
+                                               Status &error);
 
   lldb::WatchpointSP GetLastCreatedWatchpoint() {
     return m_last_created_watchpoint;
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index fec9fdef44df9..f4604f9ac74b3 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -1081,6 +1081,8 @@ enum InstructionControlFlowKind {
 FLAGS_ENUM(WatchpointKind){eWatchpointKindWrite = (1u << 0),
                            eWatchpointKindRead = (1u << 1)};
 
+enum WatchpointMode { eWatchpointModeHardware, eWatchpointModeSoftware };
+
 enum GdbSignal {
   eGdbSignalBadAccess = 0x91,
   eGdbSignalBadInstruction = 0x92,
diff --git a/lldb/source/API/SBTarget.cpp b/lldb/source/API/SBTarget.cpp
index eb56337de3c44..4ea6f25906876 100644
--- a/lldb/source/API/SBTarget.cpp
+++ b/lldb/source/API/SBTarget.cpp
@@ -1349,8 +1349,8 @@ SBTarget::WatchpointCreateByAddress(lldb::addr_t addr, size_t size,
     Status cw_error;
     // This API doesn't take in a type, so we can't figure out what it is.
     CompilerType *type = nullptr;
-    watchpoint_sp =
-        target_sp->CreateWatchpoint(addr, size, type, watch_type, cw_error);
+    watchpoint_sp = target_sp->CreateWatchpointByAddress(
+        addr, size, type, watch_type, lldb::eWatchpointModeHardware, cw_error);
     error.SetError(std::move(cw_error));
     sb_watchpoint.SetSP(watchpoint_sp);
   }
diff --git a/lldb/source/API/SBValue.cpp b/lldb/source/API/SBValue.cpp
index e300ecee3f8ac..1670883062173 100644
--- a/lldb/source/API/SBValue.cpp
+++ b/lldb/source/API/SBValue.cpp
@@ -1491,8 +1491,8 @@ lldb::SBWatchpoint SBValue::Watch(bool resolve_location, bool read, bool write,
 
     Status rc;
     CompilerType type(value_sp->GetCompilerType());
-    WatchpointSP watchpoint_sp =
-        target_sp->CreateWatchpoint(addr, byte_size, &type, watch_type, rc);
+    WatchpointSP watchpoint_sp = target_sp->CreateWatchpointByAddress(
+        addr, byte_size, &type, watch_type, lldb::eWatchpointModeHardware, rc);
     error.SetError(std::move(rc));
 
     if (watchpoint_sp) {
diff --git a/lldb/source/Breakpoint/Watchpoint.cpp b/lldb/source/Breakpoint/Watchpoint.cpp
index f1366ca538075..dad3155ff1348 100644
--- a/lldb/source/Breakpoint/Watchpoint.cpp
+++ b/lldb/source/Breakpoint/Watchpoint.cpp
@@ -10,6 +10,7 @@
 
 #include "lldb/Breakpoint/StoppointCallbackContext.h"
 #include "lldb/Breakpoint/WatchpointResource.h"
+#include "lldb/Core/Debugger.h"
 #include "lldb/Core/Value.h"
 #include "lldb/DataFormatters/DumpValueObjectOptions.h"
 #include "lldb/Expression/UserExpression.h"
@@ -26,45 +27,95 @@
 using namespace lldb;
 using namespace lldb_private;
 
-Watchpoint::Watchpoint(Target &target, lldb::addr_t addr, uint32_t size,
-                       const CompilerType *type, bool hardware)
-    : StoppointSite(0, addr, size, hardware), m_target(target),
-      m_enabled(false), m_is_hardware(hardware), m_is_watch_variable(false),
-      m_is_ephemeral(false), m_disabled_count(0), m_watch_read(0),
-      m_watch_write(0), m_watch_modify(0), m_ignore_count(0) {
-
-  if (type && type->IsValid())
-    m_type = *type;
-  else {
-    // If we don't have a known type, then we force it to unsigned int of the
-    // right size.
-    auto type_system_or_err =
-        target.GetScratchTypeSystemForLanguage(eLanguageTypeC);
-    if (auto err = type_system_or_err.takeError()) {
-      LLDB_LOG_ERROR(GetLog(LLDBLog::Watchpoints), std::move(err),
-                     "Failed to set type: {0}");
-    } else {
-      if (auto ts = *type_system_or_err) {
-        if (size <= target.GetArchitecture().GetAddressByteSize()) {
-          m_type =
-              ts->GetBuiltinTypeForEncodingAndBitSize(eEncodingUint, 8 * size);
-        } else {
-          CompilerType clang_uint8_type =
-              ts->GetBuiltinTypeForEncodingAndBitSize(eEncodingUint, 8);
-          m_type = clang_uint8_type.GetArrayType(size);
-        }
-      } else
-        LLDB_LOG_ERROR(GetLog(LLDBLog::Watchpoints), std::move(err),
-                       "Failed to set type: Typesystem is no longer live: {0}");
-    }
+static bool IsValueObjectsEqual(lldb::ValueObjectSP lhs,
+                                lldb::ValueObjectSP rhs) {
+  lldbassert(lhs);
+  lldbassert(rhs);
+
+  Status error;
+
+  DataExtractor lhs_data;
+  lhs->GetData(lhs_data, error);
+  if (error.Fail())
+    return false;
+
+  DataExtractor rhs_data;
+  rhs->GetData(rhs_data, error);
+  if (error.Fail())
+    return false;
+
+  return llvm::equal(
+      llvm::iterator_range(lhs_data.GetDataStart(), lhs_data.GetDataEnd()),
+      llvm::iterator_range(rhs_data.GetDataStart(), rhs_data.GetDataEnd()));
+}
+
+CompilerType Watchpoint::AddressWatchpointCalculateStrategy::DeriveCompilerType(
+    Target &target, uint32_t size) {
+  auto type_system_or_err =
+      target.GetScratchTypeSystemForLanguage(eLanguageTypeC);
+
+  auto err = type_system_or_err.takeError();
+  if (err) {
+    LLDB_LOG_ERROR(GetLog(LLDBLog::Watchpoints), std::move(err),
+                   "Failed to set type: {0}");
+    return CompilerType();
   }
 
-  // Set the initial value of the watched variable:
-  if (m_target.GetProcessSP()) {
-    ExecutionContext exe_ctx;
-    m_target.GetProcessSP()->CalculateExecutionContext(exe_ctx);
-    CaptureWatchedValue(exe_ctx);
+  auto ts = *type_system_or_err;
+  if (!ts) {
+    LLDB_LOG_ERROR(GetLog(LLDBLog::Watchpoints), std::move(err),
+                   "Failed to set type: Typesystem is no longer live: {0}");
+    return CompilerType();
+  }
+
+  if (size <= target.GetArchitecture().GetAddressByteSize())
+    return ts->GetBuiltinTypeForEncodingAndBitSize(eEncodingUint, 8 * size);
+
+  CompilerType clang_uint8_type =
+      ts->GetBuiltinTypeForEncodingAndBitSize(eEncodingUint, 8);
+  return clang_uint8_type.GetArrayType(size);
+}
+
+lldb::ValueObjectSP
+Watchpoint::AddressWatchpointCalculateStrategy::CalculateWatchedValue() const {
+  if (!m_type.IsValid()) {
+    // Don't know how to report new & old values, since we couldn't make a
+    // scalar type for this watchpoint. This works around an assert in
+    // ValueObjectMemory::Create.
+    // FIXME: This should not happen, but if it does in some case we care about,
+    // we can go grab the value raw and print it as unsigned.
+    return nullptr;
   }
+
+  // To obtain a value that represents memory at a given address, we only need
+  // an information about process.
+  // Create here an ExecutionContext from the current process.
+  ExecutionContext exe_ctx;
+  lldb::ProcessSP process_sp = m_target.GetProcessSP();
+  if (!process_sp)
+    return nullptr;
+  process_sp->CalculateExecutionContext(exe_ctx);
+
+  ConstString g_watch_name("$__lldb__watch_value");
+  Address watch_address(m_addr);
+  auto new_value_sp = ValueObjectMemory::Create(
+      exe_ctx.GetBestExecutionContextScope(), g_watch_name.GetStringRef(),
+      watch_address, m_type);
+  new_value_sp = new_value_sp->CreateConstantValue(g_watch_name);
+  return new_value_sp;
+}
+
+Watchpoint::Watchpoint(Target &target, lldb::addr_t addr, uint32_t size,
+                       const CompilerType *type, lldb::WatchpointMode mode)
+    : StoppointSite(0, addr, size,
+                    mode == lldb::eWatchpointModeHardware ? true : false),
+      m_target(target), m_enabled(false), m_mode(mode),
+      m_is_watch_variable(false), m_is_ephemeral(false), m_disabled_count(0),
+      m_ignore_count(0), m_watch_spec_str{},
+      m_calculate_strategy{
+          AddressWatchpointCalculateStrategy{target, addr, size, type}} {
+  // Set the initial value of the watched variable:
+  CaptureWatchedValue(CalculateWatchedValue());
 }
 
 Watchpoint::~Watchpoint() = default;
@@ -184,72 +235,121 @@ void Watchpoint::ClearCallback() {
 
 void Watchpoint::SetDeclInfo(const std::string &str) { m_decl_str = str; }
 
-std::string Watchpoint::GetWatchSpec() { return m_watch_spec_str; }
+std::string Watchpoint::GetWatchSpec() const { return m_watch_spec_str; }
 
 void Watchpoint::SetWatchSpec(const std::string &str) {
   m_watch_spec_str = str;
 }
 
-bool Watchpoint::IsHardware() const {
-  lldbassert(m_is_hardware || !HardwareRequired());
-  return m_is_hardware;
-}
-
 bool Watchpoint::IsWatchVariable() const { return m_is_watch_variable; }
 
 void Watchpoint::SetWatchVariable(bool val) { m_is_watch_variable = val; }
 
-bool Watchpoint::CaptureWatchedValue(const ExecutionContext &exe_ctx) {
-  ConstString g_watch_name("$__lldb__watch_value");
-  m_old_value_sp = m_new_value_sp;
-  Address watch_address(GetLoadAddress());
-  if (!m_type.IsValid()) {
-    // Don't know how to report new & old values, since we couldn't make a
-    // scalar type for this watchpoint. This works around an assert in
-    // ValueObjectMemory::Create.
-    // FIXME: This should not happen, but if it does in some case we care about,
-    // we can go grab the value raw and print it as unsigned.
-    return false;
-  }
-  m_new_value_sp = ValueObjectMemory::Create(
-      exe_ctx.GetBestExecutionContextScope(), g_watch_name.GetStringRef(),
-      watch_address, m_type);
-  m_new_value_sp = m_new_value_sp->CreateCons...
[truncated]

@github-actions
Copy link

github-actions bot commented Sep 19, 2025

✅ With the latest revision this PR passed the Python code formatter.

@daniilavdeev
Copy link
Contributor Author

Hi @DavidSpickett, @jasonmolenda, @jimingham,

I've decided to divide the software watchpoint support patch (#151195) into several smaller parts. This is the first one, which consists almost entirely of NFC, refactoring of the existing watchpoint logic in LLDB.

Could you please take a look when you have a moment?

This patch is the first in a series of patches that add software
watchpoint support to LLDB.

The main purpose of this patch is to refactor watchpoint functionality
in LLDB, making it easier to implement software watchpoint logic.
@daniilavdeev daniilavdeev force-pushed the dlav-sc/lldb_refactor_software_watchpoint branch from 0851eb2 to 9092e3a Compare September 19, 2025 16:23
if (error.AsCString(nullptr))
result.AppendError(error.AsCString());
WatchpointSP watch_sp;
watch_sp =
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain this diff?

Copy link
Contributor Author

@daniilavdeev daniilavdeev Sep 20, 2025

Choose a reason for hiding this comment

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

If you are asking about WatchpointSP watch_sp;, then I myself don't know why this line is placed separately :) It seems I changed these lines several times.

Fixed it.

@felipepiovezan
Copy link
Contributor

Could you explain in broad strokes the theme of the refactoring here? This is a big PR to go in without a summary of what to expect.

// shouldn't push a step over breakpoint plan or do that work.
if (GetCurrentPlan()->IsVirtualStep())
return false;
if (GetResumeState() == eStateSuspended)
Copy link
Contributor

Choose a reason for hiding this comment

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

why is a patch about watchpoints changing this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I refactored this part as well because I plan to implement the logic for adding a WatchpointThreadPlan to the thread plan stack within this function. As I mentioned above, the logic for software watchpoints is planned to be implemented using a special ThreadPlan.

Essentially, the entire refactoring of this function boils down to adding a couple of early return statements. Personally, I strongly dislike cascades of nested if/else blocks. I understand that several years ago it was apparently fashionable to write functions with a single return statement, so I'm lenient towards that practice. However, early returns are much easier to read, and since I'm about to add new logic to this function anyway, I figured why not tidy it up first.

Although, if you think it's better to implement this part together with the logic for adding the thread plan, I wouldn't be opposed. We can leave this for the next patch.

@felipepiovezan
Copy link
Contributor

This is the first one, which consists almost entirely of NFC

Could you elaborate on the "almost entirely"? If it is not, what's stopping it from being? I'm worried about a massive change that looks NFC but that might be hiding a change in behavior.

};

// Make sure watchpoint is properly disabled and subsequently enabled while
// performing watchpoint actions.
Copy link
Collaborator

Choose a reason for hiding this comment

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

These should be doxygen comments ///

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed

@JDevlieghere
Copy link
Member

Thank you for breaking this change up into smaller patches, that's always a good idea and will certainly help with reviewing.

Given that this is a significant new feature, could you please start an RFC on Discourse? We had a good amount of discussion in the original PR that would be good to capture in the RFC.

@daniilavdeev
Copy link
Contributor Author

daniilavdeev commented Sep 19, 2025

Could you explain in broad strokes the theme of the refactoring here? This is a big PR to go in without a summary of what to expect.

Alright, I can describe a few key points:

I ==== StopInfo.cpp/Watchpoint.cpp ======================================

Prior to this patch, all logic for evaluating a watchpoint hit on the host side was implemented in the StopInfoWatchpoint::PerformAction function. Specifically, checks for the watchpoint condition https://github.com/llvm/llvm-project/pull/159807/files#diff-08d3a818bf87a1dc1d1558dec9570f8b4f27fb0a1bd0a8d37c74b29d43a8b92aL979-L1026, its ignore count https://github.com/llvm/llvm-project/pull/159807/files#diff-08d3a818bf87a1dc1d1558dec9570f8b4f27fb0a1bd0a8d37c74b29d43a8b92aL968-L971, and an additional check for watchpoints of the modify type https://github.com/llvm/llvm-project/pull/159807/files#diff-08d3a818bf87a1dc1d1558dec9570f8b4f27fb0a1bd0a8d37c74b29d43a8b92aL1056-L1058 were implemented within this function.

These checks are also necessary for software watchpoints. However, these checks are not performed within the StopInfoWatchpoint::PerformAction function but are instead handled by a special ThreadPlan, which is not part of this patch.

The reason is the difference in how hardware and software watchpoints are handled. In the case of a hardware watchpoint, we enter StopInfoWatchpoint::PerformAction after the LLDB client sets the WatchpointStopReason, which itself happens after the lldb-server reports a watchpoint hit.

In contrast, for a software watchpoint, all the logic is implemented on the client side; the lldb-server knows nothing about them. A special ThreadPlan is responsible for software watchpoints checks. When this ThreadPlan detects a hit, it sets the WatchpointStopReason, and only after that do we enter the StopInfoWatchpoint::PerformAction function.

Furthermore, there were software design issues. For example, the Watchpoint class had StopInfoWatchpoint as a friend https://github.com/llvm/llvm-project/pull/159807/files#diff-19e75401810e5613449c129f4525becae9b6ea148f9487bf8f68aa657b6277aeL194 because StopInfoWatchpoint::PerformAction needed to modify the private m_hit_counter field of the Watchpoint class.

These reasons led me to the idea that the watchpoint evaluation checks on the client side should be implemented directly within the Watchpoint class. Please note the Watchpoint::WatchedValueReportable function, into which a large part of the logic from StopInfoWatchpoint::PerformAction was moved. This includes the watchpoint condition check (which was refactored into a separate function CheckWatchpointCondition https://github.com/llvm/llvm-project/pull/159807/files#diff-5496aa2a96cec82fa4af70a90383248937efbf2cea6d19f768ecf8163b1f45c8R268 ), the ignore count check https://github.com/llvm/llvm-project/pull/159807/files#diff-5496aa2a96cec82fa4af70a90383248937efbf2cea6d19f768ecf8163b1f45c8R347-R350, the software watchpoint check https://github.com/llvm/llvm-project/pull/159807/files#diff-5496aa2a96cec82fa4af70a90383248937efbf2cea6d19f768ecf8163b1f45c8R332-R340, and an additional check for hardware modify watchpoints https://github.com/llvm/llvm-project/pull/159807/files#diff-5496aa2a96cec82fa4af70a90383248937efbf2cea6d19f768ecf8163b1f45c8R325-R330.

This allowed me to eliminate the need for StopInfoWatchpoint to be a friend and to reuse the logic for checking software watchpoints in the corresponding ThreadPlan.

Note: I simply moved the WatchpointSentry https://github.com/llvm/llvm-project/pull/159807/files#diff-08d3a818bf87a1dc1d1558dec9570f8b4f27fb0a1bd0a8d37c74b29d43a8b92aL690 class entirely from StopInfo.cpp to Watchpoint.h, as I also need this class for software watchpoint checks.

P.S.

Furthermore, I believe that checking the condition of a hardware watchpoint and its ignore count within StopInfoWatchpoint::PerformAction is, in general, a bad idea. Conceptually, setting the WatchpointStopReason should occur after all checks have been completed, specifically the checks for the watchpoint condition and its ignore count.

I don't remember all the intricacies anymore, but I encountered some very rare and exceptional bugs where a watchpoint with an ignore count would break the operation of step/next, leading to a situation where the debugger wouldn't regain control after the step/next command. These are very subtle issues; the stars really have to align to catch something like that.

@daniilavdeev
Copy link
Contributor Author

daniilavdeev commented Sep 19, 2025

II ==== Watchpoint.cpp ================================================

I conducted significant refactoring of the internal logic of the Watchpoint class. Initially, I created two separate classes, SoftwareWatchpoint and HardwareWatchpoint, which inherited from the Watchpoint class, because the logic of many methods in the Watchpoint class differed significantly.

However, I later noticed that the main differences were due to how the watchpoints calculated their watched value. How a watchpoint calculates the value depends on what we are trying to track. If we are tracking a value located in memory (e.g., a local variable), then the watchpoint simply needs to read the memory contents at the address of the tracked value. If we are tracking a register value or a complex expression like foo() + boo() + d which does not reside in memory, then the watchpoint must use the expression evaluation mechanism ((lldb)expr --) to obtain the value of the expression.

Therefore, I decided to extract the logic for calculating the watched value into separate entities: AddressWatchpointCalculateStrategy and ExpressionWatchpointCalculateStrategy (the latter is not in this patch) for calculating the watched value when the expression resides in memory and when it does not, respectively.

Please note that this is generally unrelated to whether the watchpoint is software or hardware. For tracking a value in memory, we can use either a software watchpoint (which must read the memory value after each step) or a hardware watchpoint (which can simply set a trigger on that memory region). However, a watchpoint on an expression not residing in memory can only be software, as there is nowhere for hardware to set a trigger in this case.

Additionally, I extracted some logic into separate functions to improve code readability (e.g., IsValueObjectsEqual) and improved error handling in some places https://github.com/llvm/llvm-project/pull/159807/files#diff-5496aa2a96cec82fa4af70a90383248937efbf2cea6d19f768ecf8163b1f45c8R254.

@daniilavdeev
Copy link
Contributor Author

daniilavdeev commented Sep 19, 2025

III ==== StopInfo.cpp =============================================

Besides moving the logic from StopInfoWatchpoint::PerformAction into Watchpoint::WatchedValueReportable as mentioned above, I also endeavored to improve the overall readability of StopInfoWatchpoint::PerformAction itself, for example by extracting logic into separate functions https://github.com/llvm/llvm-project/pull/159807/files#diff-08d3a818bf87a1dc1d1558dec9570f8b4f27fb0a1bd0a8d37c74b29d43a8b92aR963-R981 https://github.com/llvm/llvm-project/pull/159807/files#diff-08d3a818bf87a1dc1d1558dec9570f8b4f27fb0a1bd0a8d37c74b29d43a8b92aR903-R909 .

IV ==== lldb-enumeration.h ========================================

I added a separate enum https://github.com/llvm/llvm-project/pull/159807/files#diff-62036fb08830b445f086a17fd4be2132b4dc1da85446791d8c101574edbf58f3R1084 to describe whether a watchpoint is software or hardware. Using an is_hardware flag seems less than ideal to me, as it reduces code readability.

V ==== Target.cpp ===============================================

I renamed the CreateWatchpoint function to CreateWatchpointByAddress because I plan to add a CreateWatchpointByExpression function. Furthermore, I tried to extract common logic for the CreateWatchpointByAddress and future CreateWatchpointByExpression functions into separate helper functions https://github.com/llvm/llvm-project/pull/159807/files#diff-dcc80a369493ccabaf85b5492444457f8ce3f44c8be983dc01be9330f424e35cR945-R1033, which also improves code readability.

@daniilavdeev
Copy link
Contributor Author

daniilavdeev commented Sep 20, 2025

Could you elaborate on the "almost entirely"? If it is not, what's stopping it from being? I'm worried about a massive change that looks NFC but that might be hiding a change in behavior.

Yes, of course. Here I am referring to three specific points, two of which, incidentally, were reflected in the tests:

Firstly, I slightly modified the message displayed for watchpoints, for example by the watchpoint list command https://github.com/llvm/llvm-project/pull/159807/files#diff-5496aa2a96cec82fa4af70a90383248937efbf2cea6d19f768ecf8163b1f45c8R452. It now explicitly states whether a watchpoint is software or hardware, something like this:

(lldb) watchpoint list
Number of supported hardware watchpoints: 4
Current watchpoints:
Hardware Watchpoint 1: addr = 0x55555556deb0 size = 1 state = enabled type = w
    watchpoint spec = 'g_char_ptr'
    watchpoint resources:
       #0: addr = 0x55555556deb0 size = 1
Watchpoint 1 hit:

Notice that it is now explicitly stated that Watchpoint 1 is a hardware watchpoint. This broke one test lldb/test/API/commands/watchpoints/hello_watchlocation/TestWatchLocation.py, which I subsequently updated.

Secondly, I changed the logic for setting a watchpoint on the same address. Before this patch, LLDB could silently and opaquely remove an old watchpoint and replace it with a new one https://github.com/llvm/llvm-project/pull/159807/files#diff-dcc80a369493ccabaf85b5492444457f8ce3f44c8be983dc01be9330f424e35cL1018-L1019. The fact that LLDB did this implicitly seemed very questionable to me.

Instead of implicitly removing and replacing the watchpoint, I added an explicit message stating that a user cannot have multiple watchpoints monitoring the exact same address. If a user tries to set a new watchpoint on an address that is already monitored, and if any parameter of the new watchpoint (size, type, or hardware/software mode) differs from the old one, I notify the user of the impossibility and suggest they explicitly delete the old watchpoint using the watchpoint delete command before setting a new one https://github.com/llvm/llvm-project/pull/159807/files#diff-dcc80a369493ccabaf85b5492444457f8ce3f44c8be983dc01be9330f424e35cR1007-R1031. The message looks like this:

error: Address 0x55555556deb0 is already monitored by Watchpoint 1 with different size(2), type(w) or mode(Hardware).
Multiple watchpoints on the same address are not supported. You should manually delete Watchpoint 1 before setting a new watchpoint on this address.

In cases where all parameters of the new watchpoint are identical to the old one, I display an explicit message stating that LLDB will not create a new watchpoint but will reuse the existing, identical one:

warning: Address 0x55555556deb0 is already monitored by Watchpoint 1 with matching parameters: size (1), type (w), and mode (Hardware). Reusing existing watchpoint.

Thirdly, I changed the logic of how the ignore count works.
I would expect the ignore count to work as follows: the user specifies how many subsequent hits the watchpoint should ignore. For example, I have a watchpoint that has already been hit 3 times. I set ignore count = 2. After this, I expect LLDB to ignore the 4th and 5th hits, and on the 6th hit I expect to see a hit message and the watchpoint's ignore count (i.e., the number of subsequent hits it should ignore) now being 0. This is how watchpoints work in GDB.

Before the patch, the behavior of the ignore count in LLDB was very strange. The user would set an ignore count, and if hit count <= ignore count, the watchpoint hit would not be reported https://github.com/llvm/llvm-project/pull/159807/files#diff-08d3a818bf87a1dc1d1558dec9570f8b4f27fb0a1bd0a8d37c74b29d43a8b92aL968-L970. So, in the example above, when I already have a watchpoint with hit count = 3 and I set ignore count = 2, on the 4th hit LLDB would evaluate 4 <= 2 as false and therefore report the hit to the user. Furthermore, the ignore count was not decremented on a hit. Thus, the ignore count did not represent the number of future hits to ignore, but rather served as a threshold value for the hit count, below which hits were not reported.

Perhaps this implementation of the ignore count has its merits, but I find it counterintuitive. Therefore, I decided to change the ignore count logic to match the behavior in GDB https://github.com/llvm/llvm-project/pull/159807/files#diff-5496aa2a96cec82fa4af70a90383248937efbf2cea6d19f768ecf8163b1f45c8R345-R350.

This broke one test, which I also fixed (lldb/test/API/commands/watchpoints/watchpoint_commands/TestWatchpointCommands.py).

@daniilavdeev
Copy link
Contributor Author

Given that this is a significant new feature, could you please start an RFC on Discourse?

Thanks for the suggestion. I will create a topic in a day or two and will do my best to write something sensible there :)

@daniilavdeev
Copy link
Contributor Author

The RFC topic is here: https://discourse.llvm.org/t/rfc-software-watchpoints-support/88391
Please take a look when you get a chance.

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.

5 participants