266 lines
6.1 KiB
C
266 lines
6.1 KiB
C
|
// 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);
|