Use the generic ptrace_resume code for PTRACE_SYSCALL, PTRACE_CONT,
PTRACE_KILL and PTRACE_SINGLESTEP. This implies defining
arch_has_single_step in and implementing the
user_enable_single_step and user_disable_single_step functions.
LoongArch cannot do hardware single-stepping per se, the hardware single-step
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 the watchpoint.
Signed-off-by: Qing Zhang <zhangqing@loongson.cn>
---
arch/loongarch/include/asm/inst.h | 38 ++++++++++++++
arch/loongarch/include/asm/loongarch.h | 3 ++
arch/loongarch/include/asm/processor.h | 3 ++
arch/loongarch/include/asm/ptrace.h | 2 +
arch/loongarch/kernel/hw_breakpoint.c | 35 +++++++++++--
arch/loongarch/kernel/ptrace.c | 68 ++++++++++++++++++++++++++
arch/loongarch/kernel/traps.c | 37 ++++++++++++--
7 files changed, 178 insertions(+), 8 deletions(-)
@@ -368,6 +368,44 @@ static inline bool is_stack_alloc_ins(union loongarch_instruction *ip)
is_imm12_negative(ip->reg2i12_format.immediate);
}
+static inline bool branch_ins_target_pc(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 false;
+ }
+
+ 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 false;
+ }
+
+ switch (ip->reg2i16_format.opcode) {
+ case jirl_op:
+ if (regs->regs[ip->reg2i16_format.rj] + ((unsigned long)ip->reg2i16_format.immediate << 2)
+ == (unsigned long)ip)
+ return false;
+ break;
+ 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 false;
+ }
+
+ return true;
+}
+
void simu_pc(struct pt_regs *regs, union loongarch_instruction insn);
void simu_branch(struct pt_regs *regs, union loongarch_instruction insn);
@@ -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
*/
@@ -131,6 +131,9 @@ struct thread_struct {
struct perf_event *hbp_break[LOONGARCH_MAX_BRP];
struct perf_event *hbp_watch[LOONGARCH_MAX_WRP];
+ /* Used by PTRACE_SINGLESTEP */
+ unsigned long single_step;
+
/*
* FPU & vector registers, must be at last because
* they are conditionally copied at fork().
@@ -150,4 +150,6 @@ static inline void user_stack_pointer_set(struct pt_regs *regs,
regs->regs[3] = val;
}
+#define arch_has_single_step() (1)
+
#endif /* _ASM_PTRACE_H */
@@ -153,6 +153,22 @@ static int hw_breakpoint_slot_setup(struct perf_event **slots, int max_slots,
*/
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;
+ }
+ }
}
void ptrace_hw_copy_thread(struct task_struct *tsk)
@@ -498,11 +514,20 @@ arch_initcall(arch_hw_breakpoint_init);
void hw_breakpoint_thread_switch(struct task_struct *next)
{
struct pt_regs *regs = task_pt_regs(next);
-
- /* Update breakpoints */
- update_bp_registers(regs, 1, 0);
- /* Update watchpoints */
- update_bp_registers(regs, 1, 1);
+ u64 addr, mask;
+
+ if (test_bit(TIF_SINGLESTEP, &task_thread_info(next)->flags)) {
+ addr = read_wb_reg(CSR_CFG_ADDR, 0, 0);
+ mask = read_wb_reg(CSR_CFG_MASK, 0, 0);
+ if ((task_pt_regs(next)->csr_era & ~mask) == (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)
@@ -20,6 +20,7 @@
#include <linux/context_tracking.h>
#include <linux/elf.h>
#include <linux/errno.h>
+#include <linux/hw_breakpoint.h>
#include <linux/mm.h>
#include <linux/ptrace.h>
#include <linux/regset.h>
@@ -30,6 +31,7 @@
#include <linux/stddef.h>
#include <linux/seccomp.h>
#include <linux/uaccess.h>
+#include <linux/thread_info.h>
#include <asm/byteorder.h>
#include <asm/cpu.h>
@@ -39,6 +41,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>
@@ -541,3 +544,68 @@ long arch_ptrace(struct task_struct *child, long request,
return ret;
}
+
+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 thread_struct *thread = &tsk->thread;
+ struct perf_event *bp;
+ struct perf_event_attr attr;
+ struct arch_hw_breakpoint *info;
+
+ 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);
+}
@@ -511,9 +511,40 @@ asmlinkage void noinstr do_watch(struct pt_regs *regs)
#ifdef CONFIG_HAVE_HW_BREAKPOINT
irqentry_state_t state = irqentry_enter(regs);
- breakpoint_handler(regs);
- watchpoint_handler(regs);
- force_sig(SIGTRAP);
+ if (test_tsk_thread_flag(current, TIF_SINGLESTEP)) {
+ int llbit = (csr_read32(LOONGARCH_CSR_LLBCTL) & 0x1);
+ unsigned long pc = regs->csr_era;
+ 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);
+ } else if (pc == current->thread.single_step) {
+ /*
+ * Maybe some hardware Bug caused that 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 (branch_ins_target_pc(ip, regs))
+ /* which is to 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);
+ } else {
+ force_sig(SIGTRAP);
+ }
+ } else {
+ breakpoint_handler(regs);
+ watchpoint_handler(regs);
+ force_sig(SIGTRAP);
+ }
irqentry_exit(regs, state);
#endif