arch/sim: walk frame-pointer chain for non-running tasks in up_backtrace

The previous up_backtrace() relied entirely on host_backtrace() (a thin
wrapper around glibc's backtrace()), which can only unwind the host
thread that calls it.  As a result, when assert / dump_tasks() walked the
task list and called sched_dumpstack() for every task, every task other
than the currently-running one returned a zero-length backtrace, and the
output was silently dropped.  In practice this meant that on sim only
the crashing task ever produced a usable trace.

Fix this by walking the frame-pointer chain ourselves whenever the
target tcb is not the running task.  Because sim's setjmp/longjmp is
provided by NuttX itself (libs/libc/machine/sim/arch_setjmp_*.S) and not
by host libc, the rbp/rsp/rip (or arm fp/sp/pc) saved in tcb->xcp.regs
are plain unmangled pointers, identical across Linux, macOS and Windows
hosts.  The frame layout ([fp]=prev fp, [fp+1]=return address) is also
shared by every host ABI sim supports (x86, x86_64, ARM, ARM64).

The walker validates that fp lies inside the task's stack and is
properly aligned, and stops when fp[0] is NULL, so a corrupted stack
cannot make us read out of bounds.  The running-task path is unchanged
and still uses host_backtrace() so DWARF unwinding through host
libraries continues to work.

Requires CONFIG_FRAME_POINTER=y so the compiler emits a usable fp link.

Signed-off-by: Lingao Meng <menglingao@xiaomi.com>
This commit is contained in:
Lingao Meng 2026-06-03 19:35:01 +08:00 • committed by Xiang Xiao
parent b8cd0435b2
commit 2a6059a206

View file

@ -25,38 +25,146 @@
****************************************************************************/
#include <nuttx/arch.h>
#include <nuttx/irq.h>
#include <sched/sched.h>
#include <setjmp.h>
#include <stdint.h>
#include <string.h>
#include "sim_internal.h"
/****************************************************************************
* Private Functions
****************************************************************************/
/****************************************************************************
* Name: backtrace_fp
*
* Description:
* Walk the frame-pointer chain to recover return addresses for a task
* that is not currently running on the host thread. This relies on the
* uniform frame layout used by every host ABI sim supports (x86, x86_64,
* ARM, ARM64):
* [fp + 0] = previous fp
* [fp + 1] = return address
* where one slot is sizeof(uintptr_t).
*
* Because sim's setjmp implementation is provided by NuttX itself
* (libs/libc/machine/sim/arch_setjmp_*.S) rather than by host libc, the
* saved fp/sp/pc in tcb->xcp.regs are plain pointers and contain no
* pointer-mangling, so the chain can be followed directly on Linux,
* macOS and Windows hosts.
*
****************************************************************************/
nosanitize_address
static int backtrace_fp(uintptr_t *base, uintptr_t *limit,
uintptr_t *fp, uintptr_t *pc,
void **buffer, int size, int *skip)
{
int i = 0;
if (pc != NULL)
{
if ((*skip)-- <= 0)
{
buffer[i++] = pc;
}
}
while (i < size)
{
/* Validate fp lies within [base, limit) and is properly aligned. */
if (fp < base || fp >= limit ||
((uintptr_t)fp & (sizeof(uintptr_t) - 1)) != 0)
{
break;
}
if (fp[0] == 0)
{
break;
}
if ((*skip)-- <= 0)
{
buffer[i++] = (void *)fp[1];
}
fp = (uintptr_t *)fp[0];
}
return i;
}
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: up_backtrace
*
* Description:
* up_backtrace() returns a backtrace for the TCB, in the array pointed to
* by buffer. A backtrace is the series of currently active function
* calls for the program. Each item in the array pointed to by buffer is
* of type void *, and is the return address from the corresponding stack
* frame.
*
* For the running task we keep using host_backtrace() (which wraps
* glibc's backtrace()) so that we benefit from DWARF unwinding through
* host libraries. For any other task host_backtrace() cannot help -- it
* only knows how to walk the host thread that is calling it -- so we
* instead walk the frame-pointer chain starting from the registers saved
* in tcb->xcp.regs by setjmp() at the last context switch. This requires
* CONFIG_FRAME_POINTER=y so that the compiler emits a usable fp link.
*
****************************************************************************/
nosanitize_address
int up_backtrace(struct tcb_s *tcb, void **buffer, int size, int skip)
{
void *buf[skip + size];
int ret = 0;
int i;
struct tcb_s *rtcb = running_task();
irqstate_t flags;
int ret;
if (tcb == running_task())
if (size <= 0 || buffer == NULL)
{
return 0;
}
if (tcb == NULL)
{
tcb = rtcb;
}
if (tcb == rtcb)
{
void *buf[skip + size];
ret = host_backtrace(buf, skip + size);
if (ret <= skip)
{
return ret < 0 ? ret : 0;
}
ret -= skip;
memcpy(buffer, &buf[skip], ret * sizeof(void *));
return ret;
}
if (ret <= skip)
{
return ret < 0 ? ret : 0;
}
flags = enter_critical_section();
ret -= skip;
for (i = 0; i < ret; i++)
{
buffer[i] = buf[skip + i];
}
ret = backtrace_fp((uintptr_t *)tcb->stack_base_ptr,
(uintptr_t *)((uintptr_t)tcb->stack_base_ptr +
tcb->adj_stack_size),
(uintptr_t *)tcb->xcp.regs[JB_FP],
(uintptr_t *)tcb->xcp.regs[JB_PC],
buffer, size, &skip);
leave_critical_section(flags);
return ret;
}