From: Zane Leung <liangzhen@linux.spacemit.com>
To: gdb-patches@sourceware.org
Cc: zhuangqiubin@linux.spacemit.com
Subject: [PATCH v2 1/2] gdb: riscv: Add support for hardware breakpoints/watchpoints
Date: Thu, 9 Apr 2026 10:45:45 +0800 [thread overview]
Message-ID: <20260409024546.350958-2-liangzhen@linux.spacemit.com> (raw)
In-Reply-To: <20260409024546.350958-1-liangzhen@linux.spacemit.com>
From: liangzhen <zhen.liang@spacemit.com>
Implement hardware breakpoint and watchpoint support for RISC-V
architecture.
---
gdb/Makefile.in | 3 +
gdb/configure.nat | 3 +-
gdb/nat/riscv-hw-point.c | 244 ++++++++++++++++++++++++
gdb/nat/riscv-hw-point.h | 89 +++++++++
gdb/nat/riscv-linux-hw-point.c | 192 +++++++++++++++++++
gdb/nat/riscv-linux-hw-point.h | 122 ++++++++++++
gdb/nat/riscv-linux.c | 82 ++++++++
gdb/nat/riscv-linux.h | 39 ++++
gdb/riscv-linux-nat.c | 333 +++++++++++++++++++++++++++++++++
include/elf/common.h | 2 +
10 files changed, 1108 insertions(+), 1 deletion(-)
create mode 100644 gdb/nat/riscv-hw-point.c
create mode 100644 gdb/nat/riscv-hw-point.h
create mode 100644 gdb/nat/riscv-linux-hw-point.c
create mode 100644 gdb/nat/riscv-linux-hw-point.h
create mode 100644 gdb/nat/riscv-linux.c
create mode 100644 gdb/nat/riscv-linux.h
diff --git a/gdb/Makefile.in b/gdb/Makefile.in
index 2488d789580..8998c3fdfbf 100644
--- a/gdb/Makefile.in
+++ b/gdb/Makefile.in
@@ -1571,6 +1571,9 @@ HFILES_NO_SRCDIR = \
nat/linux-procfs.h \
nat/linux-ptrace.h \
nat/linux-waitpid.h \
+ nat/riscv-hw-point.h \
+ nat/riscv-linux.h \
+ nat/riscv-linux-hw-point.h \
nat/loongarch-hw-point.h \
nat/loongarch-linux.h \
nat/loongarch-linux-hw-point.h \
diff --git a/gdb/configure.nat b/gdb/configure.nat
index 38dd4179511..aee8541f486 100644
--- a/gdb/configure.nat
+++ b/gdb/configure.nat
@@ -325,7 +325,8 @@ case ${gdb_host} in
riscv*)
# Host: RISC-V, running Linux
NATDEPFILES="${NATDEPFILES} riscv-linux-nat.o \
- nat/riscv-linux-tdesc.o"
+ nat/riscv-linux-tdesc.o nat/riscv-hw-point.o \
+ nat/riscv-linux.o nat/riscv-linux-hw-point.o"
;;
s390)
# Host: S390, running Linux
diff --git a/gdb/nat/riscv-hw-point.c b/gdb/nat/riscv-hw-point.c
new file mode 100644
index 00000000000..8c13895c668
--- /dev/null
+++ b/gdb/nat/riscv-hw-point.c
@@ -0,0 +1,244 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "gdbsupport/break-common.h"
+#include "gdbsupport/common-regcache.h"
+#include "riscv-hw-point.h"
+#include "riscv-linux-hw-point.h"
+
+/* Number of hardware breakpoints/watchpoints the target supports.
+ They are initialized with values obtained via ptrace. */
+
+int riscv_num_hwbp_regs;
+
+/* Record the insertion of one breakpoint/watchpoint, as represented by
+ ADDR, TYPE and LEN, in the process' arch-specific data area *STATE.
+ Return TRUE if the operation succeeds. */
+
+static bool
+riscv_dr_state_insert_one_point (ptid_t ptid,
+ struct riscv_debug_reg_state *state,
+ enum target_hw_bp_type type, CORE_ADDR addr,
+ int len)
+{
+ int i, idx, num_regs;
+ unsigned int *dr_type_p, *dr_len_p, *dr_ref_count;
+ CORE_ADDR *dr_addr_p;
+
+ /* Set up state pointers. */
+ num_regs = riscv_num_hwbp_regs;
+ dr_addr_p = state->dr_addr_hwbp;
+ dr_type_p = state->dr_type_hwbp;
+ dr_len_p = state->dr_len_hwbp;
+ dr_ref_count = state->dr_ref_count_hwbp;
+
+ /* Find an existing or free register in our cache. */
+ idx = -1;
+ for (i = 0; i < num_regs; ++i)
+ {
+ if (dr_ref_count[i] == 0)
+ {
+ idx = i;
+ /* no break; continue hunting for an exising one. */
+ }
+ else if (dr_addr_p[i] == addr && dr_type_p[i] == type
+ && dr_len_p[i] == len)
+ {
+ idx = i;
+ break;
+ }
+ }
+
+ /* No space. */
+ if (idx == -1)
+ return false;
+
+ /* Update our cache. */
+ if (dr_ref_count[idx] == 0)
+ {
+ /* new entry */
+ dr_addr_p[idx] = addr;
+ dr_type_p[idx] = type;
+ dr_len_p[idx] = len;
+ dr_ref_count[idx] = 1;
+
+ /* Notify the change. */
+ riscv_notify_debug_reg_change (ptid, idx);
+ }
+ else
+ {
+ /* existing entry */
+ dr_ref_count[idx]++;
+ }
+
+ return true;
+}
+
+/* Record the removal of one breakpoint/watchpoint, as represented by
+ ADDR, TYPE and LEN, in the process' arch-specific data area *STATE.
+ Return TRUE if the operation succeeds. */
+
+static bool
+riscv_dr_state_remove_one_point (ptid_t ptid,
+ struct riscv_debug_reg_state *state,
+ enum target_hw_bp_type type, CORE_ADDR addr,
+ int len)
+{
+ int i, num_regs;
+ unsigned int *dr_type_p, *dr_len_p, *dr_ref_count;
+ CORE_ADDR *dr_addr_p;
+
+ /* Set up state pointers. */
+ num_regs = riscv_num_hwbp_regs;
+ dr_addr_p = state->dr_addr_hwbp;
+ dr_type_p = state->dr_type_hwbp;
+ dr_len_p = state->dr_len_hwbp;
+ dr_ref_count = state->dr_ref_count_hwbp;
+
+ /* Find the entry that matches the ADDR, TYPE and LEN. */
+ for (i = 0; i < num_regs; ++i)
+ if (dr_addr_p[i] == addr && dr_type_p[i] == type &&
+ dr_len_p[i] == len)
+ {
+ gdb_assert (dr_ref_count[i] != 0);
+ break;
+ }
+
+ /* Not found. */
+ if (i == num_regs)
+ return false;
+
+ /* Clear our cache. */
+ if (--dr_ref_count[i] == 0)
+ {
+ dr_addr_p[i] = 0;
+ dr_type_p[i] = 0;
+ dr_len_p[i] = 0;
+
+ /* Notify the change. */
+ riscv_notify_debug_reg_change (ptid, i);
+ }
+
+ return true;
+}
+
+/* Handle insertion or removal of a hardware breakpoint/watchpoint.
+ If IS_INSERT is 1, insert the point; otherwise remove it.
+ Return TRUE if the operation succeeds. */
+
+bool
+riscv_handle_point (enum target_hw_bp_type type, CORE_ADDR addr,
+ int len, int is_insert, ptid_t ptid,
+ struct riscv_debug_reg_state *state)
+{
+ if (is_insert)
+ return riscv_dr_state_insert_one_point (ptid, state, type, addr, len);
+ else
+ return riscv_dr_state_remove_one_point (ptid, state, type, addr, len);
+}
+
+/* Return TRUE if there are any hardware breakpoints/watchpoints. */
+
+bool
+riscv_any_set_debug_regs_state (riscv_debug_reg_state *state)
+{
+ int count = riscv_num_hwbp_regs;
+ if (count == 0)
+ return false;
+
+ const CORE_ADDR *addr = state->dr_addr_hwbp;
+ const unsigned int *len = state->dr_len_hwbp;
+
+ for (int i = 0; i < count; i++)
+ if (addr[i] != 0 || len[i] != 0)
+ return true;
+
+ return false;
+}
+
+/* Print the values of the cached breakpoint/watchpoint registers. */
+
+void
+riscv_show_debug_reg_state (struct riscv_debug_reg_state *state,
+ const char *func, CORE_ADDR addr,
+ int len, enum target_hw_bp_type type)
+{
+ debug_printf ("%s", func);
+ if (addr != 0 || len != 0)
+ debug_printf (" (addr=0x%08lx, len=%d, type=%s)",
+ (unsigned long) addr, len,
+ type == hw_write ? "hw-write-watchpoint"
+ : (type == hw_read ? "hw-read-watchpoint"
+ : (type == hw_access ? "hw-access-watchpoint"
+ : (type == hw_execute ? "hw-breakpoint"
+ : "??unknown??"))));
+ debug_printf (":\n");
+
+ debug_printf ("\tHWPOINTs:\n");
+ for (int i = 0; i < riscv_num_hwbp_regs; i++)
+ debug_printf ("\t%cP%d: addr=%s, len=%d, type=%s, ref.count=%d\n",
+ state->dr_type_hwbp[i] == hw_execute ? 'B' : 'W',
+ i, core_addr_to_string_nz (state->dr_addr_hwbp[i]),
+ state->dr_len_hwbp[i],
+ type == hw_write ? "hw-write-watchpoint"
+ : (type == hw_read ? "hw-read-watchpoint"
+ : (type == hw_access ? "hw-access-watchpoint"
+ : (type == hw_execute ? "hw-breakpoint"
+ : "??unknown??"))),
+ state->dr_ref_count_hwbp[i]);
+}
+
+/* Return true if we can watch a memory region that starts address
+ ADDR and whose length is LEN in bytes. */
+
+int
+riscv_region_ok_for_watchpoint (CORE_ADDR addr, int len)
+{
+ /* Can not set watchpoints for zero or negative lengths. */
+ if (len <= 0)
+ return 0;
+
+ /* Must have hardware watchpoint debug register(s). */
+ if (riscv_num_hwbp_regs == 0)
+ return 0;
+
+ return 1;
+}
+
+/* Helper for the "stopped_data_address/low_stopped_data_address" target
+ method. Returns TRUE if a hardware watchpoint trap at ADDR_TRAP matches
+ a set watchpoint. The address of the matched watchpoint is returned in
+ *ADDR_P. */
+
+bool
+riscv_stopped_data_address (const struct riscv_debug_reg_state *state,
+ CORE_ADDR addr_trap, CORE_ADDR *addr_p)
+{
+ for (int i = 0; i < riscv_num_hwbp_regs; i++)
+ {
+ const CORE_ADDR addr = state->dr_addr_hwbp[i];
+ if (state->dr_ref_count_hwbp[i]
+ && addr_trap == addr
+ && state->dr_type_hwbp[i] != hw_execute)
+ {
+ *addr_p = addr;
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/gdb/nat/riscv-hw-point.h b/gdb/nat/riscv-hw-point.h
new file mode 100644
index 00000000000..0a6d3795d64
--- /dev/null
+++ b/gdb/nat/riscv-hw-point.h
@@ -0,0 +1,89 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef GDB_NAT_RISCV_HW_POINT_H
+#define GDB_NAT_RISCV_HW_POINT_H
+
+/* Macro definitions, data structures, and code for the hardware
+ breakpoint and hardware watchpoint support follow. We use the
+ following abbreviations throughout the code:
+
+ hw - hardware
+ bp - breakpoint
+ wp - watchpoint */
+
+/* Maximum number of hardware breakpoint + watchpoint registers. */
+
+#define RISCV_HWBP_MAX_NUM 16
+
+
+/* Structure for managing the hardware breakpoint/watchpoint resources.
+ DR_ADDR_* stores the address, DR_CTRL_* stores the control register
+ content, and DR_REF_COUNT_* counts the numbers of references to the
+ corresponding hwbp, by which way the limited hardware resources are
+ not wasted on duplicated bp/wp settings (though so far gdb has done
+ a good job by not sending duplicated bp/wp requests). */
+
+struct riscv_debug_reg_state
+{
+ /* hardware/hardware breakpoint */
+ CORE_ADDR dr_addr_hwbp[RISCV_HWBP_MAX_NUM];
+ unsigned int dr_type_hwbp[RISCV_HWBP_MAX_NUM];
+ unsigned int dr_len_hwbp[RISCV_HWBP_MAX_NUM];
+ unsigned int dr_ref_count_hwbp[RISCV_HWBP_MAX_NUM];
+};
+
+extern int riscv_num_hwbp_regs;
+
+/* Invoked when IDXth breakpoint/watchpoint register pair needs to be
+ updated. */
+
+void riscv_notify_debug_reg_change (ptid_t ptid, unsigned int idx);
+
+
+/* Handle insertion or removal of a hardware breakpoint/watchpoint.
+ If IS_INSERT is 1, insert the point; otherwise remove it.
+ Return TRUE if the operation succeeds. */
+
+bool riscv_handle_point (enum target_hw_bp_type type, CORE_ADDR addr,
+ int len, int is_insert, ptid_t ptid,
+ struct riscv_debug_reg_state *state);
+
+/* Return TRUE if there are any hardware breakpoints/watchpoints. */
+
+bool riscv_any_set_debug_regs_state (riscv_debug_reg_state *state);
+
+/* Print the values of the cached breakpoint/watchpoint registers. */
+
+void riscv_show_debug_reg_state (struct riscv_debug_reg_state *state,
+ const char *func, CORE_ADDR addr,
+ int len, enum target_hw_bp_type type);
+
+/* Return true if we can watch a memory region that starts address
+ ADDR and whose length is LEN in bytes. */
+
+int riscv_region_ok_for_watchpoint (CORE_ADDR addr, int len);
+
+/* Helper for the "stopped_data_address/low_stopped_data_address" target
+ method. Returns TRUE if a hardware watchpoint trap at ADDR_TRAP matches
+ a set watchpoint. The address of the matched watchpoint is returned in
+ *ADDR_P. */
+
+bool riscv_stopped_data_address (const struct riscv_debug_reg_state *state,
+ CORE_ADDR addr_trap, CORE_ADDR *addr_p);
+
+#endif /* GDB_NAT_RISCV_HW_POINT_H */
diff --git a/gdb/nat/riscv-linux-hw-point.c b/gdb/nat/riscv-linux-hw-point.c
new file mode 100644
index 00000000000..e7f3865bb66
--- /dev/null
+++ b/gdb/nat/riscv-linux-hw-point.c
@@ -0,0 +1,192 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "gdbsupport/break-common.h"
+#include "gdbsupport/common-regcache.h"
+#include "nat/linux-nat.h"
+#include "riscv-linux-hw-point.h"
+
+#include <sys/uio.h>
+
+/* The order in which <sys/ptrace.h> and <asm/ptrace.h> are included
+ can be important. <sys/ptrace.h> often declares various PTRACE_*
+ enums. <asm/ptrace.h> often defines preprocessor constants for
+ these very same symbols. When that's the case, build errors will
+ result when <asm/ptrace.h> is included before <sys/ptrace.h>. */
+
+#include <sys/ptrace.h>
+#include <asm/ptrace.h>
+
+#include "elf/common.h"
+
+/* Helper for riscv_notify_debug_reg_change. Records the
+ information about the change of one hardware breakpoint/watchpoint
+ setting for the thread LWP.
+ N.B. The actual updating of hardware debug registers is not
+ carried out until the moment the thread is resumed. */
+
+static int
+riscv_dr_change_callback (struct lwp_info *lwp, unsigned int idx)
+{
+ int tid = ptid_of_lwp (lwp).lwp ();
+ struct arch_lwp_info *info = lwp_arch_private_info (lwp);
+ dr_changed_t *dr_changed_ptr;
+ dr_changed_t dr_changed;
+
+ if (info == NULL)
+ {
+ info = XCNEW (struct arch_lwp_info);
+ lwp_set_arch_private_info (lwp, info);
+ }
+
+ if (show_debug_regs)
+ {
+ debug_printf ("riscv_dr_change_callback: \n\tOn entry:\n");
+ debug_printf ("\ttid%d, dr_changed_hwbp=0x%s\n", tid,
+ phex (info->dr_changed_hwbp, 8));
+ }
+
+ dr_changed_ptr = &info->dr_changed_hwbp;
+ dr_changed = *dr_changed_ptr;
+
+ gdb_assert (idx >= 0 && idx <= riscv_num_hwbp_regs);
+
+ /* The actual update is done later just before resuming the lwp,
+ we just mark that one register pair needs updating. */
+ DR_MARK_N_CHANGED (dr_changed, idx);
+ *dr_changed_ptr = dr_changed;
+
+ /* If the lwp isn't stopped, force it to momentarily pause, so
+ we can update its debug registers. */
+ if (!lwp_is_stopped (lwp))
+ linux_stop_lwp (lwp);
+
+ if (show_debug_regs)
+ {
+ debug_printf ("\tOn exit:\n\ttid%d, dr_changed_hwbp=0x%s\n", tid,
+ phex (info->dr_changed_hwbp, 8));
+ }
+
+ return 0;
+}
+
+/* Notify each thread that their IDXth breakpoint/watchpoint register
+ pair needs to be updated. The message will be recorded in each
+ thread's arch-specific data area, the actual updating will be done
+ when the thread is resumed. */
+
+void
+riscv_notify_debug_reg_change (ptid_t ptid, unsigned int idx)
+{
+ ptid_t pid_ptid = ptid_t (ptid.pid ());
+
+ iterate_over_lwps (pid_ptid, [=] (struct lwp_info *info)
+ {
+ return riscv_dr_change_callback (info,
+ idx);
+ });
+}
+
+/* Convert GDB hardware breakpoint/watchpoint type to RISC-V hardware type. */
+
+static unsigned int
+riscv_hwbp_type (unsigned int type)
+{
+ switch (type)
+ {
+ case hw_execute:
+ return RISCV_HW_EXECUTE;
+ case hw_read:
+ return RISCV_HW_READ;
+ case hw_write:
+ return RISCV_HW_WRITE;
+ case hw_access:
+ return RISCV_HW_ACCESS;
+ }
+
+ gdb_assert_not_reached ("unexpected hwbp type");
+}
+
+/* Call ptrace to set the thread TID's hardware breakpoint/watchpoint
+ registers with data from *STATE. */
+
+void
+riscv_linux_set_debug_regs (struct riscv_debug_reg_state *state,
+ int tid)
+{
+ int i, count;
+ struct iovec iov;
+ struct riscv_hwdebug_state regs;
+
+ memset (®s, 0, sizeof (regs));
+ iov.iov_base = ®s;
+ count = riscv_num_hwbp_regs;
+
+ if (count == 0)
+ return;
+
+ iov.iov_len = (offsetof (struct riscv_hwdebug_state, dbg_regs)
+ + count * sizeof (regs.dbg_regs[0]));
+ for (i = 0; i < count; i++)
+ {
+ regs.dbg_regs[i].addr = state->dr_addr_hwbp[i];
+ regs.dbg_regs[i].type = riscv_hwbp_type(state->dr_type_hwbp[i]);
+ regs.dbg_regs[i].len = state->dr_len_hwbp[i];
+ }
+
+ if (ptrace(PTRACE_SETREGSET, tid, NT_RISCV_HW_BREAK, (void *) &iov))
+ {
+ if (errno == EINVAL)
+ error (_("Invalid argument setting hardware debug registers"));
+ else
+ error (_("Unexpected error setting hardware debug registers"));
+ }
+}
+
+/* Get the hardware debug register capacity information from the
+ process represented by TID. */
+
+void
+riscv_linux_get_debug_reg_capacity (int tid)
+{
+ struct iovec iov;
+ struct riscv_hwdebug_state dbg_state;
+ int result;
+ iov.iov_base = &dbg_state;
+ iov.iov_len = sizeof (dbg_state);
+
+ /* Get hardware breakpoint/watchpoint register info. */
+ result = ptrace (PTRACE_GETREGSET, tid, NT_RISCV_HW_BREAK, &iov);
+
+ if (result == 0)
+ {
+ riscv_num_hwbp_regs = RISCV_DEBUG_NUM_SLOTS (dbg_state.dbg_info);
+ if (riscv_num_hwbp_regs > RISCV_HWBP_MAX_NUM)
+ {
+ warning (_("Unexpected number of hardware breakpoint/watchpoint"
+ " registers reported by ptrace, got %d, expected %d."),
+ riscv_num_hwbp_regs, RISCV_HWBP_MAX_NUM);
+ riscv_num_hwbp_regs = RISCV_HWBP_MAX_NUM;
+ }
+ }
+ else
+ {
+ warning (_("Unable to determine the number of hardware"
+ " breakpoints/watchpoints available."));
+ riscv_num_hwbp_regs = 0;
+ }
+}
diff --git a/gdb/nat/riscv-linux-hw-point.h b/gdb/nat/riscv-linux-hw-point.h
new file mode 100644
index 00000000000..0f0f8f79577
--- /dev/null
+++ b/gdb/nat/riscv-linux-hw-point.h
@@ -0,0 +1,122 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef GDB_NAT_RISCV_LINUX_HW_POINT_H
+#define GDB_NAT_RISCV_LINUX_HW_POINT_H
+
+#include "gdbsupport/break-common.h" /* For enum target_hw_bp_type. */
+
+#include "nat/riscv-hw-point.h"
+
+struct riscv_hwdebug_state {
+ uint64_t dbg_info;
+ struct {
+ uint64_t addr;
+ uint64_t type;
+ uint64_t len;
+ } dbg_regs[16];
+};
+
+enum riscv_hwbp_type {
+ RISCV_HW_EXECUTE = 0,
+ RISCV_HW_READ = 1,
+ RISCV_HW_WRITE = 2,
+ RISCV_HW_ACCESS = 3,
+};
+
+/* Macros to extract fields from the hardware debug information word. */
+#define RISCV_DEBUG_NUM_SLOTS(x) ((x) & 0xffff)
+
+/* Each bit of a variable of this type is used to indicate whether a
+ hardware breakpoint or watchpoint setting has been changed since
+ the last update.
+
+ Bit N corresponds to the Nth hardware breakpoint or watchpoint
+ setting which is managed in riscv_debug_reg_state, where N is
+ valid between 0 and the total number of the hardware breakpoint or
+ watchpoint debug registers minus 1.
+
+ When bit N is 1, the corresponding breakpoint or watchpoint setting
+ has changed, and therefore the corresponding hardware debug
+ register needs to be updated via the ptrace interface.
+
+ In the per-thread arch-specific data area, we define two such
+ variables for per-thread hardware breakpoint and watchpoint
+ settings respectively.
+
+ This type is part of the mechanism which helps reduce the number of
+ ptrace calls to the kernel, i.e. avoid asking the kernel to write
+ to the debug registers with unchanged values. */
+
+typedef ULONGEST dr_changed_t;
+
+/* Set each of the lower M bits of X to 1; assert X is wide enough. */
+
+#define DR_MARK_ALL_CHANGED(x, m) \
+ do \
+ { \
+ gdb_assert (sizeof ((x)) * 8 >= (m)); \
+ (x) = (((dr_changed_t)1 << (m)) - 1); \
+ } while (0)
+
+#define DR_MARK_N_CHANGED(x, n) \
+ do \
+ { \
+ (x) |= ((dr_changed_t)1 << (n)); \
+ } while (0)
+
+#define DR_CLEAR_CHANGED(x) \
+ do \
+ { \
+ (x) = 0; \
+ } while (0)
+
+#define DR_HAS_CHANGED(x) ((x) != 0)
+#define DR_N_HAS_CHANGED(x, n) ((x) & ((dr_changed_t)1 << (n)))
+
+/* Per-thread arch-specific data we want to keep. */
+
+struct arch_lwp_info
+{
+ /* When bit N is 1, it indicates the Nth hardware breakpoint or
+ watchpoint register pair needs to be updated when the thread is
+ resumed; see riscv_linux_prepare_to_resume. */
+ dr_changed_t dr_changed_hwbp;
+};
+
+/* Call ptrace to set the thread TID's hardware breakpoint/watchpoint
+ registers with data from *STATE. */
+
+void riscv_linux_set_debug_regs (struct riscv_debug_reg_state *state,
+ int tid);
+
+/* Get the hardware debug register capacity information from the
+ process represented by TID. */
+
+void riscv_linux_get_debug_reg_capacity (int tid);
+
+/* Return the debug register state for process PID. If no existing
+ state is found for this process, return nullptr. */
+
+struct riscv_debug_reg_state *riscv_lookup_debug_reg_state (pid_t pid);
+
+/* Return the debug register state for process PID. If no existing
+ state is found for this process, create new state. */
+
+struct riscv_debug_reg_state *riscv_get_debug_reg_state (pid_t pid);
+
+#endif /* GDB_NAT_RISCV_LINUX_HW_POINT_H */
diff --git a/gdb/nat/riscv-linux.c b/gdb/nat/riscv-linux.c
new file mode 100644
index 00000000000..65183fd3dbd
--- /dev/null
+++ b/gdb/nat/riscv-linux.c
@@ -0,0 +1,82 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "gdbsupport/break-common.h"
+#include "nat/linux-nat.h"
+#include "nat/riscv-linux-hw-point.h"
+#include "nat/riscv-linux.h"
+
+#include "elf/common.h"
+#include "nat/gdb_ptrace.h"
+#include <asm/ptrace.h>
+#include <sys/uio.h>
+
+/* Called when resuming a thread LWP.
+ The hardware debug registers are updated when there is any change. */
+
+void
+riscv_linux_prepare_to_resume (struct lwp_info *lwp)
+{
+ struct arch_lwp_info *info = lwp_arch_private_info (lwp);
+
+ /* NULL means this is the main thread still going through the shell,
+ or, no watchpoint has been set yet. In that case, there's
+ nothing to do. */
+ if (info == NULL)
+ return;
+
+ if (DR_HAS_CHANGED (info->dr_changed_hwbp))
+ {
+ ptid_t ptid = ptid_of_lwp (lwp);
+ int tid = ptid.lwp ();
+ struct riscv_debug_reg_state *state
+ = riscv_get_debug_reg_state (ptid.pid ());
+
+ if (show_debug_regs)
+ debug_printf ("prepare_to_resume thread %d\n", tid);
+
+ riscv_linux_set_debug_regs (state, tid);
+ DR_CLEAR_CHANGED (info->dr_changed_hwbp);
+ }
+}
+
+/* Function to call when a new thread is detected. */
+
+void
+riscv_linux_new_thread (struct lwp_info *lwp)
+{
+ ptid_t ptid = ptid_of_lwp (lwp);
+ struct riscv_debug_reg_state *state
+ = riscv_get_debug_reg_state (ptid.pid ());
+ struct arch_lwp_info *info = XCNEW (struct arch_lwp_info);
+
+ /* If there are hardware breakpoints/watchpoints in the process then mark that
+ all the hardware breakpoint/watchpoint register pairs for this thread need
+ to be initialized (with data from arch_process_info.debug_reg_state). */
+ if (riscv_any_set_debug_regs_state (state))
+ DR_MARK_ALL_CHANGED (info->dr_changed_hwbp, riscv_num_hwbp_regs);
+
+ lwp_set_arch_private_info (lwp, info);
+}
+
+/* See nat/riscv-linux.h. */
+
+void
+riscv_linux_delete_thread (struct arch_lwp_info *arch_lwp)
+{
+ xfree (arch_lwp);
+}
diff --git a/gdb/nat/riscv-linux.h b/gdb/nat/riscv-linux.h
new file mode 100644
index 00000000000..b147ba93319
--- /dev/null
+++ b/gdb/nat/riscv-linux.h
@@ -0,0 +1,39 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef GDB_NAT_RISCV_LINUX_H
+#define GDB_NAT_RISCV_LINUX_H
+
+#include <signal.h>
+
+/* Defines ps_err_e, struct ps_prochandle. */
+#include "gdb_proc_service.h"
+
+/* Called when resuming a thread LWP.
+ The hardware debug registers are updated when there is any change. */
+
+void riscv_linux_prepare_to_resume (struct lwp_info *lwp);
+
+/* Function to call when a new thread is detected. */
+
+void riscv_linux_new_thread (struct lwp_info *lwp);
+
+/* Function to call when a thread is being deleted. */
+
+void riscv_linux_delete_thread (struct arch_lwp_info *arch_lwp);
+
+#endif /* GDB_NAT_RISCV_LINUX_H */
diff --git a/gdb/riscv-linux-nat.c b/gdb/riscv-linux-nat.c
index e1e81ad50c4..77b8e868590 100644
--- a/gdb/riscv-linux-nat.c
+++ b/gdb/riscv-linux-nat.c
@@ -22,9 +22,13 @@
#include "riscv-tdep.h"
#include "inferior.h"
+#include "cli/cli-cmds.h"
#include "elf/common.h"
#include "nat/riscv-linux-tdesc.h"
+#include "nat/riscv-hw-point.h"
+#include "nat/riscv-linux.h"
+#include "nat/riscv-linux-hw-point.h"
#include <sys/ptrace.h>
@@ -33,6 +37,44 @@
# define NFPREG 33
#endif
+/* Hash table storing per-process data. We don't bind this to a
+ per-inferior registry because of targets like GNU/Linux that
+ need to keep track of processes that aren't bound to any inferior
+ (e.g., fork children, checkpoints). */
+
+static std::unordered_map<pid_t, riscv_debug_reg_state>
+ riscv_debug_process_state;
+
+/* Return the debug register state for process PID. If no existing
+ state is found for this process, return nullptr. */
+
+struct riscv_debug_reg_state *
+riscv_lookup_debug_reg_state (pid_t pid)
+{
+ auto it = riscv_debug_process_state.find (pid);
+ if (it != riscv_debug_process_state.end ())
+ return &it->second;
+
+ return nullptr;
+}
+
+/* Return the debug register state for process PID. If no existing
+ state is found for this process, create new state. */
+
+struct riscv_debug_reg_state *
+riscv_get_debug_reg_state (pid_t pid)
+{
+ return &riscv_debug_process_state[pid];
+}
+
+/* Remove any existing per-process debug state for process PID. */
+
+static void
+riscv_remove_debug_reg_state (pid_t pid)
+{
+ riscv_debug_process_state.erase (pid);
+}
+
/* RISC-V Linux native additions to the default linux support. */
class riscv_linux_nat_target final : public linux_nat_target
@@ -42,6 +84,37 @@ class riscv_linux_nat_target final : public linux_nat_target
void fetch_registers (struct regcache *regcache, int regnum) override;
void store_registers (struct regcache *regcache, int regnum) override;
+ /* Add our hardware breakpoint and watchpoint implementation. */
+ int can_use_hw_breakpoint (enum bptype type, int cnt, int othertype) override;
+ int region_ok_for_hw_watchpoint (CORE_ADDR addr, int len) override;
+ int insert_hw_breakpoint (struct gdbarch *gdbarch,
+ struct bp_target_info *bp_tgt) override;
+ int remove_hw_breakpoint (struct gdbarch *gdbarch,
+ struct bp_target_info *bp_tgt) override;
+ int insert_watchpoint (CORE_ADDR addr, int len, enum target_hw_bp_type type,
+ struct expression *cond) override;
+ int remove_watchpoint (CORE_ADDR addr, int len, enum target_hw_bp_type type,
+ struct expression *cond) override;
+ bool stopped_by_watchpoint () override;
+ std::vector<CORE_ADDR> stopped_data_addresses () override;
+
+ /* Override the GNU/Linux inferior startup hook. */
+ void post_startup_inferior (ptid_t ptid) override;
+
+ /* Override the GNU/Linux post attach hook. */
+ void post_attach (int pid) override;
+
+ /* These three defer to common nat/ code. */
+ void low_new_thread (struct lwp_info *lp) override
+ { riscv_linux_new_thread (lp); }
+ void low_delete_thread (struct arch_lwp_info *lp) override
+ { riscv_linux_delete_thread (lp); }
+ void low_prepare_to_resume (struct lwp_info *lp) override
+ { riscv_linux_prepare_to_resume (lp); }
+
+ void low_new_fork (struct lwp_info *parent, pid_t child_pid) override;
+ void low_forget_process (pid_t pid) override;
+
/* Read suitable target description. */
const struct target_desc *read_description () override;
};
@@ -327,6 +400,251 @@ riscv_linux_nat_target::store_registers (struct regcache *regcache, int regnum)
now. */
}
+/* Check if nat target can use hardware breakpoints/watchpoints. */
+
+int
+riscv_linux_nat_target::can_use_hw_breakpoint (enum bptype type, int cnt,
+ int othertype)
+{
+ switch (type)
+ {
+ case bp_hardware_breakpoint:
+ case bp_hardware_watchpoint:
+ case bp_read_watchpoint:
+ case bp_access_watchpoint:
+ case bp_watchpoint:
+ if (riscv_num_hwbp_regs == 0)
+ return 0;
+ break;
+ default:
+ gdb_assert_not_reached ("unexpected breakpoint type");
+ }
+ return 1;
+}
+
+int
+riscv_linux_nat_target::region_ok_for_hw_watchpoint (CORE_ADDR addr,
+ int len)
+{
+ return riscv_region_ok_for_watchpoint (addr, len);
+}
+
+/* Insert a hardware breakpoint at BP_TGT->placed_address.
+ Return 0 if point is inserted, -1 if not. */
+
+int
+riscv_linux_nat_target::insert_hw_breakpoint (struct gdbarch *gdbarch,
+ struct bp_target_info *bp_tgt)
+{
+ int ret;
+ CORE_ADDR addr = bp_tgt->placed_address = bp_tgt->reqstd_address;
+ int len;
+ const enum target_hw_bp_type type = hw_execute;
+ struct riscv_debug_reg_state *state
+ = riscv_get_debug_reg_state (inferior_ptid.pid ());
+
+ gdbarch_breakpoint_from_pc (gdbarch, &addr, &len);
+
+ if (show_debug_regs)
+ gdb_printf (gdb_stdlog,
+ "insert_hw_breakpoint on entry (addr=0x%08lx, len=%d))\n",
+ (unsigned long) addr, len);
+
+ ret = riscv_handle_point (type, addr, len, 1 /* is_insert */,
+ inferior_ptid, state) ? 0 : -1;
+
+ if (show_debug_regs)
+ {
+ riscv_show_debug_reg_state (state,
+ "insert_hw_breakpoint", addr, len, type);
+ }
+
+ return ret;
+}
+
+/* Remove a hardware breakpoint at BP_TGT->placed_address.
+ Return 0 if point is removeed, -1 if not. */
+
+int
+riscv_linux_nat_target::remove_hw_breakpoint (struct gdbarch *gdbarch,
+ struct bp_target_info *bp_tgt)
+{
+ int ret, len;
+ CORE_ADDR addr = bp_tgt->placed_address;
+ const enum target_hw_bp_type type = hw_execute;
+ struct riscv_debug_reg_state *state
+ = riscv_get_debug_reg_state (inferior_ptid.pid ());
+
+ gdbarch_breakpoint_from_pc (gdbarch, &addr, &len);
+
+ if (show_debug_regs)
+ gdb_printf (gdb_stdlog,
+ "remove_hw_breakpoint on entry (addr=0x%08lx, len=%d))\n",
+ (unsigned long) addr, len);
+
+ ret = riscv_handle_point (type, addr, len, 0 /* is_insert */,
+ inferior_ptid, state) ? 0 : -1;
+
+ if (show_debug_regs)
+ {
+ riscv_show_debug_reg_state (state,
+ "remove_hw_watchpoint", addr, len, type);
+ }
+
+ return ret;
+}
+
+/* Insert a hardware watchpoint of type TYPE on region starting at address ADDR
+ with LEN legth. Return 0 if point is inserted, -1 if not. */
+
+int
+riscv_linux_nat_target::insert_watchpoint (CORE_ADDR addr, int len,
+ enum target_hw_bp_type type,
+ struct expression *cond)
+{
+ int ret;
+ struct riscv_debug_reg_state *state
+ = riscv_get_debug_reg_state (inferior_ptid.pid ());
+
+ if (show_debug_regs)
+ gdb_printf (gdb_stdlog,
+ "insert_watchpoint on entry (addr=0x%08lx, len=%d)\n",
+ (unsigned long) addr, len);
+
+ gdb_assert (type != hw_execute);
+
+ ret = riscv_handle_point (type, addr, len, 1 /* is_insert */,
+ inferior_ptid, state) ? 0 : -1;
+
+ if (show_debug_regs)
+ {
+ riscv_show_debug_reg_state (state,
+ "insert_watchpoint", addr, len, type);
+ }
+
+ return ret;
+}
+
+/* Remove a hardware watchpoint of type TYPE on region starting at address ADDR
+ with LEN legth. Return 0 if point is removed, -1 if not. */
+
+int
+riscv_linux_nat_target::remove_watchpoint (CORE_ADDR addr, int len,
+ enum target_hw_bp_type type,
+ struct expression *cond)
+{
+ int ret;
+ struct riscv_debug_reg_state *state
+ = riscv_get_debug_reg_state (inferior_ptid.pid ());
+
+ if (show_debug_regs)
+ gdb_printf (gdb_stdlog,
+ "remove_watchpoint on entry (addr=0x%08lx, len=%d)\n",
+ (unsigned long) addr, len);
+
+ gdb_assert (type != hw_execute);
+
+ ret = riscv_handle_point (type, addr, len, 0 /* is_insert */,
+ inferior_ptid, state) ? 0 : -1;
+
+ if (show_debug_regs)
+ {
+ riscv_show_debug_reg_state (state,
+ "remove_watchpoint", addr, len, type);
+ }
+
+ return ret;
+}
+
+/* Implement the "stopped_data_addresses" target_ops method. */
+
+std::vector<CORE_ADDR>
+riscv_linux_nat_target::stopped_data_addresses ()
+{
+ siginfo_t siginfo;
+ struct riscv_debug_reg_state *state;
+
+ if (!linux_nat_get_siginfo (inferior_ptid, &siginfo))
+ return {};
+
+ /* This must be a hardware breakpoint. */
+ if (siginfo.si_signo != SIGTRAP || (siginfo.si_code & 0xffff) != TRAP_HWBKPT)
+ return {};
+
+ state = riscv_get_debug_reg_state (inferior_ptid.pid ());
+
+ CORE_ADDR addr;
+ if (riscv_stopped_data_address (state, (CORE_ADDR) siginfo.si_addr,
+ &addr))
+ return { addr };
+
+ return {};
+}
+
+/* Implement the "stopped_by_watchpoint" target_ops method. */
+
+bool
+riscv_linux_nat_target::stopped_by_watchpoint ()
+{
+ return !stopped_data_addresses ().empty ();
+}
+
+/* Implement the virtual inf_ptrace_target::post_startup_inferior method. */
+
+void
+riscv_linux_nat_target::post_startup_inferior (ptid_t ptid)
+{
+ low_forget_process (ptid.pid ());
+ riscv_linux_get_debug_reg_capacity (ptid.pid ());
+ linux_nat_target::post_startup_inferior (ptid);
+}
+
+/* Implement the "post_attach" target_ops method. */
+
+void
+riscv_linux_nat_target::post_attach (int pid)
+{
+ low_forget_process (pid);
+ riscv_linux_get_debug_reg_capacity (pid);
+ linux_nat_target::post_attach (pid);
+}
+
+/* linux_nat_new_fork hook. */
+
+void
+riscv_linux_nat_target::low_new_fork (struct lwp_info *parent,
+ pid_t child_pid)
+{
+ pid_t parent_pid;
+ struct riscv_debug_reg_state *parent_state;
+ struct riscv_debug_reg_state *child_state;
+
+ /* NULL means no watchpoint has ever been set in the parent. In
+ that case, there's nothing to do. */
+ if (parent->arch_private == NULL)
+ return;
+
+ /* GDB core assumes the child inherits the watchpoints/hw
+ breakpoints of the parent, and will remove them all from the
+ forked off process. Copy the debug registers mirrors into the
+ new process so that all breakpoints and watchpoints can be
+ removed together. */
+
+ parent_pid = parent->ptid.pid ();
+ parent_state = riscv_get_debug_reg_state (parent_pid);
+ child_state = riscv_get_debug_reg_state (child_pid);
+ *child_state = *parent_state;
+}
+
+/* Called whenever GDB is no longer debugging process PID. It deletes
+ data structures that keep track of debug register state. */
+
+void
+riscv_linux_nat_target::low_forget_process (pid_t pid)
+{
+ riscv_remove_debug_reg_state (pid);
+}
+
/* Initialize RISC-V Linux native support. */
INIT_GDB_FILE (riscv_linux_nat)
@@ -334,4 +652,19 @@ INIT_GDB_FILE (riscv_linux_nat)
/* Register the target. */
linux_target = &the_riscv_linux_nat_target;
add_inf_child_target (&the_riscv_linux_nat_target);
+
+ /* Add a maintenance command to enable printing the RISC-V internal
+ debug registers mirror variables. */
+ add_setshow_boolean_cmd ("show-debug-regs", class_maintenance,
+ &show_debug_regs, _("\
+Set whether to show the RISC-V debug registers state."), _("\
+Show whether to show the RISC-V debug registers state."), _("\
+Use \"on\" to enable, \"off\" to disable.\n\
+If enabled, the debug registers values are shown when GDB inserts\n\
+or removes a hardware breakpoint or watchpoint, and when the inferior\n\
+triggers a breakpoint or watchpoint."),
+ NULL,
+ NULL,
+ &maintenance_set_cmdlist,
+ &maintenance_show_cmdlist);
}
diff --git a/include/elf/common.h b/include/elf/common.h
index 1ae68221a89..5d27dcb14e0 100644
--- a/include/elf/common.h
+++ b/include/elf/common.h
@@ -781,6 +781,8 @@
/* note name must be "LINUX". */
#define NT_RISCV_CSR 0x900 /* RISC-V Control and Status Registers */
/* note name must be "LINUX". */
+#define NT_RISCV_HW_BREAK 0x903 /* RISC-V hardware breakpoint registers */
+ /* note name must be "LINUX". */
#define NT_SIGINFO 0x53494749 /* Fields of siginfo_t. */
#define NT_FILE 0x46494c45 /* Description of mapped files. */
--
2.34.1
next prev parent reply other threads:[~2026-04-09 2:46 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-09 2:45 [PATCH v2 0/2] RISC-V hardware breakpoint/watchpoint support Zane Leung
2026-04-09 2:45 ` Zane Leung [this message]
2026-04-09 2:45 ` [PATCH v2 2/2] gdbserver: riscv: Add support for hardware breakpoints/watchpoints Zane Leung
2026-04-09 4:49 ` liangzhen
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260409024546.350958-2-liangzhen@linux.spacemit.com \
--to=liangzhen@linux.spacemit.com \
--cc=gdb-patches@sourceware.org \
--cc=zhuangqiubin@linux.spacemit.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox