| /* | |
| * Xtensa hardware breakpoints/watchpoints handling functions | |
| * | |
| * This file is subject to the terms and conditions of the GNU General Public | |
| * License. See the file "COPYING" in the main directory of this archive | |
| * for more details. | |
| * | |
| * Copyright (C) 2016 Cadence Design Systems Inc. | |
| */ | |
| #include <linux/hw_breakpoint.h> | |
| #include <linux/log2.h> | |
| #include <linux/percpu.h> | |
| #include <linux/perf_event.h> | |
| #include <variant/core.h> | |
| /* Breakpoint currently in use for each IBREAKA. */ | |
| static DEFINE_PER_CPU(struct perf_event *, bp_on_reg[XCHAL_NUM_IBREAK]); | |
| /* Watchpoint currently in use for each DBREAKA. */ | |
| static DEFINE_PER_CPU(struct perf_event *, wp_on_reg[XCHAL_NUM_DBREAK]); | |
| int hw_breakpoint_slots(int type) | |
| { | |
| switch (type) { | |
| case TYPE_INST: | |
| return XCHAL_NUM_IBREAK; | |
| case TYPE_DATA: | |
| return XCHAL_NUM_DBREAK; | |
| default: | |
| pr_warn("unknown slot type: %d\n", type); | |
| return 0; | |
| } | |
| } | |
| int arch_check_bp_in_kernelspace(struct perf_event *bp) | |
| { | |
| unsigned int len; | |
| unsigned long va; | |
| struct arch_hw_breakpoint *info = counter_arch_bp(bp); | |
| va = info->address; | |
| len = bp->attr.bp_len; | |
| return (va >= TASK_SIZE) && ((va + len - 1) >= TASK_SIZE); | |
| } | |
| /* | |
| * Construct an arch_hw_breakpoint from a perf_event. | |
| */ | |
| static int arch_build_bp_info(struct perf_event *bp) | |
| { | |
| struct arch_hw_breakpoint *info = counter_arch_bp(bp); | |
| /* Type */ | |
| switch (bp->attr.bp_type) { | |
| case HW_BREAKPOINT_X: | |
| info->type = XTENSA_BREAKPOINT_EXECUTE; | |
| break; | |
| case HW_BREAKPOINT_R: | |
| info->type = XTENSA_BREAKPOINT_LOAD; | |
| break; | |
| case HW_BREAKPOINT_W: | |
| info->type = XTENSA_BREAKPOINT_STORE; | |
| break; | |
| case HW_BREAKPOINT_RW: | |
| info->type = XTENSA_BREAKPOINT_LOAD | XTENSA_BREAKPOINT_STORE; | |
| break; | |
| default: | |
| return -EINVAL; | |
| } | |
| /* Len */ | |
| info->len = bp->attr.bp_len; | |
| if (info->len < 1 || info->len > 64 || !is_power_of_2(info->len)) | |
| return -EINVAL; | |
| /* Address */ | |
| info->address = bp->attr.bp_addr; | |
| if (info->address & (info->len - 1)) | |
| return -EINVAL; | |
| return 0; | |
| } | |
| int arch_validate_hwbkpt_settings(struct perf_event *bp) | |
| { | |
| int ret; | |
| /* Build the arch_hw_breakpoint. */ | |
| ret = arch_build_bp_info(bp); | |
| return ret; | |
| } | |
| int hw_breakpoint_exceptions_notify(struct notifier_block *unused, | |
| unsigned long val, void *data) | |
| { | |
| return NOTIFY_DONE; | |
| } | |
| static void xtensa_wsr(unsigned long v, u8 sr) | |
| { | |
| /* We don't have indexed wsr and creating instruction dynamically | |
| * doesn't seem worth it given how small XCHAL_NUM_IBREAK and | |
| * XCHAL_NUM_DBREAK are. Thus the switch. In case build breaks here | |
| * the switch below needs to be extended. | |
| */ | |
| BUILD_BUG_ON(XCHAL_NUM_IBREAK > 2); | |
| BUILD_BUG_ON(XCHAL_NUM_DBREAK > 2); | |
| switch (sr) { | |
| #if XCHAL_NUM_IBREAK > 0 | |
| case SREG_IBREAKA + 0: | |
| WSR(v, SREG_IBREAKA + 0); | |
| break; | |
| #endif | |
| #if XCHAL_NUM_IBREAK > 1 | |
| case SREG_IBREAKA + 1: | |
| WSR(v, SREG_IBREAKA + 1); | |
| break; | |
| #endif | |
| #if XCHAL_NUM_DBREAK > 0 | |
| case SREG_DBREAKA + 0: | |
| WSR(v, SREG_DBREAKA + 0); | |
| break; | |
| case SREG_DBREAKC + 0: | |
| WSR(v, SREG_DBREAKC + 0); | |
| break; | |
| #endif | |
| #if XCHAL_NUM_DBREAK > 1 | |
| case SREG_DBREAKA + 1: | |
| WSR(v, SREG_DBREAKA + 1); | |
| break; | |
| case SREG_DBREAKC + 1: | |
| WSR(v, SREG_DBREAKC + 1); | |
| break; | |
| #endif | |
| } | |
| } | |
| static int alloc_slot(struct perf_event **slot, size_t n, | |
| struct perf_event *bp) | |
| { | |
| size_t i; | |
| for (i = 0; i < n; ++i) { | |
| if (!slot[i]) { | |
| slot[i] = bp; | |
| return i; | |
| } | |
| } | |
| return -EBUSY; | |
| } | |
| static void set_ibreak_regs(int reg, struct perf_event *bp) | |
| { | |
| struct arch_hw_breakpoint *info = counter_arch_bp(bp); | |
| unsigned long ibreakenable; | |
| xtensa_wsr(info->address, SREG_IBREAKA + reg); | |
| RSR(ibreakenable, SREG_IBREAKENABLE); | |
| WSR(ibreakenable | (1 << reg), SREG_IBREAKENABLE); | |
| } | |
| static void set_dbreak_regs(int reg, struct perf_event *bp) | |
| { | |
| struct arch_hw_breakpoint *info = counter_arch_bp(bp); | |
| unsigned long dbreakc = DBREAKC_MASK_MASK & -info->len; | |
| if (info->type & XTENSA_BREAKPOINT_LOAD) | |
| dbreakc |= DBREAKC_LOAD_MASK; | |
| if (info->type & XTENSA_BREAKPOINT_STORE) | |
| dbreakc |= DBREAKC_STOR_MASK; | |
| xtensa_wsr(info->address, SREG_DBREAKA + reg); | |
| xtensa_wsr(dbreakc, SREG_DBREAKC + reg); | |
| } | |
| int arch_install_hw_breakpoint(struct perf_event *bp) | |
| { | |
| int i; | |
| if (counter_arch_bp(bp)->type == XTENSA_BREAKPOINT_EXECUTE) { | |
| /* Breakpoint */ | |
| i = alloc_slot(this_cpu_ptr(bp_on_reg), XCHAL_NUM_IBREAK, bp); | |
| if (i < 0) | |
| return i; | |
| set_ibreak_regs(i, bp); | |
| } else { | |
| /* Watchpoint */ | |
| i = alloc_slot(this_cpu_ptr(wp_on_reg), XCHAL_NUM_DBREAK, bp); | |
| if (i < 0) | |
| return i; | |
| set_dbreak_regs(i, bp); | |
| } | |
| return 0; | |
| } | |
| static int free_slot(struct perf_event **slot, size_t n, | |
| struct perf_event *bp) | |
| { | |
| size_t i; | |
| for (i = 0; i < n; ++i) { | |
| if (slot[i] == bp) { | |
| slot[i] = NULL; | |
| return i; | |
| } | |
| } | |
| return -EBUSY; | |
| } | |
| void arch_uninstall_hw_breakpoint(struct perf_event *bp) | |
| { | |
| struct arch_hw_breakpoint *info = counter_arch_bp(bp); | |
| int i; | |
| if (info->type == XTENSA_BREAKPOINT_EXECUTE) { | |
| unsigned long ibreakenable; | |
| /* Breakpoint */ | |
| i = free_slot(this_cpu_ptr(bp_on_reg), XCHAL_NUM_IBREAK, bp); | |
| if (i >= 0) { | |
| RSR(ibreakenable, SREG_IBREAKENABLE); | |
| WSR(ibreakenable & ~(1 << i), SREG_IBREAKENABLE); | |
| } | |
| } else { | |
| /* Watchpoint */ | |
| i = free_slot(this_cpu_ptr(wp_on_reg), XCHAL_NUM_DBREAK, bp); | |
| if (i >= 0) | |
| xtensa_wsr(0, SREG_DBREAKC + i); | |
| } | |
| } | |
| void hw_breakpoint_pmu_read(struct perf_event *bp) | |
| { | |
| } | |
| void flush_ptrace_hw_breakpoint(struct task_struct *tsk) | |
| { | |
| int i; | |
| struct thread_struct *t = &tsk->thread; | |
| for (i = 0; i < XCHAL_NUM_IBREAK; ++i) { | |
| if (t->ptrace_bp[i]) { | |
| unregister_hw_breakpoint(t->ptrace_bp[i]); | |
| t->ptrace_bp[i] = NULL; | |
| } | |
| } | |
| for (i = 0; i < XCHAL_NUM_DBREAK; ++i) { | |
| if (t->ptrace_wp[i]) { | |
| unregister_hw_breakpoint(t->ptrace_wp[i]); | |
| t->ptrace_wp[i] = NULL; | |
| } | |
| } | |
| } | |
| /* | |
| * Set ptrace breakpoint pointers to zero for this task. | |
| * This is required in order to prevent child processes from unregistering | |
| * breakpoints held by their parent. | |
| */ | |
| void clear_ptrace_hw_breakpoint(struct task_struct *tsk) | |
| { | |
| memset(tsk->thread.ptrace_bp, 0, sizeof(tsk->thread.ptrace_bp)); | |
| memset(tsk->thread.ptrace_wp, 0, sizeof(tsk->thread.ptrace_wp)); | |
| } | |
| void restore_dbreak(void) | |
| { | |
| int i; | |
| for (i = 0; i < XCHAL_NUM_DBREAK; ++i) { | |
| struct perf_event *bp = this_cpu_ptr(wp_on_reg)[i]; | |
| if (bp) | |
| set_dbreak_regs(i, bp); | |
| } | |
| clear_thread_flag(TIF_DB_DISABLED); | |
| } | |
| int check_hw_breakpoint(struct pt_regs *regs) | |
| { | |
| if (regs->debugcause & BIT(DEBUGCAUSE_IBREAK_BIT)) { | |
| int i; | |
| struct perf_event **bp = this_cpu_ptr(bp_on_reg); | |
| for (i = 0; i < XCHAL_NUM_IBREAK; ++i) { | |
| if (bp[i] && !bp[i]->attr.disabled && | |
| regs->pc == bp[i]->attr.bp_addr) | |
| perf_bp_event(bp[i], regs); | |
| } | |
| return 0; | |
| } else if (regs->debugcause & BIT(DEBUGCAUSE_DBREAK_BIT)) { | |
| struct perf_event **bp = this_cpu_ptr(wp_on_reg); | |
| int dbnum = (regs->debugcause & DEBUGCAUSE_DBNUM_MASK) >> | |
| DEBUGCAUSE_DBNUM_SHIFT; | |
| if (dbnum < XCHAL_NUM_DBREAK && bp[dbnum]) { | |
| if (user_mode(regs)) { | |
| perf_bp_event(bp[dbnum], regs); | |
| } else { | |
| set_thread_flag(TIF_DB_DISABLED); | |
| xtensa_wsr(0, SREG_DBREAKC + dbnum); | |
| } | |
| } else { | |
| WARN_ONCE(1, | |
| "Wrong/unconfigured DBNUM reported in DEBUGCAUSE: %d\n", | |
| dbnum); | |
| } | |
| return 0; | |
| } | |
| return -ENOENT; | |
| } |