gdb: riscv: Add support for hardware breakpoints/watchpoints
Checks
| Context |
Check |
Description |
| linaro-tcwg-bot/tcwg_gdb_build--master-arm |
success
|
Build passed
|
| linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 |
success
|
Build passed
|
| linaro-tcwg-bot/tcwg_gdb_check--master-arm |
success
|
Test passed
|
| linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 |
success
|
Test passed
|
Commit Message
From: liangzhen <zhen.liang@spacemit.com>
Implement hardware breakpoint and watchpoint support for RISC-V
architecture.
For this patch you will need Linux kernel support. Base on Himanshu's work:
https://lore.kernel.org/all/20260223044918.1359983-2-himanshu.chauhan@oss.qualcomm.com
---
gdb/Makefile.in | 3 +
gdb/configure.nat | 3 +-
gdb/nat/riscv-hw-point.c | 217 +++++++++++++++++++++++
gdb/nat/riscv-hw-point.h | 79 +++++++++
gdb/nat/riscv-linux-hw-point.c | 232 +++++++++++++++++++++++++
gdb/nat/riscv-linux-hw-point.h | 128 ++++++++++++++
gdb/nat/riscv-linux.c | 83 +++++++++
gdb/nat/riscv-linux.h | 40 +++++
gdb/riscv-linux-nat.c | 305 +++++++++++++++++++++++++++++++++
include/elf/common.h | 2 +
10 files changed, 1091 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
Comments
Mark,
Thanks for working on this. I took an initial scan through, but not in
great detail. There's a bunch of code style issues that will need to be
addressed, but nothing too huge I think.
I don't have a setup that would allow me to test this unfortunately, I
should probably revisit setting up QEMU for this sort of patch.
Mark Zhuang <zhuangqiubin@linux.spacemit.com> writes:
> From: liangzhen <zhen.liang@spacemit.com>
>
> Implement hardware breakpoint and watchpoint support for RISC-V
> architecture.
There's no gdbserver support in here yet. I don't think that's a
requirement for getting this work merged, and it looks like you've
thought about this given that you've placed some code into gdb/nat/, but
it would be nice to see this support mentioned here, even if it is just
a single sentence to acknowledge the lack of support at this point.
>
> For this patch you will need Linux kernel support. Base on Himanshu's work:
> https://lore.kernel.org/all/20260223044918.1359983-2-himanshu.chauhan@oss.qualcomm.com
> ---
> gdb/Makefile.in | 3 +
> gdb/configure.nat | 3 +-
> gdb/nat/riscv-hw-point.c | 217 +++++++++++++++++++++++
> gdb/nat/riscv-hw-point.h | 79 +++++++++
> gdb/nat/riscv-linux-hw-point.c | 232 +++++++++++++++++++++++++
> gdb/nat/riscv-linux-hw-point.h | 128 ++++++++++++++
> gdb/nat/riscv-linux.c | 83 +++++++++
> gdb/nat/riscv-linux.h | 40 +++++
> gdb/riscv-linux-nat.c | 305 +++++++++++++++++++++++++++++++++
> include/elf/common.h | 2 +
> 10 files changed, 1091 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..6cc21bb23c3
> --- /dev/null
> +++ b/gdb/nat/riscv-hw-point.c
> @@ -0,0 +1,217 @@
> +/* Copyright (C) 2026 Free Software Foundation, Inc.
> + Contributed by Spacemit Ltd.
These "Contributed by ..." lines need to be removed. Please see point
#8 on: https://sourceware.org/gdb/wiki/ContributionChecklist. Your
contributions are still tracked via the git commit logs.
> +
> + 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 and CTRL, in the process' arch-specific data area *STATE. */
It would be nice if all the arguments could be mentioned and described
in the header, e.g. LEN is missing here. Also the return value should
be documented. In this case you only return -1 or 0, I wonder if
returning a bool would make more sense. i.e. "Return true if the
insertion is successful, otherwise, return false. "?
I think this comment applies to lots of the functions in this series.
> +
> +static int
> +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. */
Lots of comments in this series are missing the double space after a
'.'. I'll not point them all out, but there's lots.
> + 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)
GDB/GNU style is to line wrap before the operator, so this should be:
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 -1;
> +
> + /* 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 0;
> +}
> +
> +/* Record the removal of one breakpoint/watchpoint, as represented by
> + ADDR and CTRL, in the process' arch-specific data area *STATE. */
> +
> +static int
> +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 and CTRL. */
> + 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 -1;
> +
> + /* 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 0;
> +}
> +
> +int
> +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)
This function is missing its comment.
> +{
> + 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);
> +}
> +
> +/* See nat/riscv-hw-point.h. */
> +
> +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)
> +{
> + int i;
This can be moved down into the for loop line.
> +
> + debug_printf ("%s", func);
> + if (addr || len)
GDB/GNU style is not to treat non-bool as bool, so this should be:
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 (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;
> +}
> diff --git a/gdb/nat/riscv-hw-point.h b/gdb/nat/riscv-hw-point.h
> new file mode 100644
> index 00000000000..d13c47b5851
> --- /dev/null
> +++ b/gdb/nat/riscv-hw-point.h
> @@ -0,0 +1,79 @@
> +/* Copyright (C) 2024 Free Software Foundation, Inc.
> + Contributed by Spacemit Ltd.
> +
> + 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_ctrl_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);
> +
> +
> +int 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);
Missing a comment.
> +
> +/* 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);
> +
> +#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..2c152f53f1c
> --- /dev/null
> +++ b/gdb/nat/riscv-linux-hw-point.c
> @@ -0,0 +1,232 @@
> +/* Copyright (C) 2026 Free Software Foundation, Inc.
> + Contributed by Spacemit Ltd.
> +
> + 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"
> +
> +/* Hash table storing per-process data. We don't bind this to a
> + per-inferior registry because of targets like x86 GNU/Linux that
> + need to keep track of processes that aren't bound to any inferior
> + (e.g., fork children, checkpoints). */
I see this comment has been copied and paste from target to target :)
Maybe you could just delete the 'x86' as the text still makes sense, and
I think is less confusing.
> +
> +static std::unordered_map<pid_t, riscv_debug_reg_state>
> +riscv_debug_process_state;
Indent this second line with a tab.
> +
> +/* See riscv-linux-hw-point.h */
This comment seems orphaned.
> +
> +/* 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));
> + }
Single statement bodies can drop the '{ ... }'.
> +
> + 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);
> + });
> +}
> +
> +/* */
Empty comment.
Thanks,
Andrew
> +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;
> + }
> +}
> +
> +/* 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. */
> +
> +void
> +riscv_remove_debug_reg_state (pid_t pid)
> +{
> + riscv_debug_process_state.erase (pid);
> +}
> diff --git a/gdb/nat/riscv-linux-hw-point.h b/gdb/nat/riscv-linux-hw-point.h
> new file mode 100644
> index 00000000000..49049f000c0
> --- /dev/null
> +++ b/gdb/nat/riscv-linux-hw-point.h
> @@ -0,0 +1,128 @@
> +/* Copyright (C) 2026 Free Software Foundation, Inc.
> + Contributed by Spacemit Ltd.
> +
> + 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);
> +
> +/* Remove any existing per-process debug state for process PID. */
> +
> +void riscv_remove_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..e1fef39ff15
> --- /dev/null
> +++ b/gdb/nat/riscv-linux.c
> @@ -0,0 +1,83 @@
> +/* Copyright (C) 2026 Free Software Foundation, Inc.
> + Contributed by Spacemit Ltd.
> +
> + 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..7904bc0283e
> --- /dev/null
> +++ b/gdb/nat/riscv-linux.h
> @@ -0,0 +1,40 @@
> +/* Copyright (C) 2026 Free Software Foundation, Inc.
> + Contributed by Spacemit Ltd.
> +
> + 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..b0164284fd8 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>
>
> @@ -42,6 +46,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 +362,261 @@ 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);
> +
> + 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 inserted, -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);
> +
> + 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);
> +
> + 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 inserted, -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);
> +
> + 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 ()
> +{
> + int i;
> + siginfo_t siginfo;
> + CORE_ADDR addr_trap;
> + struct riscv_debug_reg_state *state;
> + std::vector<CORE_ADDR> matching_addresses;
> +
> + 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 {};
> +
> + addr_trap = (CORE_ADDR) siginfo.si_addr;
> + state = riscv_get_debug_reg_state (inferior_ptid.pid ());
> +
> + /* Check if the address matches any watched address. */
> + for (i = 0; i < riscv_num_hwbp_regs; i++)
> + {
> + 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)
> + {
> + matching_addresses.push_back (addr);
> + }
> + }
> + return matching_addresses;
> +}
> +
> +/* 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 +624,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
Hi Andrew,
Thank you for your comment. I will address code style issues and provide gdbserver support in the next revision.
Thanks,
liangzhen
On 4/7/2026 9:11 PM, Andrew Burgess wrote:
> Mark,
>
> Thanks for working on this. I took an initial scan through, but not in
> great detail. There's a bunch of code style issues that will need to be
> addressed, but nothing too huge I think.
>
> I don't have a setup that would allow me to test this unfortunately, I
> should probably revisit setting up QEMU for this sort of patch.
>
> Mark Zhuang <zhuangqiubin@linux.spacemit.com> writes:
>
>> From: liangzhen <zhen.liang@spacemit.com>
>>
>> Implement hardware breakpoint and watchpoint support for RISC-V
>> architecture.
> There's no gdbserver support in here yet. I don't think that's a
> requirement for getting this work merged, and it looks like you've
> thought about this given that you've placed some code into gdb/nat/, but
> it would be nice to see this support mentioned here, even if it is just
> a single sentence to acknowledge the lack of support at this point.
>
>> For this patch you will need Linux kernel support. Base on Himanshu's work:
>> https://lore.kernel.org/all/20260223044918.1359983-2-himanshu.chauhan@oss.qualcomm.com
>> ---
>> gdb/Makefile.in | 3 +
>> gdb/configure.nat | 3 +-
>> gdb/nat/riscv-hw-point.c | 217 +++++++++++++++++++++++
>> gdb/nat/riscv-hw-point.h | 79 +++++++++
>> gdb/nat/riscv-linux-hw-point.c | 232 +++++++++++++++++++++++++
>> gdb/nat/riscv-linux-hw-point.h | 128 ++++++++++++++
>> gdb/nat/riscv-linux.c | 83 +++++++++
>> gdb/nat/riscv-linux.h | 40 +++++
>> gdb/riscv-linux-nat.c | 305 +++++++++++++++++++++++++++++++++
>> include/elf/common.h | 2 +
>> 10 files changed, 1091 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..6cc21bb23c3
>> --- /dev/null
>> +++ b/gdb/nat/riscv-hw-point.c
>> @@ -0,0 +1,217 @@
>> +/* Copyright (C) 2026 Free Software Foundation, Inc.
>> + Contributed by Spacemit Ltd.
> These "Contributed by ..." lines need to be removed. Please see point
> #8 on: https://sourceware.org/gdb/wiki/ContributionChecklist. Your
> contributions are still tracked via the git commit logs.
>
>> +
>> + 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 and CTRL, in the process' arch-specific data area *STATE. */
> It would be nice if all the arguments could be mentioned and described
> in the header, e.g. LEN is missing here. Also the return value should
> be documented. In this case you only return -1 or 0, I wonder if
> returning a bool would make more sense. i.e. "Return true if the
> insertion is successful, otherwise, return false. "?
>
> I think this comment applies to lots of the functions in this series.
>
>> +
>> +static int
>> +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. */
> Lots of comments in this series are missing the double space after a
> '.'. I'll not point them all out, but there's lots.
>
>> + 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)
> GDB/GNU style is to line wrap before the operator, so this should be:
>
> 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 -1;
>> +
>> + /* 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 0;
>> +}
>> +
>> +/* Record the removal of one breakpoint/watchpoint, as represented by
>> + ADDR and CTRL, in the process' arch-specific data area *STATE. */
>> +
>> +static int
>> +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 and CTRL. */
>> + 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 -1;
>> +
>> + /* 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 0;
>> +}
>> +
>> +int
>> +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)
> This function is missing its comment.
>
>> +{
>> + 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);
>> +}
>> +
>> +/* See nat/riscv-hw-point.h. */
>> +
>> +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)
>> +{
>> + int i;
> This can be moved down into the for loop line.
>
>> +
>> + debug_printf ("%s", func);
>> + if (addr || len)
> GDB/GNU style is not to treat non-bool as bool, so this should be:
>
> 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 (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;
>> +}
>> diff --git a/gdb/nat/riscv-hw-point.h b/gdb/nat/riscv-hw-point.h
>> new file mode 100644
>> index 00000000000..d13c47b5851
>> --- /dev/null
>> +++ b/gdb/nat/riscv-hw-point.h
>> @@ -0,0 +1,79 @@
>> +/* Copyright (C) 2024 Free Software Foundation, Inc.
>> + Contributed by Spacemit Ltd.
>> +
>> + 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_ctrl_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);
>> +
>> +
>> +int 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);
> Missing a comment.
>
>> +
>> +/* 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);
>> +
>> +#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..2c152f53f1c
>> --- /dev/null
>> +++ b/gdb/nat/riscv-linux-hw-point.c
>> @@ -0,0 +1,232 @@
>> +/* Copyright (C) 2026 Free Software Foundation, Inc.
>> + Contributed by Spacemit Ltd.
>> +
>> + 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"
>> +
>> +/* Hash table storing per-process data. We don't bind this to a
>> + per-inferior registry because of targets like x86 GNU/Linux that
>> + need to keep track of processes that aren't bound to any inferior
>> + (e.g., fork children, checkpoints). */
> I see this comment has been copied and paste from target to target :)
> Maybe you could just delete the 'x86' as the text still makes sense, and
> I think is less confusing.
>
>> +
>> +static std::unordered_map<pid_t, riscv_debug_reg_state>
>> +riscv_debug_process_state;
> Indent this second line with a tab.
>
>> +
>> +/* See riscv-linux-hw-point.h */
> This comment seems orphaned.
>
>> +
>> +/* 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));
>> + }
> Single statement bodies can drop the '{ ... }'.
>
>> +
>> + 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);
>> + });
>> +}
>> +
>> +/* */
> Empty comment.
>
> Thanks,
> Andrew
>
>
>> +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;
>> + }
>> +}
>> +
>> +/* 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. */
>> +
>> +void
>> +riscv_remove_debug_reg_state (pid_t pid)
>> +{
>> + riscv_debug_process_state.erase (pid);
>> +}
>> diff --git a/gdb/nat/riscv-linux-hw-point.h b/gdb/nat/riscv-linux-hw-point.h
>> new file mode 100644
>> index 00000000000..49049f000c0
>> --- /dev/null
>> +++ b/gdb/nat/riscv-linux-hw-point.h
>> @@ -0,0 +1,128 @@
>> +/* Copyright (C) 2026 Free Software Foundation, Inc.
>> + Contributed by Spacemit Ltd.
>> +
>> + 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);
>> +
>> +/* Remove any existing per-process debug state for process PID. */
>> +
>> +void riscv_remove_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..e1fef39ff15
>> --- /dev/null
>> +++ b/gdb/nat/riscv-linux.c
>> @@ -0,0 +1,83 @@
>> +/* Copyright (C) 2026 Free Software Foundation, Inc.
>> + Contributed by Spacemit Ltd.
>> +
>> + 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..7904bc0283e
>> --- /dev/null
>> +++ b/gdb/nat/riscv-linux.h
>> @@ -0,0 +1,40 @@
>> +/* Copyright (C) 2026 Free Software Foundation, Inc.
>> + Contributed by Spacemit Ltd.
>> +
>> + 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..b0164284fd8 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>
>>
>> @@ -42,6 +46,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 +362,261 @@ 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);
>> +
>> + 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 inserted, -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);
>> +
>> + 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);
>> +
>> + 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 inserted, -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);
>> +
>> + 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 ()
>> +{
>> + int i;
>> + siginfo_t siginfo;
>> + CORE_ADDR addr_trap;
>> + struct riscv_debug_reg_state *state;
>> + std::vector<CORE_ADDR> matching_addresses;
>> +
>> + 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 {};
>> +
>> + addr_trap = (CORE_ADDR) siginfo.si_addr;
>> + state = riscv_get_debug_reg_state (inferior_ptid.pid ());
>> +
>> + /* Check if the address matches any watched address. */
>> + for (i = 0; i < riscv_num_hwbp_regs; i++)
>> + {
>> + 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)
>> + {
>> + matching_addresses.push_back (addr);
>> + }
>> + }
>> + return matching_addresses;
>> +}
>> +
>> +/* 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 +624,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
@@ -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 \
@@ -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
new file mode 100644
@@ -0,0 +1,217 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+ Contributed by Spacemit Ltd.
+
+ 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 and CTRL, in the process' arch-specific data area *STATE. */
+
+static int
+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 -1;
+
+ /* 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 0;
+}
+
+/* Record the removal of one breakpoint/watchpoint, as represented by
+ ADDR and CTRL, in the process' arch-specific data area *STATE. */
+
+static int
+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 and CTRL. */
+ 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 -1;
+
+ /* 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 0;
+}
+
+int
+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);
+}
+
+/* See nat/riscv-hw-point.h. */
+
+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)
+{
+ int i;
+
+ debug_printf ("%s", func);
+ if (addr || len)
+ 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 (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;
+}
new file mode 100644
@@ -0,0 +1,79 @@
+/* Copyright (C) 2024 Free Software Foundation, Inc.
+ Contributed by Spacemit Ltd.
+
+ 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_ctrl_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);
+
+
+int 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);
+
+#endif /* GDB_NAT_RISCV_HW_POINT_H */
new file mode 100644
@@ -0,0 +1,232 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+ Contributed by Spacemit Ltd.
+
+ 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"
+
+/* Hash table storing per-process data. We don't bind this to a
+ per-inferior registry because of targets like x86 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;
+
+/* See riscv-linux-hw-point.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);
+ });
+}
+
+/* */
+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;
+ }
+}
+
+/* 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. */
+
+void
+riscv_remove_debug_reg_state (pid_t pid)
+{
+ riscv_debug_process_state.erase (pid);
+}
new file mode 100644
@@ -0,0 +1,128 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+ Contributed by Spacemit Ltd.
+
+ 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);
+
+/* Remove any existing per-process debug state for process PID. */
+
+void riscv_remove_debug_reg_state (pid_t pid);
+
+
+#endif /* GDB_NAT_RISCV_LINUX_HW_POINT_H */
new file mode 100644
@@ -0,0 +1,83 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+ Contributed by Spacemit Ltd.
+
+ 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);
+}
new file mode 100644
@@ -0,0 +1,40 @@
+/* Copyright (C) 2026 Free Software Foundation, Inc.
+ Contributed by Spacemit Ltd.
+
+ 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 */
@@ -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>
@@ -42,6 +46,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 +362,261 @@ 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);
+
+ 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 inserted, -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);
+
+ 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);
+
+ 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 inserted, -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);
+
+ 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 ()
+{
+ int i;
+ siginfo_t siginfo;
+ CORE_ADDR addr_trap;
+ struct riscv_debug_reg_state *state;
+ std::vector<CORE_ADDR> matching_addresses;
+
+ 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 {};
+
+ addr_trap = (CORE_ADDR) siginfo.si_addr;
+ state = riscv_get_debug_reg_state (inferior_ptid.pid ());
+
+ /* Check if the address matches any watched address. */
+ for (i = 0; i < riscv_num_hwbp_regs; i++)
+ {
+ 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)
+ {
+ matching_addresses.push_back (addr);
+ }
+ }
+ return matching_addresses;
+}
+
+/* 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 +624,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);
}
@@ -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. */