操作系统小记(三):函数序言、调试器断点与堆栈回溯¶
Estimated time to read: 1 minute
调试 OS 实验代码时,堆栈回溯(stack unwind)是一个非常有用的工具。比如,如果能在 Trap 处理时追踪被打断的函数调用栈,就能更容易地定位问题。然而,因为 OS 实验代码混有汇编,想实现堆栈回溯需要一些额外的工作。
本节涉及的知识在《编译原理》课程中也会用到。
栈帧(Stack Frame)¶
在程序运行时,每一次函数调用都会在内存的栈(stack)上分配一块区域,用来保存该函数执行所需的上下文信息,这块区域就叫做一个栈帧(stack frame)。
一个典型的栈帧通常包含以下几类内容:
- 返回地址(Return Address):函数执行完毕后要跳回的位置。
- 前一个帧指针(Old Frame Pointer):保存调用者函数的帧指针,用于恢复上层栈的状态。
- 局部变量(Local Variables):当前函数内定义的变量。
- 被保存的寄存器(Saved Registers):如果当前函数使用了一些需要保护的寄存器(如
s0、rbx等),也会把它们的旧值保存到栈中。
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)分配出局部变量空间; - 更新帧指针(
fp或s0)。
当函数返回时,栈帧被销毁,寄存器和栈指针恢复到调用前的状态。
堆栈回溯(Stack Unwinding)¶
堆栈回溯(或称“栈回溯”、“stack trace”)是调试器、异常处理器等工具的一种机制,用来从当前函数逐级向上恢复调用路径。例如,在程序崩溃时,调试器通过读取每一层函数的栈帧,就能显示出调用链:
为了实现这种能力,调试器需要知道:
- 当前函数的栈帧在内存中的位置;
- 每个寄存器(尤其是返回地址寄存器)的保存位置;
- 如何从当前帧恢复到上一个帧。
这些信息可以通过两种方式获得:
- 传统帧指针法(frame-pointer unwinding):依靠固定寄存器(如
rbp、s0)形成链式结构,每一帧都保存上一个帧指针。 - CFI(Call Frame Information)法:汇编器通过
.cfi_*指令在目标文件中记录栈帧结构信息,调试器或异常处理器读取.eh_frame或.debug_frame段中的 CFI 数据来重建调用栈。