Skip to content

操作系统小记(三):函数序言、调试器断点与堆栈回溯

Estimated time to read: 1 minute

调试 OS 实验代码时,堆栈回溯(stack unwind)是一个非常有用的工具。比如,如果能在 Trap 处理时追踪被打断的函数调用栈,就能更容易地定位问题。然而,因为 OS 实验代码混有汇编,想实现堆栈回溯需要一些额外的工作。

本节涉及的知识在《编译原理》课程中也会用到。

栈帧(Stack Frame)

在程序运行时,每一次函数调用都会在内存的栈(stack)上分配一块区域,用来保存该函数执行所需的上下文信息,这块区域就叫做一个栈帧(stack frame)

一个典型的栈帧通常包含以下几类内容:

  1. 返回地址(Return Address):函数执行完毕后要跳回的位置。
  2. 前一个帧指针(Old Frame Pointer):保存调用者函数的帧指针,用于恢复上层栈的状态。
  3. 局部变量(Local Variables):当前函数内定义的变量。
  4. 被保存的寄存器(Saved Registers):如果当前函数使用了一些需要保护的寄存器(如 s0rbx 等),也会把它们的旧值保存到栈中。

RISC-V 调用约定就具体描述了栈帧的结构。其中,栈帧指针(frame pointer)作为可选的结构:

The presence of a frame pointer is optional. If a frame pointer exists, it must reside in x8(s0); the register remains callee-saved.

当一个函数被调用时,CPU 会:

  • 把返回地址压入栈;
  • 可能保存旧的帧指针;
  • 移动栈指针(sp)分配出局部变量空间;
  • 更新帧指针(fps0)。

当函数返回时,栈帧被销毁,寄存器和栈指针恢复到调用前的状态。

堆栈回溯(Stack Unwinding)

堆栈回溯(或称“栈回溯”、“stack trace”)是调试器、异常处理器等工具的一种机制,用来从当前函数逐级向上恢复调用路径。例如,在程序崩溃时,调试器通过读取每一层函数的栈帧,就能显示出调用链:

#0  crash() at main.c:42
#1  do_work() at worker.c:18
#2  main() at main.c:10

为了实现这种能力,调试器需要知道:

  • 当前函数的栈帧在内存中的位置;
  • 每个寄存器(尤其是返回地址寄存器)的保存位置;
  • 如何从当前帧恢复到上一个帧。

这些信息可以通过两种方式获得:

  1. 传统帧指针法(frame-pointer unwinding):依靠固定寄存器(如 rbps0)形成链式结构,每一帧都保存上一个帧指针。
  2. CFI(Call Frame Information)法:汇编器通过 .cfi_* 指令在目标文件中记录栈帧结构信息,调试器或异常处理器读取 .eh_frame.debug_frame 段中的 CFI 数据来重建调用栈。

CFI 汇编指令

函数序言