arch/risc-v: fix up_backtrace() bugs and rework stack splicing

Bugs fixed:
1. BUILD_FLAT/BUILD_PROTECTED: the interrupt-context self backtrace
   printed the same frames twice.
2. BUILD_KERNEL: a cross-tcb backtrace never included the target
   task's kernel stack, so frames on it were missing.

Also reworked the implementation around a list of independent
starting points, each carrying every stack range its fp chain may
cross into, splicing them into one backtrace.

Assisted-by: Claude Code:claude-sonnet-5
Signed-off-by: liang.huang <liang.huang@houmo.ai>
This commit is contained in:
liang.huang 2026-07-18 08:12:10 +08:00
parent a5615419e7
commit d57d3fc3fd

View file

@ -25,11 +25,85 @@
****************************************************************************/
#include <nuttx/config.h>
#include <stdbool.h>
#include <nuttx/arch.h>
#include <nuttx/addrenv.h>
#include <nuttx/compiler.h>
#include "sched/sched.h"
#include "riscv_internal.h"
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
/* Maximum number of independent starting points up_backtrace() can splice
* together. Current worst case: the main fp chain (possibly starting on
* the interrupt or kernel stack) plus the xcp.sregs splice used to reach
* the user call site of a task that is mid-syscall.
*/
#define BACKTRACE_MAX_SEGMENTS 2
/* Upper bound on how many candidate ranges a single segment can
* register: interrupt stack, kernel stack, user stack.
*/
#define BACKTRACE_MAX_RANGES 3
/****************************************************************************
* Private Types
****************************************************************************/
/* One candidate stack range a segment's fp chain may be walking. */
struct backtrace_range_s
{
uintptr_t *base;
uintptr_t *limit;
#ifdef CONFIG_ARCH_ADDRENV
struct addrenv_s *addrenv;
#endif
};
/* One independent fp/ra starting point for backtrace_segments() to walk,
* together with every stack range its chain may legitimately be found on.
*/
struct backtrace_segment_s
{
uintptr_t *fp;
uintptr_t *ra;
struct backtrace_range_s ranges[BACKTRACE_MAX_RANGES];
int nranges;
};
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
static struct backtrace_segment_s *
backtrace_new_segment(struct backtrace_segment_s *segments,
int *nsegments, uintptr_t *fp, uintptr_t *ra);
static void backtrace_add_range(struct backtrace_segment_s *segment,
uintptr_t *base, uintptr_t *limit
#ifdef CONFIG_ARCH_ADDRENV
, struct addrenv_s *addrenv
#endif
);
/* BACKTRACE_ADD_RANGE() drops the trailing addrenv argument when
* CONFIG_ARCH_ADDRENV is not configured, so call sites never need their own
* #ifdef.
*/
#ifdef CONFIG_ARCH_ADDRENV
# define BACKTRACE_ADD_RANGE(segment, base, limit, addrenv) \
backtrace_add_range(segment, base, limit, addrenv)
#else
# define BACKTRACE_ADD_RANGE(segment, base, limit, addrenv) \
backtrace_add_range(segment, base, limit)
#endif
/****************************************************************************
* Private Functions
****************************************************************************/
@ -42,7 +116,7 @@
*
****************************************************************************/
static inline uintptr_t getfp(void)
static always_inline_function uintptr_t getfp(void)
{
register uintptr_t fp;
@ -55,6 +129,61 @@ static inline uintptr_t getfp(void)
return fp;
}
/****************************************************************************
* Name: backtrace_add_range
*
* Description:
* Append one candidate stack range to a segment.
*
****************************************************************************/
static void backtrace_add_range(struct backtrace_segment_s *segment,
uintptr_t *base, uintptr_t *limit
#ifdef CONFIG_ARCH_ADDRENV
, struct addrenv_s *addrenv
#endif
)
{
struct backtrace_range_s *range;
DEBUGASSERT(segment->nranges < BACKTRACE_MAX_RANGES);
range = &segment->ranges[segment->nranges++];
range->base = base;
range->limit = limit;
#ifdef CONFIG_ARCH_ADDRENV
range->addrenv = addrenv;
#endif
}
/****************************************************************************
* Name: backtrace_new_segment
*
* Description:
* Allocate the next free segment slot and initialize its fp/ra starting
* point.
*
* Returned Value:
* A pointer to the newly allocated segment, ready for BACKTRACE_ADD_RANGE()
* calls.
*
****************************************************************************/
static struct backtrace_segment_s *
backtrace_new_segment(struct backtrace_segment_s *segments,
int *nsegments, uintptr_t *fp, uintptr_t *ra)
{
struct backtrace_segment_s *segment;
segment = &segments[(*nsegments)++];
segment->fp = fp;
segment->ra = ra;
segment->nranges = 0;
return segment;
}
/****************************************************************************
* Name: backtrace
*
@ -63,30 +192,24 @@ static inline uintptr_t getfp(void)
*
****************************************************************************/
nosanitize_address
nosanitize_address noinline_function
static int backtrace(uintptr_t *base, uintptr_t *limit,
uintptr_t *fp, uintptr_t *ra,
void **buffer, int size, int *skip
void **buffer, int size, int *skip,
uintptr_t **bridge
#ifdef CONFIG_ARCH_ADDRENV
, FAR struct addrenv_s *addrenv
, struct addrenv_s *addrenv
#endif
)
{
int i = 0;
uintptr_t *next_fp;
#if CONFIG_ARCH_INTERRUPTSTACK > 15
int cpu = up_cpu_index();
uintptr_t *intstack_limit =
(uintptr_t *)((uintptr_t)g_intstacktop -
(CONFIG_ARCH_INTERRUPTSTACK * cpu));
uintptr_t *intstack_base =
(uintptr_t *)((uintptr_t)intstack_limit - CONFIG_ARCH_INTERRUPTSTACK);
#endif
#ifdef CONFIG_ARCH_ADDRENV
FAR struct addrenv_s *oldenv;
struct addrenv_s *oldenv;
#endif
*bridge = NULL;
if (ra)
{
if ((*skip)-- <= 0)
@ -95,18 +218,19 @@ static int backtrace(uintptr_t *base, uintptr_t *limit,
}
}
for (; i < size; fp = next_fp)
for (; i < size && fp != NULL; fp = next_fp)
{
if ((fp > limit || fp < base)
#if CONFIG_ARCH_INTERRUPTSTACK > 15
&& (fp > intstack_limit || fp < intstack_base)
#endif
)
if (fp > limit || fp < base)
{
/* fp is outside [base, limit]. Report it back through
* *bridge and stop; the caller decides what to do with it.
*/
*bridge = fp;
break;
}
/* fp/fp-1/fp-2 are addresses in the *target* tcb's stack. If that
/* fp-1/fp-2 are addresses in the *target* tcb's stack. If that
* tcb has its own address environment, switch to it only for these
* two dereferences. buffer belongs to the caller, not the target,
* and must always be written using the caller's own address
@ -144,6 +268,89 @@ static int backtrace(uintptr_t *base, uintptr_t *limit,
return i;
}
/****************************************************************************
* Name: backtrace_walk_segment
*
* Description:
* Walk one segment's fp chain, switching to the next candidate range
* whenever backtrace() bridges out of the one it was given. Ranges are
* only ever crossed outward (e.g. interrupt stack -> kernel stack -> user
* stack, never backwards), so the search for the next range picks up from
* where the previous one left off instead of rescanning from the start.
*
****************************************************************************/
static int backtrace_walk_segment(struct backtrace_segment_s *segment,
void **buffer, int size, int *skip)
{
uintptr_t *fp = segment->fp;
uintptr_t *ra = segment->ra;
uintptr_t *bridge;
int total = 0;
int ir = 0;
for (; ; )
{
for (; ir < segment->nranges; ir++)
{
if (fp <= segment->ranges[ir].limit &&
fp >= segment->ranges[ir].base)
{
break;
}
}
if (ir == segment->nranges)
{
break;
}
total += backtrace(segment->ranges[ir].base, segment->ranges[ir].limit,
fp, ra, &buffer[total], size - total, skip,
&bridge
#ifdef CONFIG_ARCH_ADDRENV
, segment->ranges[ir].addrenv
#endif
);
if (bridge == NULL || total >= size)
{
break;
}
fp = bridge;
ra = NULL;
}
return total;
}
/****************************************************************************
* Name: backtrace_segments
*
* Description:
* Walk a short list of stack segments in order, carrying over into the
* next one whenever the current segment stops producing frames before
* the buffer is full.
*
****************************************************************************/
static int backtrace_segments(struct backtrace_segment_s *segments,
int nsegments, void **buffer, int size,
int *skip)
{
int ret = 0;
int i;
for (i = 0; i < nsegments && ret < size; i++)
{
ret += backtrace_walk_segment(&segments[i], &buffer[ret],
size - ret, skip);
}
return ret;
}
/****************************************************************************
* Public Functions
****************************************************************************/
@ -162,6 +369,12 @@ static int backtrace(uintptr_t *base, uintptr_t *limit,
* recent function calls are returned; to obtain the complete backtrace,
* make sure that buffer and size are large enough.
*
* A task that is (or was, at the last trap) mid-syscall has its call
* chain split across two stacks: the kernel stack (from the trap that
* entered the syscall up to the ecall itself) and, continuing from
* there, the user stack (the application code that issued the ecall).
* Both segments are walked and spliced together into one buffer.
*
* Input Parameters:
* tcb - Address of the task's TCB
* buffer - Return address from the corresponding stack frame
@ -184,100 +397,103 @@ static int backtrace(uintptr_t *base, uintptr_t *limit,
int up_backtrace(struct tcb_s *tcb, void **buffer, int size, int skip)
{
struct tcb_s *rtcb = running_task();
int ret;
struct backtrace_segment_s segments[BACKTRACE_MAX_SEGMENTS];
struct backtrace_segment_s *segment;
uintptr_t *ubase;
uintptr_t *ulimit;
bool self = (tcb == NULL || tcb == rtcb);
int nsegments = 0;
#if CONFIG_ARCH_INTERRUPTSTACK > 15
int cpu;
uintptr_t *ilimit;
uintptr_t *ibase;
#endif
#ifdef CONFIG_LIB_SYSCALL
bool insyscall;
#endif
if (size <= 0 || !buffer)
{
return 0;
}
if (tcb == NULL || tcb == rtcb)
if (self)
{
if (up_interrupt_context())
{
ret = backtrace(rtcb->stack_base_ptr,
(uintptr_t *)((uintptr_t)rtcb->stack_base_ptr +
rtcb->adj_stack_size),
(void *)getfp(), NULL, buffer, size, &skip
#ifdef CONFIG_ARCH_ADDRENV
, NULL
#endif
);
if (ret < size)
{
ret += backtrace(rtcb->stack_base_ptr, (uintptr_t *)
((uintptr_t)rtcb->stack_base_ptr +
rtcb->adj_stack_size),
running_regs()[REG_FP],
running_regs()[REG_EPC],
&buffer[ret], size - ret, &skip
#ifdef CONFIG_ARCH_ADDRENV
, NULL
#endif
);
}
}
else
{
#ifdef CONFIG_ARCH_KERNEL_STACK
if ((rtcb->flags & TCB_FLAG_SYSCALL) != 0)
{
ret = backtrace(rtcb->stack_base_ptr, (uintptr_t *)
((uintptr_t)rtcb->stack_base_ptr +
rtcb->adj_stack_size),
(void *)rtcb->xcp.sregs[REG_FP],
(void *)rtcb->xcp.sregs[REG_EPC],
buffer, size, &skip
#ifdef CONFIG_ARCH_ADDRENV
, NULL
#endif
);
}
else
#endif
{
ret = backtrace(rtcb->stack_base_ptr, (uintptr_t *)
((uintptr_t)rtcb->stack_base_ptr +
rtcb->adj_stack_size),
(void *)getfp(), NULL, buffer, size, &skip
#ifdef CONFIG_ARCH_ADDRENV
, NULL
#endif
);
}
}
}
else
{
#ifdef CONFIG_ARCH_KERNEL_STACK
if ((tcb->flags & TCB_FLAG_SYSCALL) != 0)
{
ret = backtrace(tcb->stack_base_ptr,
(uintptr_t *)((uintptr_t)tcb->stack_base_ptr +
tcb->adj_stack_size),
(void *)tcb->xcp.sregs[REG_FP],
(void *)tcb->xcp.sregs[REG_EPC],
buffer, size, &skip
#ifdef CONFIG_ARCH_ADDRENV
, tcb->addrenv_own
#endif
);
}
else
#endif
{
ret = backtrace(tcb->stack_base_ptr,
(uintptr_t *)((uintptr_t)tcb->stack_base_ptr +
tcb->adj_stack_size),
(void *)tcb->xcp.regs[REG_FP],
(void *)tcb->xcp.regs[REG_EPC],
buffer, size, &skip
#ifdef CONFIG_ARCH_ADDRENV
, tcb->addrenv_own
#endif
);
}
tcb = rtcb;
}
return ret;
ubase = tcb->stack_base_ptr;
ulimit = (uintptr_t *)((uintptr_t)tcb->stack_base_ptr +
tcb->adj_stack_size);
#ifdef CONFIG_LIB_SYSCALL
insyscall = (tcb->flags & TCB_FLAG_SYSCALL) != 0;
#endif
/* The main fp chain may start on (and naturally unwind through) any
* stack the tcb was last executing on, innermost first: the interrupt
* stack, then the kernel stack, then finally the user stack. Every
* range it could legitimately cross into is listed up front instead of
* picking just one ahead of time, since the trap entry copies the
* pre-trap fp/ra into each synthetic frame, letting the chain unwind
* straight through a trap boundary onto the next stack out.
*/
segment = backtrace_new_segment(segments, &nsegments,
self ? (uintptr_t *)getfp() :
(uintptr_t *)tcb->xcp.regs[REG_FP],
self ? NULL :
(uintptr_t *)tcb->xcp.regs[REG_EPC]);
#if CONFIG_ARCH_INTERRUPTSTACK > 15
if (self)
{
/* Only a self chain can genuinely start on the interrupt stack: it
* is per-CPU scratch space, reused on every dispatch, so a snapshot
* taken from a not-running tcb can never legitimately point into
* it.
*/
cpu = up_cpu_index();
ilimit = (uintptr_t *)((uintptr_t)g_intstacktop -
(CONFIG_ARCH_INTERRUPTSTACK * cpu));
ibase = (uintptr_t *)((uintptr_t)ilimit - CONFIG_ARCH_INTERRUPTSTACK);
BACKTRACE_ADD_RANGE(segment, ibase, ilimit, NULL);
}
#endif
#if defined(CONFIG_ARCH_KERNEL_STACK) && defined(CONFIG_ARCH_ADDRENV)
if (tcb->xcp.kstack != NULL)
{
BACKTRACE_ADD_RANGE(segment, tcb->xcp.kstack,
(uintptr_t *)((uintptr_t)tcb->xcp.kstack +
ARCH_KERNEL_STACKSIZE), NULL);
}
#endif
BACKTRACE_ADD_RANGE(segment, ubase, ulimit,
self ? NULL : tcb->addrenv_own);
#ifdef CONFIG_LIB_SYSCALL
/* The main chain above stops at the nearest ecall boundary --
* dispatch_syscall()'s own call frame has no relation to the user fp
* chain, so there is nothing for it to unwind into. xcp.sregs is
* stable (written once by dispatch_syscall(), never by reserved
* syscalls) and gives the real user call site, so splice it in as an
* independent starting point.
*/
if (insyscall)
{
segment = backtrace_new_segment(segments, &nsegments,
(uintptr_t *)tcb->xcp.sregs[REG_FP],
(uintptr_t *)tcb->xcp.sregs[REG_EPC]);
BACKTRACE_ADD_RANGE(segment, ubase, ulimit,
self ? NULL : tcb->addrenv_own);
}
#endif
return backtrace_segments(segments, nsegments, buffer, size, &skip);
}