xv6-lab-trap
6.s081 fa20 lab4:一个很有趣,码量不大但是烧脑的lab
traps
调试小技巧
MIT 6.S081 xv6 调试指北_xv6 调试 exec-CSDN博客
调试用户程序,可以在exec打一个断点,原理是使用sh调试时,都是用exec执行二进制程序,所以可以在这里打断点
然后在sh里面输入用户程序,在gdb里面调试,但是我试图 file user/_alarmtest 显示can not access memory,待解决
测试方法
1 | |
backtrace
此实验目的是panic时,通过backtrace打印出程序调用堆栈
最终效果如下,打印出了地址,但是还需要你手动转换

手动转换:
addr2line是一个命令行工具,用于将地址转换成源码文件名和行号。它通常用于调试内核或其他程序时,根据地址查找对应的源代码位置。这个工具对于理解程序在运行时遇到的错误位置非常有用
1 | |

最后有一个 Challenge:让你不止打印出地址,而是打印出addr2line的内容
xv6 默认的编译模式会在生成的可执行文件中,含有调试信息,其中包含了所有符号的名称以及其对应的地址。理论上 backtrace 可以做类似 addr2line 的操作,通过解析可执行文件本身附带的调试信息获得地址对应的源码文件以及行号。这里跳过该 challenge
实现

通过这个栈的布局来写程序,可以看一下Lec5
你只需要通过fp来读出ra,然后让fp等于他的上一个fp
那么在哪里结束?xv6的stack至多为1页,所以读到该页的最上面就ok了
1 | |
Alarm
每当程序消耗了CPU时间达到n个 tick,内核应当使应用程序函数fn被调用,当fn返回时,应用应当在它离开的地方恢复执行
如果一个程序调用了sigalarm(0, 0),系统应当停止生成周期性的报警调用
test 0
实现在tick达到设定的值之后,调用给定的函数
初次尝试
初次尝试,我想的是把用户的 handler 地址翻译为他在 物理内存中的地址,时间到了的时候就直接通过这个物理地址(函数指针)来调用,为什么不行呢?
- 你在内核态能够执行用户态的代码吗?内核和用户的地址的映射是不一样的
- 调用函数,函数的栈那些都是用户栈,你怎么办?
调用
1 | |
报错
1 | |
根据sepc来看,就是调用的时候出错了
所以只能转向trap的本身,修改 trapframe 中的 epc 寄存器,来让这个clockintr返回的时候直接返回到我们的handler代码,这样就调用了一次处理函数
1 | |
test1
上面的做法有个问题,会导致test1失败
由于每次使用 ecall 进入中断处理前,都会使用 trapframe 存储当时的寄存器信息,包括时钟中断。因此 trapframe 在每次中断前后都会产生变换,如果要恢复状态,需要额外存储 handler 执行前的 trapframe(即更改返回值为 handler 前的 trapframe),这样,无论中间发生多少次时钟中断或是其他中断,保存的值都不会变。
好了,现在我们的目的就是从用户态程序的角度来说,保证调用 handler 函数前后的状态没有变化(对他而言是透明的),必须确保完成报警处理程序后返回到用户程序最初被计时器中断的指令执行。必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以不受干扰地继续运行
思考问题
在test0当中,从内核中返回的时候返回地址是periodic,在periodic执行结束之后conut加一,循环中可以判断出count不为0了之后break并且输出提示信息。test0当中并未直接调用periodie,请问这个过程中是怎么从periodic跳转到test0的循环当中继续执行的?
要保存哪些寄存器,全部保存吗?
我们修改了epc为handler之后,是哪一个函数会破坏我们trapframe中保存的值,首先我们知道的是要保存原来的epc,因为我们修改了epc,然后其他的trapframe里面的寄存器此时都还是原样。在这时我们返回到用户态,此时寄存器除了epc之外都被trapframe恢复为原样了。实际上在这一步,我们省去了call handler之前的寄存器保存工作,因为我们直接到了调用函数部分,所以需要事先保存caller-saved寄存器,其实这应该是编译器来做的东西,但是由于我们自定义修改了epc,所以需要我们自己来做。然后handler最后通过系统调用sigreturn进入内核,我们把保存的寄存器恢复
如果在handler执行,以及sigreturn之前发生了时钟中断,并且又达到了handler处理的间隔,怎么办?这种情况你仅仅保存caller-saved寄存器还可以吗?
大致流程:

- 在你运行代码的时候,时不时的会产生定时器中断,进入内核。当clockintr调用n次,达到设置的警告
- 内核在处理定时器中断时,我们修改其epc,使其返回到用户态 handler 函数
- 回到handler函数继续执行,根据代码其内部调用 sigreturn
- 由于 sigreturn 是一个 syscall,再通过syscall陷入内核
- 此时到了内核,我们怎么想办法此时是返回到被打断的地方重新执行
实现:
- proc 结构体新增一个字段:
struct trapframe *alarmframe;,用来保存clockintr时的 trapframe,方便后续返回 - 然后对于这个 alarmframe 要进行页分配(allocproc函数)以及页回收(freeproc函数)
- 在第2步,返回之前,保存此时的 trapframe 做一个快照,方便后面恢复
- 在第4步,恢复快照,恢复到最初第1步被打断的地方
完成之后test1 pass
test2:报错
1 | |
分析一下为什么报这个错误
由于slow_handler中的循环使其运行缓慢,如果此期间发生另一个闹钟中断,将再次触发处理程序
所以我们要保证:在前一个闹钟处理程序完成之前,闹钟处理程序不应该再次被调用。
在 proc.h 添加一个 in_handler 字段,在调用handler之前判断,该字段是否为0表示没有被占用,从此才能调用
其实有点类似上了一个锁的过程。
思考:如果在执行handler时关时钟中断是否可行呢?
要注意sigreturn的返回值应该是 p->trapframe->a0
测试通过:

总结:
这个lab也不能算完全独立完成,中间也参考了别人的实现。有些实现还不够优雅,比如说对于alarmframe,不应该直接复制trapframe,有些寄存器你是不用保存的。你要保证的是调用handler前后,所有寄存器状态没有改变。这个地方其实只用保存caller-saved寄存器就可以了,然后调用handler,在sigretun的时候恢复这些寄存器。
还有个问题没有想清楚,就是到底是在哪一步会破坏寄存器?是在 handler吧
还需要理一下,什么是编译器自动保存的,什么是trap保存的,这样不会多保存寄存器。但是对于这样一个os,其实多保存寄存器带来的开销是可以忽略的,而且思维难度会降低很多。。
一个问题,需要自己的回答
请问大家一个问题,我在做 fa20 的 lab4 traps 实验的 alarm 部分,已经通过测试了但还是有点疑惑
在 test0 当中(此时还没有实现 sigreturn 保存寄存器,sigreturn 只是简单的 return 0),这部分我的实现是:修改从内核中返回的时候 epc 寄存器的值为 handler函数(periodic)的地址,根据 periodic 代码,在periodic执行结束之后 count+1
也就是我一直在 test0 的循环中,然后某时候要触发对 periodic 的调用,test0 的循环中可以判断出 count 不为 0 了之后 break 并且输出提示信息。那么触发 periodic 调用之后我又是怎么返回到这个循环里面的呢?
我本来以为是修改 epc 之后返回到用户态,然后就一直往下开始顺着 pc = epc 这条执行流执行,然后后面重新进入了test0,但是从输出可以看到test0只被执行了一次,所以不太明白这个地方