Zephyr 堆栈回溯

堆栈回溯(Stack Trace 或 Stack Backtrace)是程序在运行时,当发生异常、错误或主动调试时,打印出的当前调用栈(Call Stack)信息。它能够帮助开发者了解程序在特定时刻的执行状态,对于调试和诊断程序问题非常有用。

使用堆栈回溯 链接到标题

Zephyr 使用 arch_stack_walk 函数进行堆栈回溯,该函数的原型定义在 zephyr/include/zephyr/arch/arch_interface.h 文件中:

// 回溯栈帧时调用注册函数的原型
// cookie 为 arch_stack_walk 传入的参数,addr 为当前栈帧的地址
typedef bool (*stack_trace_callback_fn)(void *cookie, unsigned long addr);

// 每回溯一层栈帧,将会调用一次 callback_fn
// thread 指定对哪个线程进行回溯,当为 NULL 时表示当前线程
// esf 可以指定从哪一层栈帧开始回溯,当为 NULL 时表示从当前位置开始
void arch_stack_walk(stack_trace_callback_fn callback_fn, void *cookie,
          const struct k_thread *thread, const struct arch_esf *esf);

下面是一个实际的回溯函数示例,可以显示调用位置的堆栈信息:

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);
}

当配置 CONFIG_SYMTAB=y 时,可以打印出每层堆栈对应的函数名和偏移地址。如果没有配置该选项,则直接打印每层堆栈的地址。

支持回溯的架构 链接到标题

支持堆栈回溯的架构会在其 Kconfig 文件中定义 ARCH_HAS_STACKWALK 选项,例如在 zephyr/arch/riscv/Kconfig 中:

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.

zephyr/arch/Kconfig 中,当检测到 ARCH_HAS_STACKWALK 时,会自动开启 CONFIG_ARCH_STACKWALKCONFIG_ARCH_STACKWALK_MAX_FRAMES

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.

CONFIG_ARCH_STACKWALK_MAX_FRAMES 默认值为 8,即最多只能回溯 8 层栈帧。可以在项目的 prj.conf 文件中将其配置为更大的值。

目前 Zephyr 仅支持以下三种架构进行堆栈回溯:

  • arm64
  • riscv
  • x86

帧指针配置 链接到标题

关闭帧指针(frame pointer)后,编译器不再将栈帧指针保存在寄存器中。这样可以在函数的前序(prologue)和后序(epilogue)代码中节省几条指令,并将 fp 寄存器用作通用寄存器,这在寄存器数量有限的架构上可以带来较好的性能提升。

但在某些架构(包括 x86)上,省略帧指针会使定位局部变量变得困难,从而影响调试效果。

编译优化与帧指针 链接到标题

在优化等级为 -O1 及以上时,gcc 会自动启用 -fomit-frame-pointer 选项。如果不希望在优化时省略帧指针,需要通过配置 CONFIG_OVERRIDE_FRAME_POINTER_DEFAULT=yCONFIG_OMIT_FRAME_POINTER=n 来启用 -fno-omit-frame-pointer

zephyr/CMakeLists.txt 中的相关配置如下:

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()

CONFIG_FRAME_POINTER=y 时,会默认选中 CONFIG_OVERRIDE_FRAME_POINTER_DEFAULT 但不选中 CONFIG_OMIT_FRAME_POINTER,因此会直接选择 zephyr_cc_option(-fno-omit-frame-pointer)

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.

不同架构的帧指针要求 链接到标题

不同的体系架构对堆栈回溯的帧指针配置有不同的要求:

  • riscvCONFIG_FRAME_POINTER=y 会启用 -fno-omit-frame-pointer,使回溯结果更加精确。即使不配置该选项,也能使用 arch_stack_walk 进行回溯,但结果可能不准确。

  • x86CONFIG_FRAME_POINTER=y 会启用 -fno-omit-frame-pointer,这是必须配置的选项,否则不能使用 arch_stack_walk 函数。

  • arm64CONFIG_FRAME_POINTER=y 会启用 -fno-omit-frame-pointer-mno-omit-leaf-frame-pointer,同时会在 zephyr/arch/arm64/core/vector_table.S 中增加对 fp 寄存器的处理。这也是必须配置的选项,否则不能使用 arch_stack_walk 函数。

Zephyr 中堆栈回溯的使用场景 链接到标题

异常处理 链接到标题

当配置了 CONFIG_EXCEPTION_STACK_TRACE=y 时,支持回溯的体系结构会在发生故障(fault)时自动打印当时的堆栈回溯信息。

Shell 命令调试 链接到标题

在配置了 CONFIG_KERNEL_THREAD_SHELL_UNWIND=y 的情况下,shell 会提供一个 unwind 命令用于对指定线程进行回溯。使用方法如下:

使用 kernel thread list 命令列出所有线程,最前面的十六进制数是线程 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 %)

kernel thread unwind <thread id> 列出指定线程当前的 backtrace,下面就是列出deep_stack_thread的 backtrace

uart:~$ kernel thread unwind 0x3fc8fea0
Unwinding 0x3fc8fea0 deep_stack_thread
ra: 0x420015c8 [level_1_thread_entry+0x4a]
ra: 0x42002f14 [z_thread_entry+0x32]

不带 thread id 就是当前线程的 backtrace。