A stack trace (Stack Trace or Stack Backtrace) is the current call stack information printed when an exception, error, or active debugging occurs during program execution. It helps developers understand the execution state of the program at a specific moment, which is very useful for debugging and diagnosing program issues.
Using Stack Backtrace
Zephyr uses the <span>arch_stack_walk</span> function for stack backtrace, which is defined in the prototype in the <span>zephyr/include/zephyr/arch/arch_interface.h</span> file:
// Prototype of the callback function called when backtracing stack frames
// cookie is the parameter passed to arch_stack_walk, addr is the address of the current stack frame
typedef bool (*stack_trace_callback_fn)(void *cookie, unsigned long addr);
// The callback_fn will be called once for each stack frame backtrace
// thread specifies which thread to backtrace; NULL indicates the current thread
// esf can specify from which stack frame to start backtracing; NULL indicates to start from the current position
void arch_stack_walk(stack_trace_callback_fn callback_fn, void *cookie,
const struct k_thread *thread, const struct arch_esf *esf);
Below is an example of an actual backtrace function that can display stack information at the call site:
static bool print_trace_address(void *arg, unsigned long ra)
{
#ifdef CONFIG_SYMTAB
uint32_t offset = 0;
const char *name = symtab_find_symbol_name(ra, &offset);
LOG_INF("ra: %p [%s+0x%x]", (void *)ra, name, offset);
#else
LOG_INF("ra: %p", (void *)ra);
#endif
return true;
}
void __attribute__((noinline)) show_backtrace(void)
{
arch_stack_walk(print_trace_address, NULL, _current, NULL);
}
When configured with <span>CONFIG_SYMTAB=y</span>, it can print the function name and offset address corresponding to each stack level. If this option is not configured, it will directly print the address of each stack level.
Architectures Supporting Backtrace
Architectures that support stack backtrace will define the <span>ARCH_HAS_STACKWALK</span> option in their Kconfig file, for example in <span>zephyr/arch/riscv/Kconfig</span>:
config ARCH_HAS_STACKWALK
bool
default y
imply THREAD_STACK_INFO
help
Internal config to indicate that the arch_stack_walk() API is implemented
and it can be enabled.
In <span>zephyr/arch/Kconfig</span>, when <span>ARCH_HAS_STACKWALK</span> is detected, it will automatically enable <span>CONFIG_ARCH_STACKWALK</span> and <span>CONFIG_ARCH_STACKWALK_MAX_FRAMES</span>:
config ARCH_STACKWALK
bool "Compile the stack walking function"
default y
depends on ARCH_HAS_STACKWALK
help
Select Y here to compile the `arch_stack_walk()` function
config ARCH_STACKWALK_MAX_FRAMES
int "Max depth for stack walk function"
default 8
depends on ARCH_STACKWALK
help
Depending on implementation, this can place a hard limit on the depths of the stack
for the stack walk function to examine.
<span>CONFIG_ARCH_STACKWALK_MAX_FRAMES</span> has a default value of 8, meaning it can backtrace a maximum of 8 stack frames. This can be configured to a larger value in the project’s <span>prj.conf</span> file.
Currently, Zephyr only supports the following three architectures for stack backtrace:
- arm64
- riscv
- x86
Frame Pointer Configuration
When the frame pointer is disabled, the compiler no longer saves the stack frame pointer in registers. This can save a few instructions in the function prologue and epilogue code and allows the fp register to be used as a general-purpose register, which can provide better performance on architectures with limited registers.
However, on some architectures (including x86), omitting the frame pointer can make locating local variables difficult, thus affecting debugging effectiveness.
Compilation Optimization and Frame Pointer
At optimization levels of -O1 and above, gcc will automatically enable the <span>-fomit-frame-pointer</span> option. If you do not want to omit the frame pointer during optimization, you need to enable <span>CONFIG_OVERRIDE_FRAME_POINTER_DEFAULT=y</span> and <span>CONFIG_OMIT_FRAME_POINTER=n</span> to enable <span>-fno-omit-frame-pointer</span>.
The relevant configuration in <span>zephyr/CMakeLists.txt</span> is as follows:
if(CONFIG_OVERRIDE_FRAME_POINTER_DEFAULT)
if(CONFIG_OMIT_FRAME_POINTER)
zephyr_cc_option(-fomit-frame-pointer)
else()
zephyr_cc_option(-fno-omit-frame-pointer)
endif()
endif()
When <span>CONFIG_FRAME_POINTER=y</span>, it will default to selecting <span>CONFIG_OVERRIDE_FRAME_POINTER_DEFAULT</span> but not selecting <span>CONFIG_OMIT_FRAME_POINTER</span>, thus directly selecting <span>zephyr_cc_option(-fno-omit-frame-pointer)</span><span>.</span>
config FRAME_POINTER
bool "Compile the kernel with frame pointers"
select OVERRIDE_FRAME_POINTER_DEFAULT
help
Select Y here to gain precise stack traces at the expense of slightly
increased size and decreased speed.
Frame Pointer Requirements for Different Architectures
Different architectures have different requirements for frame pointer configuration in stack backtrace:
-
riscv:
<span>CONFIG_FRAME_POINTER=y</span>will enable<span>-fno-omit-frame-pointer</span>, making the backtrace results more accurate. Even if this option is not configured, the<span>arch_stack_walk</span>can still be used for backtrace, but the results may not be accurate. -
x86:
<span>CONFIG_FRAME_POINTER=y</span>will enable<span>-fno-omit-frame-pointer</span>, which is a required configuration; otherwise, the<span>arch_stack_walk</span>function cannot be used. -
arm64:
<span>CONFIG_FRAME_POINTER=y</span>will enable<span>-fno-omit-frame-pointer</span>and<span>-mno-omit-leaf-frame-pointer</span>, and will also add handling for the fp register in<span>zephyr/arch/arm64/core/vector_table.S</span>. This is also a required configuration; otherwise, the<span>arch_stack_walk</span>function cannot be used.
Use Cases for Stack Backtrace in Zephyr
Exception Handling
When configured with <span>CONFIG_EXCEPTION_STACK_TRACE=y</span>, architectures that support backtrace will automatically print the stack backtrace information at the time of a fault.
Shell Command Debugging
When configured with <span>CONFIG_KERNEL_THREAD_SHELL_UNWIND=y</span>, the shell will provide an <span>unwind</span> command to backtrace a specified thread. The usage is as follows:
Use the <span>kernel thread list</span> command to list all threads; the first hexadecimal number is the thread ID.
Scheduler: 2510 since last call
Threads:
0x3fc8fea0 deep_stack_thread
options: 0x0, priority: 7 timeout: 1251
state: sleeping, entry: 0x4200157e
Total execution cycles: 660429 (0 %)
stack size 1024, unused 464, usage 560 / 1024 (54 %)
*0x3fc904b8 shell_uart
options: 0x0, priority: 14 timeout: 0
state: queued, entry: 0x42006e4e
Total execution cycles: 18770030 (0 %)
stack size 2048, unused 668, usage 1380 / 2048 (67 %)
0x3fc90b80 sysworkq
options: 0x1, priority: -1 timeout: 0
state: pending, entry: 0x40383394
Total execution cycles: 138 (0 %)
stack size 1024, unused 716, usage 308 / 1024 (30 %)
Use <span>kernel thread unwind <thread id></span> to list the current backtrace of the specified thread; below is the backtrace of <span>deep_stack_thread</span>:
uart:~$ kernel thread unwind 0x3fc8fea0
Unwinding 0x3fc8fea0 deep_stack_thread
ra: 0x420015c8 [level_1_thread_entry+0x4a]
ra: 0x42002f14 [z_thread_entry+0x32]
Without the thread ID, it will show the backtrace of the current thread.