MIT6.S081 Lab 4 Traps
Lab 4 Traps
Backtrace(moderate)
backtrace:打印栈中错误发生位置上方的所有函数调用
编译器会在每个栈帧中放置一个栈帧指针,用于保存调用者栈帧指针的地址。您的反向跟踪应使用这些帧指针在堆栈中向上追踪,并在每个栈帧中打印保存的返回地址。
提示
-
在 kernel/defs.h 中添加 backtrace 原型,以便在 sys_sleep 中调用 backtrace。
-
GCC 编译器会将当前执行函数的帧指针存储在寄存器 s0 中。将以下函数添加到 kernel/riscv.h:
static inline uint64 r_fp() { uint64 x; asm volatile("mv %0, s0" : "=r" (x) ); return x; }并在回溯中调用该函数来读取当前帧指针。该函数使用内联汇编读取 s0。
-
这些讲义中有一张堆栈帧布局的图片。请注意,返回地址位于堆栈帧指针的固定偏移量(-8)处,而保存的帧指针位于帧指针的固定偏移量(-16)处。
-
xv6 为 xv6 内核中的每个堆栈分配一个 PAGE 对齐地址的页面。您可以使用 PGROUNDDOWN(fp) 和 PGROUNDUP(fp) 计算堆栈页面的顶部和底部地址(参见 kernel/riscv.h)这些数字有助于反向跟踪终止其循环。
一旦你的backtrace正常工作,就可以在 kernel/printf.c 的 panic 中调用它,这样当内核panic时,你就能看到内核的backtrace。
完成
kernel/printf.c
void backtrace(void){
uint64* fp;
uint64 retAddr;
fp = (uint64*)r_fp(); // 获取当前fp
uint64 top = PGROUNDUP((uint64)fp);
printf("backtrace:\n");
while((uint64)fp < top){
retAddr = *(fp - 1); // 获取返回地址
printf("%p\n", retAddr);
fp = (uint64*)*(fp - 2); // 指向前一fp
}
}
(uint64)fp - 8 == fp - 1,fp是unit64类型的指针,fp - 1实际为fp向下移动sizeof(uint64),即8
虽然是栈指针但回过来看似乎直接定义 uint64 fp 更方便一些
结果
backtrace追踪的栈地址:

使用addr2line查看地址对应函数:

