LoongArch: Add prologue unwinder support
authorQing Zhang <zhangqing@loongson.cn>
Sat, 6 Aug 2022 08:10:03 +0000 (16:10 +0800)
committerHuacai Chen <chenhuacai@loongson.cn>
Fri, 12 Aug 2022 05:10:11 +0000 (13:10 +0800)
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>
arch/loongarch/Kconfig.debug
arch/loongarch/include/asm/inst.h
arch/loongarch/include/asm/unwind.h
arch/loongarch/kernel/Makefile
arch/loongarch/kernel/traps.c
arch/loongarch/kernel/unwind_prologue.c [new file with mode: 0644]

index 68634d4fa27b9935522775d6b8ade1e683de535c..8d36aab530083b9c57355ea0dd96f69e23a64765 100644 (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
index 575d1bb66ffb52f781d06cbb76f9e577d9bf0cad..7b07cbb3188c0a4c1c8df07007ed1d5e788b67cf 100644 (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);
index 206fcbe24c0aff3a35a41890bdb4155b7e56fd87..6af4718bdf015fbd40ecbab9fc8619a5f6660add 100644 (file)
 
 #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,
index c5fa4adb23b64f5d87837af19ff39b46bffcc4c0..918600e7b30f768246bbf5eb9f1fe09d41e5850e 100644 (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)
index f65fdf90d29e8fe292a18ee5d4f493cecaf4eb0f..aa1c95aaf595ba0402e80d10f82cffb74db3c735 100644 (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)) {
diff --git a/arch/loongarch/kernel/unwind_prologue.c b/arch/loongarch/kernel/unwind_prologue.c
new file mode 100644 (file)
index 0000000..b206d91
--- /dev/null
@@ -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);