LoongArch: ptrace: Add hardware single step support
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)
Use the generic ptrace_resume code for PTRACE_SYSCALL, PTRACE_CONT,
PTRACE_KILL and PTRACE_SINGLESTEP handling. This implies defining
arch_has_single_step() and implementing the user_enable_single_step()
and user_disable_single_step() functions.

LoongArch cannot do hardware single-stepping per se, the hardware
single-stepping it is achieved by configuring the instruction fetch
watchpoints (FWPS) and specifies that the next instruction must trigger
the watch exception by setting the mask bit. In some scenarios
CSR.FWPS.Skip is used to ignore the next hit result, avoid endless
repeated triggering of the same watchpoint without canceling it.

Signed-off-by: Qing Zhang <zhangqing@loongson.cn>
Signed-off-by: Huacai Chen <chenhuacai@loongson.cn>
arch/loongarch/include/asm/inst.h
arch/loongarch/include/asm/loongarch.h
arch/loongarch/include/asm/processor.h
arch/loongarch/include/asm/ptrace.h
arch/loongarch/kernel/hw_breakpoint.c
arch/loongarch/kernel/ptrace.c
arch/loongarch/kernel/traps.c

index 7eedd83fd0d72127fc570cd87dc759f1d8b6b12f..36cc5eb4f852b017051febc98e3379b19655581a 100644 (file)
@@ -351,6 +351,44 @@ static inline bool is_stack_alloc_ins(union loongarch_instruction *ip)
                is_imm12_negative(ip->reg2i12_format.immediate);
 }
 
+static inline bool is_self_loop_ins(union loongarch_instruction *ip, struct pt_regs *regs)
+{
+       switch (ip->reg0i26_format.opcode) {
+       case b_op:
+       case bl_op:
+               if (ip->reg0i26_format.immediate_l == 0
+                   && ip->reg0i26_format.immediate_h == 0)
+                       return true;
+       }
+
+       switch (ip->reg1i21_format.opcode) {
+       case beqz_op:
+       case bnez_op:
+       case bceqz_op:
+               if (ip->reg1i21_format.immediate_l == 0
+                   && ip->reg1i21_format.immediate_h == 0)
+                       return true;
+       }
+
+       switch (ip->reg2i16_format.opcode) {
+       case beq_op:
+       case bne_op:
+       case blt_op:
+       case bge_op:
+       case bltu_op:
+       case bgeu_op:
+               if (ip->reg2i16_format.immediate == 0)
+                       return true;
+               break;
+       case jirl_op:
+               if (regs->regs[ip->reg2i16_format.rj] +
+                   ((unsigned long)ip->reg2i16_format.immediate << 2) == (unsigned long)ip)
+                       return true;
+       }
+
+       return false;
+}
+
 int larch_insn_read(void *addr, u32 *insnp);
 int larch_insn_write(void *addr, u32 insn);
 int larch_insn_patch_text(void *addr, u32 insn);
index e9aed583a0643a08396efa314a6e053fd1a5e3f5..65b7dcdea16d0f4f59bfed5cbee2444767327a6c 100644 (file)
@@ -1055,6 +1055,9 @@ static __always_inline void iocsr_write64(u64 val, u32 reg)
 #define LOONGARCH_CSR_DERA             0x501   /* debug era */
 #define LOONGARCH_CSR_DESAVE           0x502   /* debug save */
 
+#define CSR_FWPC_SKIP_SHIFT            16
+#define CSR_FWPC_SKIP                  (_ULCAST_(1) << CSR_FWPC_SKIP_SHIFT)
+
 /*
  * CSR_ECFG IM
  */
