LoongArch: Add prologue unwinder support

It unwind the stack frame based on prologue code analyze.
CONFIG_KALLSYMS is needed, at least the address and length
of each function.

Three stages when we do unwind,
  1) unwind_start(), the prapare of unwinding, fill unwind_state.
  2) unwind_done(), judge whether the unwind process is finished or not.
  3) unwind_next_frame(), unwind the next frame.

Dividing unwinder helps to add new unwinders in the future, e.g.:
unwinder_frame, unwinder_orc, .etc.

Signed-off-by: Qing Zhang <zhangqing@loongson.cn>
Signed-off-by: Huacai Chen <chenhuacai@loongson.cn>
This commit is contained in:
Qing Zhang 2022-08-06 16:10:03 +08:00 committed by Huacai Chen
parent 49232773d8
commit 49aef111e2
6 changed files with 259 additions and 1 deletions

View File

@ -1,3 +1,11 @@
choice
prompt "Choose kernel unwinder"
default UNWINDER_PROLOGUE if KALLSYMS
help
This determines which method will be used for unwinding kernel stack
traces for panics, oopses, bugs, warnings, perf, /proc/<pid>/stack,
lockdep, and more.
config UNWINDER_GUESS
bool "Guess unwinder"
help
@ -7,3 +15,15 @@ config UNWINDER_GUESS
While this option often produces false positives, it can still be
useful in many cases.
config UNWINDER_PROLOGUE
bool "Prologue unwinder"
depends on KALLSYMS
help
This option enables the "prologue" unwinder for unwinding kernel stack
traces. It unwind the stack frame based on prologue code analyze. Symbol
information is needed, at least the address and length of each function.
Some of the addresses it reports may be incorrect (but better than the
Guess unwinder).
endchoice

View File