Alarm(hard)
在这个实验中,将为 xv6 添加一项功能,在进程占用 CPU 时定期发出警报。可能进程需要限制占用的CPU时间,或希望在计算的同时定期采取某些行动。更一般地说,您将实现一种原始形式的用户级中断/故障处理程序;例如,你可以使用类似的方法来处理应用程序中的页面故障。
您应该添加一个新的 sigalarm(interval, handler) 系统调用。如果应用程序调用 sigalarm(n,fn),那么程序每消耗 n 个 CPU 时间后,内核就会调用应用程序函数fn。当 fn 返回时,应用程序应继续运行。在 xv6 中,"tick "是一个相当随意的时间单位,由硬件定时器产生中断的频率决定。如果应用程序调用 sigalarm(0,0),内核就应停止产生alarm调用。
您将在 xv6 仓库中找到 user/alarmtest.c 文件。将其添加到 Makefile 中。在添加 sigalarm 和 sigreturn 系统调用(见下文)之前,该文件无法正确编译。
alarmtest 在 test0 中调用 sigalarm(2,periodic),要求内核每隔 2 个ticks强制调用 periodic(),然后旋转一段时间。您可以在 user/alarmtest.asm 中查看 alarmtest 的汇编代码,这对调试可能很方便。当 alarmtest 产生这样的输出且 usertests 也正确运行时,您的解决方案就是正确的:
完成后,您的解决方案将只有几行代码,但要做到正确可能很困难。我们将使用origin仓库中的 alarmtest.c 版本测试您的代码。你可以修改 alarmtest.c 来帮助调试,但要确保通过origin仓库中的 alarmtest 所有测试。
test0: invoke handler
首先修改内核,跳转到用户空间的警报处理程序,这将导致 test0 打印 "alarm!"。先不用担心 "alarm!"输出后会发生什么;如果程序在打印 "alarm!"后崩溃,现在也没关系。
提示
-
您需要修改 Makefile,以便将 alarmtest.c 作为 xv6 用户程序编译。
-
在 user/user.h 中正确的声明是
int sigalarm(int ticks, void (*handler)()); int sigreturn(void); -
更新 user/usys.pl(生成 user/usys.S)、kernel/syscall.h 和 kernel/syscall.c,允许 alarmtest 调用 sigalarm 和 sigreturn 系统调用。
-
目前,您的 sys_sigreturn 应该只返回 0。
-
sys_sigalarm() 应该在 proc 结构(kernel/proc.h)的新字段中存储警报间隔和处理函数的指针。
-
您需要记录自上次调用(或下次调用前)进程的警报处理程序以来已经过去了多少个刻钟;为此,您还需要在 struct proc 中添加一个新字段。您可以在 proc.c 文件的 allocproc() 中初始化 proc 字段。
-
每滴答一次,硬件时钟就会强制中断一次,由 kernel/trap.c 中的 usertrap() 处理。
-
只有在有定时器中断的情况下,您才会想操作进程的闹钟滴答声;您需要类似于
if(which_dev == 2) ... -
只有当进程有计时器未执行时,才调用警报函数。请注意,用户报警函数的地址可能是 0(例如,在 user/alarmtest.asm 中,periodic 位于地址 0)。
-
您需要修改 usertrap(),以便当进程的警报间隔到期时,用户进程执行处理函数。当 RISC-V 上的陷阱返回用户空间时,用户空间代码恢复执行的指令地址由什么决定?
-
如果告诉 qemu 只使用一个 CPU,使用 gdb 查看陷阱会更方便。
make CPUS=1 qemu-gdb -
如果 alarmtest 打印出 "alarm!",则表示成功
完成
sys_sigalarm获取检查参数,并设置proc字段。当ticks满足要求时通过修改p->trapframe->epc来达到执行新的代码的效果
sysproc.c
uint64 sys_sigalarm(void){
int ticks;
uint64 hander;
if(argint(0, &ticks) < 0) return -1;
if(argaddr(1, &hander) < 0) return -1;
if(ticks <= 0) return -1;
struct proc *p = myproc();
p->ticks = ticks;
p->hander = hander;
return 0;
}
uint64 sys_sigreturn(void){
return 0;
}
trap.c/usertrap中添加定时器中断检测和对应代码
if(which_dev == 2 && p->ticks != 0){
p->passedticks++;
if(p->passedticks == p->ticks){
// execuate hander
p->trapframe->epc = p->hander;
p->passedticks = 0;
}
}
结果
test0 passed
test1/test2(): resume interrupted code
可能是 alarmtest 在打印 "alarm!"后在 test0 或 test1 中崩溃,或者是 alarmtest打印 "test1 失败",或者是 alarmtest 退出时没有打印 "test1 通过"。要解决这个问题,必须确保警报处理程序完成后,控制返回到用户程序最初被定时器中断中断的指令。必须确保寄存器内容恢复到中断发生时的值,这样用户程序才能在警报发生后不受干扰地继续运行。最后,每次警报触发后都应重置警报计数器,以便定期调用处理程序。
作为一个起点,我们为您做出了一个设计决定:用户警报处理程序在完成后必须调用 sigreturn 系统调用。请看 alarmtest.c 中的 periodic 示例。这意味着您可以在 usertrap 和 sys_sigreturn 中添加代码,使用户进程在处理完警报后正常恢复。
提示
- 您的解决方案需要保存和恢复寄存器--您需要保存和恢复哪些寄存器才能正确恢复被中断的代码? 提示:会有很多)。
- 当计时器关闭时,让 usertrap 在 struct proc 中保存足够的状态,以便 sigreturn 能正确返回被中断的用户代码。
- 防止处理程序的重入调用,如果处理程序尚未返回,内核不应再次调用它。test2 测试了这一点。
通过 test0、test1 和 test2 后,运行 usertests,确保没有破坏内核的其他部分。
完成
添加标志位is_handering,中断检测中检测是否重入,sigreturn检测是否被hander进程调用
中断准备执行hander时保存trapframe,在sigreturn时恢复
uint64
sys_sigalarm(void){ // get arguments and set alarm
int ticks;
uint64 hander;
if(argint(0, &ticks) < 0) return -1;
if(argaddr(1, &hander) < 0) return -1;
if(ticks < 0 || hander < 0) return 0;
struct proc *p = myproc();
if(ticks == 0 && hander == 0){ // disable
// use ticks as flag
p->ticks = 0;
}
else if(ticks > 0){ // enable
p->ticks = ticks;
p->hander = hander;
}
else
return -1;
return 0;
}
uint64
sys_sigreturn(void){
struct proc *p = myproc();
if(p->is_handering == 1 && p->ticks != 0){ // is handering and enable
// restore registers
memmove(p->trapframe, p->savedtrapframe, sizeof(struct trapframe));
// reset
p->is_handering = 0;
p->passedticks = 0;
// back
usertrapret();
}
else{
return -1;
}
return 0;
}
if(which_dev == 2){
if(p->ticks > 0 && p->is_handering == 0){ // alarm enabled and no hander not returned
p->passedticks++;
if(p->passedticks >= p->ticks){
// store trapframe
memmove(p->savedtrapframe, p->trapframe, sizeof(struct trapframe));
// execuate hander
p->is_handering = 1;
p->trapframe->epc = p->hander;
}
}
else{
// give up the CPU if this is a timer interrupt.
yield();
}
}
结果
alarmtest通过

usertest未通过
最终发现问题出在给savedtrapframe分配空间,在freeproc里没释放
全部测试通过

浙公网安备 33010602011771号