[PATCH 07/10] vmcore_tasks: Add generic user-space stack unwinder

Pnina Feder pnina.feder at mobileye.com
Mon Jun 22 13:54:45 PDT 2026


Add architecture-independent user-space frame unwinder that uses
arch-provided instruction callbacks to walk the call stack.

The unwinder tries recovery in order:
  1. Signal frame: if arch reports PC looks like a signal trampoline,
     walks rt_sigframe -> ucontext -> sigcontext using vmcoreinfo
     offsets to recover saved PC, SP, and RA.
  2. Prologue scan: scans backward from PC looking for stack frame
     setup (SP adjustment) and RA save instructions. Uses a buffered
     read strategy with page-boundary handling for efficiency.

Architecture-specific behavior is provided via arch.unwind_ops:
  - is_sp_move_ins(): detect stack pointer adjustment
  - is_ra_save_ins(): detect return address save to stack
  - is_end_of_backtrace(): detect end-of-trace markers (optional)
  - post_unwind_fixup(): arch-specific post-processing (optional)
  - likely_signal_pc(): hint whether PC is in a signal trampoline
  - signal_layout: describes signal frame structure offsets

This separation allows adding new architectures by implementing
only the instruction decode callbacks, without duplicating the
unwinding logic.

Signed-off-by: Pnina Feder <pnina.feder at mobileye.com>
---
 vmcore_tasks/unwind.c | 267 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 267 insertions(+)
 create mode 100644 vmcore_tasks/unwind.c

