在学完 Traps 一节课后,了解了在 OS 中,用户态是如何转换到内核态,再转回用户态的。
Task: Backtrace
这个任务目标是实现 backtrace()
函数,它用来打印当前堆栈中的所有函数调用信息。
为了完成这个任务,我们需要遍历函数调用栈中的每个栈帧(frame),并打印每个 frame 中的 Return Address 信息。因此,我们需要看一下函数调用堆栈的结构:
上图展示了一个堆栈结构,堆栈从上向下生长,其中每个 stack frame 中有两个关键字段:
- Return Address:这个函数的返回地址,也是在 backtrace() 函数中需要打印的信息;
- To Prev Frame:指向上一个 frame 的一个指针
为了遍历所有 frames,我们需要认识两个关键的寄存器:
- SP 寄存器:指向当前函数栈的底部并代表了当前栈帧的位置
- FP 寄存器:它指向当前函数栈帧的顶部
在这里,我们可以借助 FP 的值来遍历所有栈帧,而且我们所需要的两个关键字段都相对于栈帧顶部具有固定的 offset。
根据实验提示,我们现在 kernel/riscv.h 文件中添加一个用于获取 FP 寄存器值的函数:
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
在 kernel/printf.c 中添加 backtrace() 函数的实现:
void
backtrace()
{
printf("backtrace:\n");
uint64 fp = r_fp(); // FP 寄存器值
uint64 base = PGROUNDUP(fp); // 栈底地址
while (fp < base) {
// 向前遍历 frame,直到达到 base
printf("%p\n", *((uint64*)(fp - 8))); // 打印 Return Address 字段值
fp = *((uint64*)(fp - 16)); // prev frame
}
}
在 kernel/defs.h 添加 backtrace 函数的声明:
void backtrace(void);
在 sysproc.c 中的 sys_sleep 函数中添加对 backtrace 函数的调用:
uint64
sys_sleep(void)
{
int n;
uint ticks0;
if(argint(0, &n) < 0)
return -1;
backtrace();
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
这样,就可以在 make qemu
后执行 bttest
来测试:
按照官网的提示测试后,如果 backtrace() 函数没问题,就可以在 kernel/printf.c 的 panic()
中加入对 backtrace 的函数调用了,这样当程序 panic 时会打印当前的堆栈信息:
void
panic(char *s)
{
pr.locking = 0;
printf("panic: ");
printf(s);
printf("\n");
backtrace();
panicked = 1; // freeze uart output from other CPUs
for(;;)
;
}