| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) 2022 Loongson Technology Corporation Limited |
| */ |
| #include <linux/cpumask.h> |
| #include <linux/ftrace.h> |
| #include <linux/kallsyms.h> |
| |
| #include <asm/inst.h> |
| #include <asm/loongson.h> |
| #include <asm/ptrace.h> |
| #include <asm/setup.h> |
| #include <asm/unwind.h> |
| |
| extern const int unwind_hint_ade; |
| extern const int unwind_hint_ale; |
| extern const int unwind_hint_bp; |
| extern const int unwind_hint_fpe; |
| extern const int unwind_hint_fpu; |
| extern const int unwind_hint_lsx; |
| extern const int unwind_hint_lasx; |
| extern const int unwind_hint_lbt; |
| extern const int unwind_hint_ri; |
| extern const int unwind_hint_watch; |
| extern unsigned long eentry; |
| #ifdef CONFIG_NUMA |
| extern unsigned long pcpu_handlers[NR_CPUS]; |
| #endif |
| |
| static inline bool scan_handlers(unsigned long entry_offset) |
| { |
| int idx, offset; |
| |
| if (entry_offset >= EXCCODE_INT_START * VECSIZE) |
| return false; |
| |
| idx = entry_offset / VECSIZE; |
| offset = entry_offset % VECSIZE; |
| switch (idx) { |
| case EXCCODE_ADE: |
| return offset == unwind_hint_ade; |
| case EXCCODE_ALE: |
| return offset == unwind_hint_ale; |
| case EXCCODE_BP: |
| return offset == unwind_hint_bp; |
| case EXCCODE_FPE: |
| return offset == unwind_hint_fpe; |
| case EXCCODE_FPDIS: |
| return offset == unwind_hint_fpu; |
| case EXCCODE_LSXDIS: |
| return offset == unwind_hint_lsx; |
| case EXCCODE_LASXDIS: |
| return offset == unwind_hint_lasx; |
| case EXCCODE_BTDIS: |
| return offset == unwind_hint_lbt; |
| case EXCCODE_INE: |
| return offset == unwind_hint_ri; |
| case EXCCODE_WATCH: |
| return offset == unwind_hint_watch; |
| default: |
| return false; |
| } |
| } |
| |
| static inline bool fix_exception(unsigned long pc) |
| { |
| #ifdef CONFIG_NUMA |
| int cpu; |
| |
| for_each_possible_cpu(cpu) { |
| if (!pcpu_handlers[cpu]) |
| continue; |
| if (scan_handlers(pc - pcpu_handlers[cpu])) |
| return true; |
| } |
| #endif |
| return scan_handlers(pc - eentry); |
| } |
| |
| /* |
| * As we meet ftrace_regs_entry, reset first flag like first doing |
| * tracing. Prologue analysis will stop soon because PC is at entry. |
| */ |
| static inline bool fix_ftrace(unsigned long pc) |
| { |
| #ifdef CONFIG_DYNAMIC_FTRACE |
| return pc == (unsigned long)ftrace_call + LOONGARCH_INSN_SIZE; |
| #else |
| return false; |
| #endif |
| } |
| |
| static inline bool unwind_state_fixup(struct unwind_state *state) |
| { |
| if (!fix_exception(state->pc) && !fix_ftrace(state->pc)) |
| return false; |
| |
| state->reset = true; |
| return true; |
| } |
| |
| /* |
| * LoongArch function prologue is like follows, |
| * [instructions not use stack var] |
| * addi.d sp, sp, -imm |
| * st.d xx, sp, offset <- save callee saved regs and |
| * st.d yy, sp, offset save ra if function is nest. |
| * [others instructions] |
| */ |
| static bool unwind_by_prologue(struct unwind_state *state) |
| { |
| long frame_ra = -1; |
| unsigned long frame_size = 0; |
| unsigned long size, offset, pc; |
| struct pt_regs *regs; |
| struct stack_info *info = &state->stack_info; |
| union loongarch_instruction *ip, *ip_end; |
| |
| if (state->sp >= info->end || state->sp < info->begin) |
| return false; |
| |
| if (state->reset) { |
| regs = (struct pt_regs *)state->sp; |
| state->first = true; |
| state->reset = false; |
| state->pc = regs->csr_era; |
| state->ra = regs->regs[1]; |
| state->sp = regs->regs[3]; |
| return true; |
| } |
| |
| /* |
| * When first is not set, the PC is a return address in the previous frame. |
| * We need to adjust its value in case overflow to the next symbol. |
| */ |
| pc = state->pc - (state->first ? 0 : LOONGARCH_INSN_SIZE); |
| if (!kallsyms_lookup_size_offset(pc, &size, &offset)) |
| return false; |
| |
| ip = (union loongarch_instruction *)(pc - offset); |
| ip_end = (union loongarch_instruction *)pc; |
| |
| while (ip < ip_end) { |
| if (is_stack_alloc_ins(ip)) { |
| frame_size = (1 << 12) - ip->reg2i12_format.immediate; |
| ip++; |
| break; |
| } |
| ip++; |
| } |
| |
| /* |
| * Can't find stack alloc action, PC may be in a leaf function. Only the |
| * first being true is reasonable, otherwise indicate analysis is broken. |
| */ |
| if (!frame_size) { |
| if (state->first) |
| goto first; |
| |
| return false; |
| } |
| |
| while (ip < ip_end) { |
| if (is_ra_save_ins(ip)) { |
| frame_ra = ip->reg2i12_format.immediate; |
| break; |
| } |
| if (is_branch_ins(ip)) |
| break; |
| ip++; |
| } |
| |
| /* Can't find save $ra action, PC may be in a leaf function, too. */ |
| if (frame_ra < 0) { |
| if (state->first) { |
| state->sp = state->sp + frame_size; |
| goto first; |
| } |
| return false; |
| } |
| |
| state->pc = *(unsigned long *)(state->sp + frame_ra); |
| state->sp = state->sp + frame_size; |
| goto out; |
| |
| first: |
| state->pc = state->ra; |
| |
| out: |
| state->first = false; |
| return unwind_state_fixup(state) || __kernel_text_address(state->pc); |
| } |
| |
| static bool next_frame(struct unwind_state *state) |
| { |
| unsigned long pc; |
| struct pt_regs *regs; |
| struct stack_info *info = &state->stack_info; |
| |
| if (unwind_done(state)) |
| return false; |
| |
| do { |
| if (unwind_by_prologue(state)) { |
| state->pc = unwind_graph_addr(state, state->pc, state->sp); |
| return true; |
| } |
| |
| if (info->type == STACK_TYPE_IRQ && info->end == state->sp) { |
| regs = (struct pt_regs *)info->next_sp; |
| pc = regs->csr_era; |
| |
| if (user_mode(regs) || !__kernel_text_address(pc)) |
| goto out; |
| |
| state->first = true; |
| state->pc = pc; |
| state->ra = regs->regs[1]; |
| state->sp = regs->regs[3]; |
| get_stack_info(state->sp, state->task, info); |
| |
| return true; |
| } |
| |
| state->sp = info->next_sp; |
| |
| } while (!get_stack_info(state->sp, state->task, info)); |
| |
| out: |
| state->error = true; |
| return false; |
| } |
| |
| unsigned long unwind_get_return_address(struct unwind_state *state) |
| { |
| return __unwind_get_return_address(state); |
| } |
| EXPORT_SYMBOL_GPL(unwind_get_return_address); |
| |
| void unwind_start(struct unwind_state *state, struct task_struct *task, |
| struct pt_regs *regs) |
| { |
| __unwind_start(state, task, regs); |
| state->type = UNWINDER_PROLOGUE; |
| state->first = true; |
| |
| /* |
| * The current PC is not kernel text address, we cannot find its |
| * relative symbol. Thus, prologue analysis will be broken. Luckily, |
| * we can use the default_next_frame(). |
| */ |
| if (!__kernel_text_address(state->pc)) { |
| state->type = UNWINDER_GUESS; |
| if (!unwind_done(state)) |
| unwind_next_frame(state); |
| } |
| } |
| EXPORT_SYMBOL_GPL(unwind_start); |
| |
| bool unwind_next_frame(struct unwind_state *state) |
| { |
| return state->type == UNWINDER_PROLOGUE ? |
| next_frame(state) : default_next_frame(state); |
| } |
| EXPORT_SYMBOL_GPL(unwind_next_frame); |