LoongArch: ptrace: Expose hardware breakpoints to debuggers
authorQing Zhang <zhangqing@loongson.cn>
Sat, 25 Feb 2023 07:52:57 +0000 (15:52 +0800)
committerHuacai Chen <chenhuacai@loongson.cn>
Sat, 25 Feb 2023 14:12:17 +0000 (22:12 +0800)
Implement the regset-based ptrace interface that exposes hardware
breakpoints to user-space debuggers to query and set instruction and
data breakpoints.

Signed-off-by: Qing Zhang <zhangqing@loongson.cn>
Signed-off-by: Huacai Chen <chenhuacai@loongson.cn>
arch/loongarch/include/uapi/asm/ptrace.h
arch/loongarch/kernel/ptrace.c
include/uapi/linux/elf.h

index 083193f4a5d5d8e1771872b6f90610347f8fde01..cc48ed262021244bcbcc7e0f98b63d8dc7e4b9de 100644 (file)
@@ -46,6 +46,15 @@ struct user_fp_state {
        uint32_t    fcsr;
 };
 
+struct user_watch_state {
+       uint16_t dbg_info;
+       struct {
+               uint64_t    addr;
+               uint64_t    mask;
+               uint32_t    ctrl;
+       } dbg_regs[8];
+};
+
 #define PTRACE_SYSEMU                  0x1f
 #define PTRACE_SYSEMU_SINGLESTEP       0x20
 
index dc2b82ea894cd26c1c3d662e488145d82d1fba0d..11b49139563674077adfece83d9d7de3ad21a4f7 100644 (file)
@@ -20,7 +20,9 @@
 #include <linux/context_tracking.h>
 #include <linux/elf.h>
 #include <linux/errno.h>
+#include <linux/hw_breakpoint.h>
 #include <linux/mm.h>
+#include <linux/nospec.h>
 #include <linux/ptrace.h>
 #include <linux/regset.h>
 #include <linux/sched.h>
@@ -246,6 +248,384 @@ static int cfg_set(struct task_struct *target,
        return 0;
 }
 
