[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