diff --git a/vmcore_tasks/unwind.c b/vmcore_tasks/unwind.c
new file mode 100644
index 00000000..99d23322
--- /dev/null
+++ b/vmcore_tasks/unwind.c
@@ -0,0 +1,267 @@
+#include <stdio.h>
+#include <string.h>
+#include "memory.h"
+#include "vmcore_tasks_defs.h"
+
+/* Return codes for unwind_one_frame */
+#define UNWIND_OK        0
+#define UNWIND_LEAF     -1   /* No prologue found — possible leaf function */
+#define UNWIND_FAIL     -2   /* Hard failure (bad memory, etc.) */
+
+static bool sigframe_candidate_ok(uint64_t old_pc, uint64_t old_sp, uint64_t old_ra,
+				  uint64_t new_pc, uint64_t new_sp, uint64_t new_ra)
+{
+	if (!new_pc || !new_sp)
+		return false;
+	if (new_pc == old_pc && new_ra == old_ra)
+		return false;
+	/* Signal return should restore caller SP above current sigframe SP. */
+	if (new_sp <= old_sp)
+		return false;
+	/* Reject absurd jumps in SP (false positives). */
+	if ((new_sp - old_sp) > (1ULL << 20))
+		return false;
+	return true;
+}
+
+static int unwind_signal_frame(struct stackframe *frame)
+{
+	const struct signal_unwind_layout *layout = arch.unwind_ops.signal_layout;
+	uint64_t old_pc, old_sp, old_ra;
+	uint64_t uc_addr, mctx_addr, regs_addr;
+	uint64_t new_pc = 0, new_sp = 0, new_ra = 0;
+
+	if (!frame || !layout)
+		return -1;
+
+	/* Required vmcoreinfo keys to locate rt_sigframe -> ucontext -> sigcontext regs. */
+	if (!layout->rt_sigframe_uc_key ||
+	    !layout->ucontext_mcontext_key ||
+	    !layout->sigcontext_regs_key)
+		return -1;
+
+	if (!OFFSET_EXISTS(layout->rt_sigframe_uc_key) ||
+	    !OFFSET_EXISTS(layout->ucontext_mcontext_key) ||
+	    !OFFSET_EXISTS(layout->sigcontext_regs_key))
+		return -1;
+
+	/* Walk the layout chain from current SP to saved register block. */
+	uc_addr   = frame->sp + OFFSET(layout->rt_sigframe_uc_key);
+	mctx_addr = uc_addr   + OFFSET(layout->ucontext_mcontext_key);
+	regs_addr = mctx_addr + OFFSET(layout->sigcontext_regs_key);
+
+	/* Always recover RA and SP from arch provided indices */
+	if (readmem(regs_addr + ((uint64_t)layout->ra_reg_index * layout->reg_width),
+	    &new_ra, layout->reg_width, "sigcontext ra", UVADDR) < 0)
+		return -1;
+	if (readmem(regs_addr + ((uint64_t)layout->sp_reg_index * layout->reg_width),
+	    &new_sp, layout->reg_width, "sigcontext sp", UVADDR) < 0)
+		return -1;
+
+	old_pc = frame->pc;
+	old_sp = frame->sp;
+	old_ra = frame->ra;
+
+	/* PC can be either a dedicated field or a slot in the regs array. */
+	if (layout->sigcontext_pc_key && OFFSET_EXISTS(layout->sigcontext_pc_key)) {
+		uint64_t pc_addr = mctx_addr + OFFSET(layout->sigcontext_pc_key);
+		if (readmem(pc_addr, &new_pc, layout->reg_width,
+		    "sigcontext pc", UVADDR) < 0)
+			return -1;
+	} else {
+		if (readmem(regs_addr + ((uint64_t)layout->pc_reg_index * layout->reg_width),
+				 &new_pc, layout->reg_width, "sigcontext pc", UVADDR) < 0)
+			return -1;
+		/* Try alternate PC slot for kernels/ABIs with different ordering. */
+		if (!sigframe_candidate_ok(old_pc, old_sp, old_ra, new_pc, new_sp, new_ra) &&
+		    layout->pc_reg_alt_index >= 0) {
+			if (readmem(regs_addr + ((uint64_t)layout->pc_reg_alt_index * layout->reg_width),
+					 &new_pc, layout->reg_width, "sigcontext pc(alt)", UVADDR) < 0)
+				return -1;
+		}
+	}
+
+	/* Final sanity gate before committing recovered frame. */
+	if (!sigframe_candidate_ok(old_pc, old_sp, old_ra, new_pc, new_sp, new_ra))
+		return -1;
+
+	pr_verbose("sigframe-candidate: old[pc=0x%016llx sp=0x%016llx ra=0x%016llx] new[pc=0x%016llx sp=0x%016llx ra=0x%016llx] chain[uc=0x%016llx mctx=0x%016llx regs=0x%016llx] src=%s idx[sp=%d ra=%d pc=%d alt=%d] w=%u\n",
+		   (unsigned long long)old_pc, (unsigned long long)old_sp, (unsigned long long)old_ra,
+		   (unsigned long long)new_pc, (unsigned long long)new_sp, (unsigned long long)new_ra,
+		   (unsigned long long)uc_addr, (unsigned long long)mctx_addr, (unsigned long long)regs_addr,
+		   (layout->sigcontext_pc_key && OFFSET_EXISTS(layout->sigcontext_pc_key)) ? "sigcontext_pc_key" : "regs[]",
+		   layout->sp_reg_index, layout->ra_reg_index, layout->pc_reg_index, layout->pc_reg_alt_index,
+		   layout->reg_width);
+
+	frame->pc = (unsigned long)new_pc;
+	frame->sp = (unsigned long)new_sp;
+	frame->ra = (unsigned long)new_ra;
+	return 0;
+}
+
+/*
+ * Try to recover the previous frame by scanning backward for prologue
+ * instructions (SP adjustment + RA save).
+ *
+ * Returns: UNWIND_OK on success, UNWIND_LEAF if no prologue found,
+ *          UNWIND_FAIL on hard error.
+ */
+static int unwind_prologue_scan(struct stackframe *frame, unsigned int max_check)
+{
+	off_t ra_offset = 0;
+	size_t stack_size = 0;
+	unsigned long scan_addr, scan_end;
+	uint32_t insn;
+	int sp_adjust = 0;
+	unsigned long ra_slot = 0;
+
+	uint8_t code_buf[CODE_BUF_SIZE];
+	unsigned long buf_base = 0;
+	ssize_t buf_len = 0;
+
+	scan_addr = frame->pc - arch.unwind_ops.insn_size;
+
+	if (max_check >= frame->pc)
+		scan_end = 0;
+	else
+		scan_end = frame->pc - max_check;
+
+	while (scan_addr >= scan_end) {
+		/* Refill code buffer when needed */
+		if (buf_len == 0 || scan_addr < buf_base) {
+			if (scan_addr + arch.unwind_ops.insn_size < CODE_BUF_SIZE)
+				buf_base = 0;
+			else
+				buf_base = scan_addr + arch.unwind_ops.insn_size - CODE_BUF_SIZE;
+
+			if (buf_base < scan_end)
+				buf_base = scan_end & ~((unsigned long)arch.unwind_ops.insn_size - 1);
+
+			size_t to_read = scan_addr + arch.unwind_ops.insn_size - buf_base;
+			buf_len = readmem(buf_base, code_buf, to_read,
+					  "read code chunk", UVADDR);
+			if (buf_len <= 0) {
+				unsigned long next_page = (buf_base & SYS_PAGE_MASK) + SYS_PAGE_SIZE;
+				pr_trace("Failed to read at 0x%lx, advancing scan_end to 0x%lx\n",
+					 buf_base, next_page);
+				scan_end = next_page;
+				buf_len = 0;
+				continue;
+			}
+		}
+
+		size_t offset = scan_addr - buf_base;
+		if (offset + arch.unwind_ops.insn_size > (size_t)buf_len) {
+			pr_verbose("Offset out of buffer range\n");
+			return UNWIND_FAIL;
+		}
+		memcpy(&insn, code_buf + offset, arch.unwind_ops.insn_size);
+
+		if (arch.unwind_ops.is_sp_move_ins(insn, &sp_adjust)) {
+			if (sp_adjust < 0) {
+				stack_size = (size_t)(-sp_adjust);
+				pr_trace("sp_move: pc=0x%lx insn=0x%08x adjust=%d\n",
+					 scan_addr, insn, sp_adjust);
+				break;
+			}
+		} else if (arch.unwind_ops.is_ra_save_ins(insn, &ra_slot)) {
+			pr_trace("ra_save: pc=0x%lx insn=0x%08x slot=%lu\n",
+				 scan_addr, insn, ra_slot);
+			ra_offset = (off_t)ra_slot;
+		}
+
+		if (scan_addr < arch.unwind_ops.insn_size)
+			break;
+		scan_addr -= arch.unwind_ops.insn_size;
+	}
+
+	if (!stack_size)
+		return UNWIND_LEAF;
+
+	if (ra_offset) {
+		unsigned long ra_addr = frame->sp + ra_offset;
+		unsigned long new_ra;
+		if (readmem(ra_addr, &new_ra, arch.unwind_ops.ra_width,
+			    "read RA from stack", UVADDR) != (ssize_t)arch.unwind_ops.ra_width) {
+			pr_trace("RA slot unreadable at 0x%lx; treating as end of backtrace\n",
+				 ra_addr);
+			return UNWIND_FAIL;
+		}
+		frame->ra = new_ra;
+		frame->pc = new_ra;
+	} else if (frame->ra != 0) {
+		/* No RA slot found — use existing RA as fallback (e.g. from registers) */
+		frame->pc = frame->ra;
+	} else {
+		fprintf(stderr, "No RA found and frame->ra is 0\n");
+		return UNWIND_FAIL;
+	}
+
+	frame->sp += stack_size;
+
+	pr_trace("unwound: pc=0x%lx ra=0x%lx sp=0x%lx\n",
+		 frame->pc, frame->ra, frame->sp);
+	return UNWIND_OK;
+}
+
+/*
+ * Try signal-frame recovery if the arch hints that PC looks like a
+ * signal trampoline.
+ */
+static int try_signal_frame_unwind(struct stackframe *frame)
+{
+	if (!arch.unwind_ops.signal_layout)
+		return -1;
+
+	bool likely = true;
+	if (arch.unwind_ops.likely_signal_pc)
+		likely = arch.unwind_ops.likely_signal_pc(frame->pc);
+
+	if (!likely)
+		return -1;
+
+	struct stackframe candidate = *frame;
+	if (unwind_signal_frame(&candidate) != 0)
+		return -1;
+
+	pr_verbose("signal-frame: pc 0x%lx->0x%lx sp 0x%lx->0x%lx ra 0x%lx->0x%lx\n",
+		 frame->pc, candidate.pc, frame->sp, candidate.sp,
+		 frame->ra, candidate.ra);
+	*frame = candidate;
+	return 0;
+}
+
+/*
+ * Public entry point: unwind one user-space frame.
+ *
+ * Tries, in order:
+ *   1. Signal-frame recovery
+ *   2. Prologue-scan unwinding
+ *
+ * Returns UNWIND_OK, UNWIND_LEAF, or UNWIND_FAIL.
+ */
+int unwind_user_frame(struct stackframe *frame, unsigned int max_check)
+{
+	int ret;
+
+	if (!frame->pc || !frame->sp) {
+		fprintf(stderr, "error: pc or sp is zero: pc=0x%lx sp=0x%lx\n",
+			frame->pc, frame->sp);
+		return UNWIND_FAIL;
+	}
+
+	if (try_signal_frame_unwind(frame) == 0)
+		return UNWIND_OK;
+
+	ret = unwind_prologue_scan(frame, max_check);
+	if (ret != UNWIND_OK)
+		return ret;
+
+	/* Optional arch-specific fixup after unwinding. */
+	if (arch.unwind_ops.post_unwind_fixup) {
+		if (arch.unwind_ops.post_unwind_fixup(frame, max_check) != 0)
+			return UNWIND_FAIL;
+	}
+
+	return UNWIND_OK;
+}
\ No newline at end of file
-- 
2.43.0




More information about the kexec mailing list