index 5edf78c3a5b4c0481a47150ac34a16be25c4b413..636e1c66398c17c7bfb930b5b225bd241f831a45 100644 (file)
@@ -125,6 +125,7 @@ struct thread_struct {
        /* Other stuff associated with the thread. */
        unsigned long trap_nr;
        unsigned long error_code;
+       unsigned long single_step; /* Used by PTRACE_SINGLESTEP */
        struct loongarch_vdso_info *vdso;
 
        /*
index 5d3872131866304af82787a4ba56a92b1e098716..6eab4393b108fd71704f9156961725b62dacddcb 100644 (file)
@@ -183,4 +183,8 @@ static inline void user_stack_pointer_set(struct pt_regs *regs,
        regs->regs[3] = val;
 }
 
+#ifdef CONFIG_HAVE_HW_BREAKPOINT
+#define arch_has_single_step()         (1)
+#endif
+
 #endif /* _ASM_PTRACE_H */
index b88efe6c672eb6539784eb5d9c5db807a8b34ae9..2406c95b34cc4f0023f1db8755e60ff7ea0cc18a 100644 (file)
@@ -153,6 +153,22 @@ void ptrace_hw_copy_thread(struct task_struct *tsk)
  */
 void flush_ptrace_hw_breakpoint(struct task_struct *tsk)
 {
+       int i;
+       struct thread_struct *t = &tsk->thread;
+
+       for (i = 0; i < LOONGARCH_MAX_BRP; i++) {
+               if (t->hbp_break[i]) {
+                       unregister_hw_breakpoint(t->hbp_break[i]);
+                       t->hbp_break[i] = NULL;
+               }
+       }
+
+       for (i = 0; i < LOONGARCH_MAX_WRP; i++) {
+               if (t->hbp_watch[i]) {
+                       unregister_hw_breakpoint(t->hbp_watch[i]);
+                       t->hbp_watch[i] = NULL;
+               }
+       }
 }
 
 static int hw_breakpoint_control(struct perf_event *bp,
@@ -501,12 +517,21 @@ arch_initcall(arch_hw_breakpoint_init);
 
 void hw_breakpoint_thread_switch(struct task_struct *next)
 {
+       u64 addr, mask;
        struct pt_regs *regs = task_pt_regs(next);
 
-       /* Update breakpoints */
-       update_bp_registers(regs, 1, 0);
-       /* Update watchpoints */
-       update_bp_registers(regs, 1, 1);
+       if (test_tsk_thread_flag(next, TIF_SINGLESTEP)) {
+               addr = read_wb_reg(CSR_CFG_ADDR, 0, 0);
+               mask = read_wb_reg(CSR_CFG_MASK, 0, 0);
+               if (!((regs->csr_era ^ addr) & ~mask))
+                       csr_write32(CSR_FWPC_SKIP, LOONGARCH_CSR_FWPS);
+               regs->csr_prmd |= CSR_PRMD_PWE;
+       } else {
+               /* Update breakpoints */
+               update_bp_registers(regs, 1, 0);
+               /* Update watchpoints */
+               update_bp_registers(regs, 1, 1);
+       }
 }
 
 void hw_breakpoint_pmu_read(struct perf_event *bp)
index 11b49139563674077adfece83d9d7de3ad21a4f7..06bceae7d1040c6cfb38fe07acea7f1f765eb1a0 100644 (file)
@@ -31,6 +31,7 @@
 #include <linux/smp.h>
 #include <linux/stddef.h>
 #include <linux/seccomp.h>
+#include <linux/thread_info.h>
 #include <linux/uaccess.h>
 
 #include <asm/byteorder.h>
@@ -41,6 +42,7 @@
 #include <asm/page.h>
 #include <asm/pgtable.h>
 #include <asm/processor.h>
+#include <asm/ptrace.h>
 #include <asm/reg.h>
 #include <asm/syscall.h>
 
@@ -833,3 +835,71 @@ long arch_ptrace(struct task_struct *child, long request,
 
        return ret;
 }
+
+#ifdef CONFIG_HAVE_HW_BREAKPOINT
+static void ptrace_triggered(struct perf_event *bp,
+                     struct perf_sample_data *data, struct pt_regs *regs)
+{
+       struct perf_event_attr attr;
+
+       attr = bp->attr;
+       attr.disabled = true;
+       modify_user_hw_breakpoint(bp, &attr);
+}
+
+static int set_single_step(struct task_struct *tsk, unsigned long addr)
+{
+       struct perf_event *bp;
+       struct perf_event_attr attr;
+       struct arch_hw_breakpoint *info;
+       struct thread_struct *thread = &tsk->thread;
+
+       bp = thread->hbp_break[0];
+       if (!bp) {
+               ptrace_breakpoint_init(&attr);
+
+               attr.bp_addr = addr;
+               attr.bp_len = HW_BREAKPOINT_LEN_8;
+               attr.bp_type = HW_BREAKPOINT_X;
+
+               bp = register_user_hw_breakpoint(&attr, ptrace_triggered,
+                                                NULL, tsk);
+               if (IS_ERR(bp))
+                       return PTR_ERR(bp);
+
+               thread->hbp_break[0] = bp;
+       } else {
+               int err;
+
+               attr = bp->attr;
+               attr.bp_addr = addr;
+
+               /* Reenable breakpoint */
+               attr.disabled = false;
+               err = modify_user_hw_breakpoint(bp, &attr);
+               if (unlikely(err))
+                       return err;
+
+               csr_write64(attr.bp_addr, LOONGARCH_CSR_IB0ADDR);
+       }
+       info = counter_arch_bp(bp);
+       info->mask = TASK_SIZE - 1;
+
+       return 0;
+}
+
+/* ptrace API */
+void user_enable_single_step(struct task_struct *task)
+{
+       struct thread_info *ti = task_thread_info(task);
+
+       set_single_step(task, task_pt_regs(task)->csr_era);
+       task->thread.single_step = task_pt_regs(task)->csr_era;
+       set_ti_thread_flag(ti, TIF_SINGLESTEP);
+}
+
+void user_disable_single_step(struct task_struct *task)
+{
+       clear_tsk_thread_flag(task, TIF_SINGLESTEP);
+}
+#endif
index ba67d787f0d77f4276d61b6010efe6cd1ce75a79..1d47747e2154a5af8075c51559dbb3de32a13762 100644 (file)
@@ -513,14 +513,49 @@ asmlinkage void noinstr do_watch(struct pt_regs *regs)
 {
        irqentry_state_t state = irqentry_enter(regs);
 
-#ifdef CONFIG_HAVE_HW_BREAKPOINT
-       breakpoint_handler(regs);
-       watchpoint_handler(regs);
-       force_sig(SIGTRAP);
-#else
+#ifndef CONFIG_HAVE_HW_BREAKPOINT
        pr_warn("Hardware watch point handler not implemented!\n");
-#endif
+#else
+       if (test_tsk_thread_flag(current, TIF_SINGLESTEP)) {
+               int llbit = (csr_read32(LOONGARCH_CSR_LLBCTL) & 0x1);
+               unsigned long pc = instruction_pointer(regs);
+               union loongarch_instruction *ip = (union loongarch_instruction *)pc;
+
+               if (llbit) {
+                       /*
+                        * When the ll-sc combo is encountered, it is regarded as an single
+                        * instruction. So don't clear llbit and reset CSR.FWPS.Skip until
+                        * the llsc execution is completed.
+                        */
+                       csr_write32(CSR_FWPC_SKIP, LOONGARCH_CSR_FWPS);
+                       csr_write32(CSR_LLBCTL_KLO, LOONGARCH_CSR_LLBCTL);
+                       goto out;
+               }
 
+               if (pc == current->thread.single_step) {
+                       /*
+                        * Certain insns are occasionally not skipped when CSR.FWPS.Skip is
+                        * set, such as fld.d/fst.d. So singlestep needs to compare whether
+                        * the csr_era is equal to the value of singlestep which last time set.
+                        */
+                       if (!is_self_loop_ins(ip, regs)) {
+                               /*
+                                * Check if the given instruction the target pc is equal to the
+                                * current pc, If yes, then we should not set the CSR.FWPS.SKIP
+                                * bit to break the original instruction stream.
+                                */
+                               csr_write32(CSR_FWPC_SKIP, LOONGARCH_CSR_FWPS);
+                               goto out;
+                       }
+               }
+       } else {
+               breakpoint_handler(regs);
+               watchpoint_handler(regs);
+       }
+
+       force_sig(SIGTRAP);
+out:
+#endif
        irqentry_exit(regs, state);
 }