+#ifdef CONFIG_HAVE_HW_BREAKPOINT
+
+/*
+ * Handle hitting a HW-breakpoint.
+ */
+static void ptrace_hbptriggered(struct perf_event *bp,
+                               struct perf_sample_data *data,
+                               struct pt_regs *regs)
+{
+       int i;
+       struct arch_hw_breakpoint *bkpt = counter_arch_bp(bp);
+
+       for (i = 0; i < LOONGARCH_MAX_BRP; ++i)
+               if (current->thread.hbp_break[i] == bp)
+                       break;
+
+       for (i = 0; i < LOONGARCH_MAX_WRP; ++i)
+               if (current->thread.hbp_watch[i] == bp)
+                       break;
+
+       force_sig_ptrace_errno_trap(i, (void __user *)bkpt->address);
+}
+
+static struct perf_event *ptrace_hbp_get_event(unsigned int note_type,
+                                              struct task_struct *tsk,
+                                              unsigned long idx)
+{
+       struct perf_event *bp;
+
+       switch (note_type) {
+       case NT_LOONGARCH_HW_BREAK:
+               if (idx >= LOONGARCH_MAX_BRP)
+                       return ERR_PTR(-EINVAL);
+               idx = array_index_nospec(idx, LOONGARCH_MAX_BRP);
+               bp = tsk->thread.hbp_break[idx];
+               break;
+       case NT_LOONGARCH_HW_WATCH:
+               if (idx >= LOONGARCH_MAX_WRP)
+                       return ERR_PTR(-EINVAL);
+               idx = array_index_nospec(idx, LOONGARCH_MAX_WRP);
+               bp = tsk->thread.hbp_watch[idx];
+               break;
+       }
+
+       return bp;
+}
+
+static int ptrace_hbp_set_event(unsigned int note_type,
+                               struct task_struct *tsk,
+                               unsigned long idx,
+                               struct perf_event *bp)
+{
+       switch (note_type) {
+       case NT_LOONGARCH_HW_BREAK:
+               if (idx >= LOONGARCH_MAX_BRP)
+                       return -EINVAL;
+               idx = array_index_nospec(idx, LOONGARCH_MAX_BRP);
+               tsk->thread.hbp_break[idx] = bp;
+               break;
+       case NT_LOONGARCH_HW_WATCH:
+               if (idx >= LOONGARCH_MAX_WRP)
+                       return -EINVAL;
+               idx = array_index_nospec(idx, LOONGARCH_MAX_WRP);
+               tsk->thread.hbp_watch[idx] = bp;
+               break;
+       }
+
+       return 0;
+}
+
+static struct perf_event *ptrace_hbp_create(unsigned int note_type,
+                                           struct task_struct *tsk,
+                                           unsigned long idx)
+{
+       int err, type;
+       struct perf_event *bp;
+       struct perf_event_attr attr;
+
+       switch (note_type) {
+       case NT_LOONGARCH_HW_BREAK:
+               type = HW_BREAKPOINT_X;
+               break;
+       case NT_LOONGARCH_HW_WATCH:
+               type = HW_BREAKPOINT_RW;
+               break;
+       default:
+               return ERR_PTR(-EINVAL);
+       }
+
+       ptrace_breakpoint_init(&attr);
+
+       /*
+        * Initialise fields to sane defaults
+        * (i.e. values that will pass validation).
+        */
+       attr.bp_addr    = 0;
+       attr.bp_len     = HW_BREAKPOINT_LEN_4;
+       attr.bp_type    = type;
+       attr.disabled   = 1;
+
+       bp = register_user_hw_breakpoint(&attr, ptrace_hbptriggered, NULL, tsk);
+       if (IS_ERR(bp))
+               return bp;
+
+       err = ptrace_hbp_set_event(note_type, tsk, idx, bp);
+       if (err)
+               return ERR_PTR(err);
+
+       return bp;
+}
+
+static int ptrace_hbp_fill_attr_ctrl(unsigned int note_type,
+                                    struct arch_hw_breakpoint_ctrl ctrl,
+                                    struct perf_event_attr *attr)
+{
+       int err, len, type, offset;
+
+       err = arch_bp_generic_fields(ctrl, &len, &type, &offset);
+       if (err)
+               return err;
+
+       switch (note_type) {
+       case NT_LOONGARCH_HW_BREAK:
+               if ((type & HW_BREAKPOINT_X) != type)
+                       return -EINVAL;
+               break;
+       case NT_LOONGARCH_HW_WATCH:
+               if ((type & HW_BREAKPOINT_RW) != type)
+                       return -EINVAL;
+               break;
+       default:
+               return -EINVAL;
+       }
+
+       attr->bp_len    = len;
+       attr->bp_type   = type;
+       attr->bp_addr   += offset;
+
+       return 0;
+}
+
+static int ptrace_hbp_get_resource_info(unsigned int note_type, u16 *info)
+{
+       u8 num;
+       u16 reg = 0;
+
+       switch (note_type) {
+       case NT_LOONGARCH_HW_BREAK:
+               num = hw_breakpoint_slots(TYPE_INST);
+               break;
+       case NT_LOONGARCH_HW_WATCH:
+               num = hw_breakpoint_slots(TYPE_DATA);
+               break;
+       default:
+               return -EINVAL;
+       }
+
+       *info = reg | num;
+
+       return 0;
+}
+
+static struct perf_event *ptrace_hbp_get_initialised_bp(unsigned int note_type,
+                                                       struct task_struct *tsk,
+                                                       unsigned long idx)
+{
+       struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx);
+
+       if (!bp)
+               bp = ptrace_hbp_create(note_type, tsk, idx);
+
+       return bp;
+}
+
+static int ptrace_hbp_get_ctrl(unsigned int note_type,
+                              struct task_struct *tsk,
+                              unsigned long idx, u32 *ctrl)
+{
+       struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx);
+
+       if (IS_ERR(bp))
+               return PTR_ERR(bp);
+
+       *ctrl = bp ? encode_ctrl_reg(counter_arch_bp(bp)->ctrl) : 0;
+
+       return 0;
+}
+
+static int ptrace_hbp_get_mask(unsigned int note_type,
+                              struct task_struct *tsk,
+                              unsigned long idx, u64 *mask)
+{
+       struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx);
+
+       if (IS_ERR(bp))
+               return PTR_ERR(bp);
+
+       *mask = bp ? counter_arch_bp(bp)->mask : 0;
+
+       return 0;
+}
+
+static int ptrace_hbp_get_addr(unsigned int note_type,
+                              struct task_struct *tsk,
+                              unsigned long idx, u64 *addr)
+{
+       struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx);
+
+       if (IS_ERR(bp))
+               return PTR_ERR(bp);
+
+       *addr = bp ? counter_arch_bp(bp)->address : 0;
+
+       return 0;
+}
+
+static int ptrace_hbp_set_ctrl(unsigned int note_type,
+                              struct task_struct *tsk,
+                              unsigned long idx, u32 uctrl)
+{
+       int err;
+       struct perf_event *bp;
+       struct perf_event_attr attr;
+       struct arch_hw_breakpoint_ctrl ctrl;
+
+       bp = ptrace_hbp_get_initialised_bp(note_type, tsk, idx);
+       if (IS_ERR(bp))
+               return PTR_ERR(bp);
+
+       attr = bp->attr;
+       decode_ctrl_reg(uctrl, &ctrl);
+       err = ptrace_hbp_fill_attr_ctrl(note_type, ctrl, &attr);
+       if (err)
+               return err;
+
+       return modify_user_hw_breakpoint(bp, &attr);
+}
+
+static int ptrace_hbp_set_mask(unsigned int note_type,
+                              struct task_struct *tsk,
+                              unsigned long idx, u64 mask)
+{
+       struct perf_event *bp;
+       struct perf_event_attr attr;
+       struct arch_hw_breakpoint *info;
+
+       bp = ptrace_hbp_get_initialised_bp(note_type, tsk, idx);
+       if (IS_ERR(bp))
+               return PTR_ERR(bp);
+
+       attr = bp->attr;
+       info = counter_arch_bp(bp);
+       info->mask = mask;
+
+       return modify_user_hw_breakpoint(bp, &attr);
+}
+
+static int ptrace_hbp_set_addr(unsigned int note_type,
+                              struct task_struct *tsk,
+                              unsigned long idx, u64 addr)
+{
+       struct perf_event *bp;
+       struct perf_event_attr attr;
+
+       bp = ptrace_hbp_get_initialised_bp(note_type, tsk, idx);
+       if (IS_ERR(bp))
+               return PTR_ERR(bp);
+
+       attr = bp->attr;
+       attr.bp_addr = addr;
+
+       return modify_user_hw_breakpoint(bp, &attr);
+}
+
+#define PTRACE_HBP_CTRL_SZ     sizeof(u32)
+#define PTRACE_HBP_ADDR_SZ     sizeof(u64)
+#define PTRACE_HBP_MASK_SZ     sizeof(u64)
+
+static int hw_break_get(struct task_struct *target,
+                       const struct user_regset *regset,
+                       struct membuf to)
+{
+       u16 info;
+       u32 ctrl;
+       u64 addr, mask;
+       int ret, idx = 0;
+       unsigned int note_type = regset->core_note_type;
+
+       /* Resource info */
+       ret = ptrace_hbp_get_resource_info(note_type, &info);
+       if (ret)
+               return ret;
+
+       membuf_write(&to, &info, sizeof(info));
+
+       /* (address, ctrl) registers */
+       while (to.left) {
+               ret = ptrace_hbp_get_addr(note_type, target, idx, &addr);
+               if (ret)
+                       return ret;
+
+               ret = ptrace_hbp_get_mask(note_type, target, idx, &mask);
+               if (ret)
+                       return ret;
+
+               ret = ptrace_hbp_get_ctrl(note_type, target, idx, &ctrl);
+               if (ret)
+                       return ret;
+
+               membuf_store(&to, addr);
+               membuf_store(&to, mask);
+               membuf_store(&to, ctrl);
+               idx++;
+       }
+
+       return 0;
+}
+
+static int hw_break_set(struct task_struct *target,
+                       const struct user_regset *regset,
+                       unsigned int pos, unsigned int count,
+                       const void *kbuf, const void __user *ubuf)
+{
+       u32 ctrl;
+       u64 addr, mask;
+       int ret, idx = 0, offset, limit;
+       unsigned int note_type = regset->core_note_type;
+
+       /* Resource info */
+       offset = offsetof(struct user_watch_state, dbg_regs);
+       user_regset_copyin_ignore(&pos, &count, &kbuf, &ubuf, 0, offset);
+
+       /* (address, ctrl) registers */
+       limit = regset->n * regset->size;
+       while (count && offset < limit) {
+               if (count < PTRACE_HBP_ADDR_SZ)
+                       return -EINVAL;
+
+               ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &addr,
+                                        offset, offset + PTRACE_HBP_ADDR_SZ);
+               if (ret)
+                       return ret;
+
+               ret = ptrace_hbp_set_addr(note_type, target, idx, addr);
+               if (ret)
+                       return ret;
+               offset += PTRACE_HBP_ADDR_SZ;
+
+               if (!count)
+                       break;
+
+               ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &mask,
+                                        offset, offset + PTRACE_HBP_ADDR_SZ);
+               if (ret)
+                       return ret;
+
+               ret = ptrace_hbp_set_mask(note_type, target, idx, mask);
+               if (ret)
+                       return ret;
+               offset += PTRACE_HBP_MASK_SZ;
+
+               ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &mask,
+                                        offset, offset + PTRACE_HBP_MASK_SZ);
+               if (ret)
+                       return ret;
+
+               ret = ptrace_hbp_set_ctrl(note_type, target, idx, ctrl);
+               if (ret)
+                       return ret;
+               offset += PTRACE_HBP_CTRL_SZ;
+               idx++;
+       }
+
+       return 0;
+}
+
+#endif
+
 struct pt_regs_offset {
        const char *name;
        int offset;
@@ -319,6 +699,10 @@ enum loongarch_regset {
        REGSET_GPR,
        REGSET_FPR,
        REGSET_CPUCFG,
+#ifdef CONFIG_HAVE_HW_BREAKPOINT
+       REGSET_HW_BREAK,
+       REGSET_HW_WATCH,
+#endif
 };
 
 static const struct user_regset loongarch64_regsets[] = {
@@ -346,6 +730,24 @@ static const struct user_regset loongarch64_regsets[] = {
                .regset_get     = cfg_get,
                .set            = cfg_set,
        },
+#ifdef CONFIG_HAVE_HW_BREAKPOINT
+       [REGSET_HW_BREAK] = {
+               .core_note_type = NT_LOONGARCH_HW_BREAK,
+               .n = sizeof(struct user_watch_state) / sizeof(u32),
+               .size = sizeof(u32),
+               .align = sizeof(u32),
+               .regset_get = hw_break_get,
+               .set = hw_break_set,
+       },
+       [REGSET_HW_WATCH] = {
+               .core_note_type = NT_LOONGARCH_HW_WATCH,
+               .n = sizeof(struct user_watch_state) / sizeof(u32),
+               .size = sizeof(u32),
+               .align = sizeof(u32),
+               .regset_get = hw_break_get,
+               .set = hw_break_set,
+       },
+#endif
 };
 
 static const struct user_regset_view user_loongarch64_view = {
index 4c6a8fa5e7ed624a61412160b7e996bbe615b3e3..9db473d9405d49a015a0b756101a6069a2dafe95 100644 (file)
@@ -444,6 +444,8 @@ typedef struct elf64_shdr {
 #define NT_LOONGARCH_LSX       0xa02   /* LoongArch Loongson SIMD Extension registers */
 #define NT_LOONGARCH_LASX      0xa03   /* LoongArch Loongson Advanced SIMD Extension registers */
 #define NT_LOONGARCH_LBT       0xa04   /* LoongArch Loongson Binary Translation registers */
+#define NT_LOONGARCH_HW_BREAK  0xa05   /* LoongArch hardware breakpoint registers */
+#define NT_LOONGARCH_HW_WATCH  0xa06   /* LoongArch hardware watchpoint registers */
 
 /* Note types with note name "GNU" */
 #define NT_GNU_PROPERTY_TYPE_0 5