@ -23,12 +23,33 @@ enum reg1i20_op {
lu32id_op = 0x0b,
};
enum reg1i21_op {
beqz_op = 0x10,
bnez_op = 0x11,
};
enum reg2i12_op {
addiw_op = 0x0a,
addid_op = 0x0b,
lu52id_op = 0x0c,
ldb_op = 0xa0,
ldh_op = 0xa1,
ldw_op = 0xa2,
ldd_op = 0xa3,
stb_op = 0xa4,
sth_op = 0xa5,
stw_op = 0xa6,
std_op = 0xa7,
};
enum reg2i16_op {
jirl_op = 0x13,
beq_op = 0x16,
bne_op = 0x17,
blt_op = 0x18,
bge_op = 0x19,
bltu_op = 0x1a,
bgeu_op = 0x1b,
};
struct reg0i26_format {
@ -110,6 +131,37 @@ enum loongarch_gpr {
LOONGARCH_GPR_MAX
};
#define is_imm12_negative(val) is_imm_negative(val, 12)
static inline bool is_imm_negative(unsigned long val, unsigned int bit)
{
return val & (1UL << (bit - 1));
}
static inline bool is_branch_ins(union loongarch_instruction *ip)
{
return ip->reg1i21_format.opcode >= beqz_op &&
ip->reg1i21_format.opcode <= bgeu_op;
}
static inline bool is_ra_save_ins(union loongarch_instruction *ip)
{
/* st.d $ra, $sp, offset */
return ip->reg2i12_format.opcode == std_op &&
ip->reg2i12_format.rj == LOONGARCH_GPR_SP &&
ip->reg2i12_format.rd == LOONGARCH_GPR_RA &&
!is_imm12_negative(ip->reg2i12_format.immediate);
}
static inline bool is_stack_alloc_ins(union loongarch_instruction *ip)
{
/* addi.d $sp, $sp, -imm */
return ip->reg2i12_format.opcode == addid_op &&
ip->reg2i12_format.rj == LOONGARCH_GPR_SP &&
ip->reg2i12_format.rd == LOONGARCH_GPR_SP &&
is_imm12_negative(ip->reg2i12_format.immediate);
}
u32 larch_insn_gen_lu32id(enum loongarch_gpr rd, int imm);
u32 larch_insn_gen_lu52id(enum loongarch_gpr rd, enum loongarch_gpr rj, int imm);
u32 larch_insn_gen_jirl(enum loongarch_gpr rd, enum loongarch_gpr rj, unsigned long pc, unsigned long dest);

View File

@ -11,11 +11,17 @@
#include <asm/stacktrace.h>
enum unwinder_type {
UNWINDER_GUESS,
UNWINDER_PROLOGUE,
};
struct unwind_state {
char type; /* UNWINDER_XXX */
struct stack_info stack_info;
struct task_struct *task;
bool first, error;
unsigned long sp, pc;
unsigned long sp, pc, ra;
};
void unwind_start(struct unwind_state *state,

View File

@ -23,5 +23,6 @@ obj-$(CONFIG_SMP) += smp.o
obj-$(CONFIG_NUMA) += numa.o
obj-$(CONFIG_UNWINDER_GUESS) += unwind_guess.o
obj-$(CONFIG_UNWINDER_PROLOGUE) += unwind_prologue.o
CPPFLAGS_vmlinux.lds := $(KBUILD_CFLAGS)

View File

@ -71,6 +71,9 @@ static void show_backtrace(struct task_struct *task, const struct pt_regs *regs,
if (!task)
task = current;
if (user_mode(regs))
state.type = UNWINDER_GUESS;
printk("%sCall Trace:", loglvl);
for (unwind_start(&state, task, pregs);
!unwind_done(&state); unwind_next_frame(&state)) {

View File

@ -0,0 +1,176 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2022 Loongson Technology Corporation Limited
*/
#include <linux/kallsyms.h>
#include <asm/inst.h>
#include <asm/ptrace.h>
#include <asm/unwind.h>
unsigned long unwind_get_return_address(struct unwind_state *state)
{
if (unwind_done(state))
return 0;
else if (state->type)
return state->pc;
else if (state->first)
return state->pc;
return *(unsigned long *)(state->sp);
}
EXPORT_SYMBOL_GPL(unwind_get_return_address);
static bool unwind_by_guess(struct unwind_state *state)
{
struct stack_info *info = &state->stack_info;
unsigned long addr;
for (state->sp += sizeof(unsigned long);
state->sp < info->end;
state->sp += sizeof(unsigned long)) {
addr = *(unsigned long *)(state->sp);
if (__kernel_text_address(addr))
return true;
}
return false;
}
static bool unwind_by_prologue(struct unwind_state *state)
{
struct stack_info *info = &state->stack_info;
union loongarch_instruction *ip, *ip_end;
unsigned long frame_size = 0, frame_ra = -1;
unsigned long size, offset, pc = state->pc;
if (state->sp >= info->end || state->sp < info->begin)
return false;
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++;
}
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++;
}
if (frame_ra < 0) {
if (state->first) {
state->sp = state->sp + frame_size;
goto first;
}
return false;
}
if (state->first)
state->first = false;
state->pc = *(unsigned long *)(state->sp + frame_ra);
state->sp = state->sp + frame_size;
return !!__kernel_text_address(state->pc);
first:
state->first = false;
if (state->pc == state->ra)
return false;
state->pc = state->ra;
return !!__kernel_text_address(state->ra);
}
void unwind_start(struct unwind_state *state, struct task_struct *task,
struct pt_regs *regs)
{
memset(state, 0, sizeof(*state));
if (regs && __kernel_text_address(regs->csr_era)) {
state->pc = regs->csr_era;
state->sp = regs->regs[3];
state->ra = regs->regs[1];
state->type = UNWINDER_PROLOGUE;
}
state->task = task;
state->first = true;
get_stack_info(state->sp, state->task, &state->stack_info);
if (!unwind_done(state) && !__kernel_text_address(state->pc))
unwind_next_frame(state);
}
EXPORT_SYMBOL_GPL(unwind_start);
bool unwind_next_frame(struct unwind_state *state)
{
struct stack_info *info = &state->stack_info;
struct pt_regs *regs;
unsigned long pc;
if (unwind_done(state))
return false;
do {
switch (state->type) {
case UNWINDER_GUESS:
state->first = false;
if (unwind_by_guess(state))
return true;
break;
case UNWINDER_PROLOGUE:
if (unwind_by_prologue(state))
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))
return false;
state->pc = pc;
state->sp = regs->regs[3];
state->ra = regs->regs[1];
state->first = true;
get_stack_info(state->sp, state->task, info);
return true;
}
}
state->sp = info->next_sp;
} while (!get_stack_info(state->sp, state->task, info));
return false;
}
EXPORT_SYMBOL_GPL(unwind_next_frame);