一、实验内容:
Part A RISC-V assembly
见问题回答
Part B Backtrace
- Add the prototype for your
backtrace()
tokernel/defs.h
so that you can invokebacktrace
insys_sleep
.
- Add the following function to
kernel/riscv.h
, and call this function inbacktrace
to read the current frame pointer.r_fp()
uses in-line assembly to reads0
.
在backtrace中获取s0的值,并用于读取第一个所需的返回地址:
- 循环,向上遍历帧指针
实现思路:
正如第一个地址的操作,每次都是打印fp-8所指向的内容。故可将打印写作循环里,并调整fp为上一个的帧指针,即fp-16所指向的内容。同时,通过PGROUNDDOWN(fp)
和PGROUNDUP(fp)
来作为循环终止的判断条件。
具体实现:
测试:
Part C Alarm
test0: invoke handler
- 准备工作
Modify the Makefile to causealarmtest.c
to be compiled as an xv6 user program.
The right declarations to put in user/user.h
Update user/usys.pl (which generates user/usys.S), kernel/syscall.h, and kernel/syscall.c to allow alarmtest
to invoke the sigalarm and sigreturn system calls.
- For now,
sys_sigreturn
should just return zero. sys_sigalarm()
should store the alarm interval and the pointer to the handler function in new fields in theproc
structure (inkernel/proc.h
) . And keep track of how many ticks have passed since the last call (or are left until the next call) to a process’s alarm handler; add a new field instruct proc
for this too.
- Initialize
proc
fields inallocproc()
inproc.c
. - Every tick, the hardware clock forces an interrupt, which is handled in
usertrap()
inkernel/trap.c
inif(which_dev == 2) ...
. And modifyusertrap()
so that when a process’s alarm interval expires, the user process executes the handler function.
实现思路:
首先,时间中断处要进行ticks的++,如何判断ticks是否和interval相同,若是则需执行handler函数。执行该函数的具体做法便是将handler的地址赋给程序计数器,即寄存器epc。要注意前面给的sigalarm(0,0)是停止周期调用,其实n==0的意义不大,ticks会恒大于等于0,故要判断interval是否为0,不是才要做上述操作。
具体实现:
测试:
可见test0正常通过,但test1不行
test1/test2()/test3(): resume interrupted code
- Save and restore registers
实现思路:
类似于trapframe的构造,再构造一个save_trapframe用于保存之前的所有寄存器。像trapframe,故需要在allocproc()/freeproc()
中进行alloc和free。当需要做程序跳转时,将当前的trapframe的内容放到save_trapframe中。
具体实现:
sigreturn
should:
- correctly return to the interrupted user code.
- prevent re-entrant calls to the handler----if a handler hasn’t returned yet, the kernel shouldn’t call it again.
- return a0.
实现思路:
借助于之前的save_trapframe,直接将其内容给trapframe就行了。因为epc也在其中,程序计数器会自动跟着做,同时a0也可以轻易的读到。至于保证handler执行时不被调用,只需要把置0这一步放到sigreturn中即可,因为执行的条件是要ticks==interval,只要不置0,handler就不会再被执行。
测试:
Make grade
添加answers-traps.txt和time.txt,运行make grade
二、问题回答:
(1) 函数的参数包含在哪些寄存器中?例如在main对printf的调用中,哪个寄存器保存13?
从图中可以看出,函数的参数包含在a0~a7寄存器中。在main对printf的调用中,保存13的是a2寄存器。
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
(2) Main的汇编代码中对函数f的调用在哪里?对g的调用在哪里?(Hint:编译器可能内联函数)
编译器进行了函数内联,直接将f(8)+1的值计算了出来,即12
26: 45b1 li a1,12
(3) 函数printf位于哪个地址?
0x65a
34: 62a080e7 jalr 1578(ra) # 65a <printf>
(4) 在jalr到main中的printf之后,寄存器ra中存储的值是?
ra寄存器保存的是发生函数调用后应该返回的地址值,调用printf后应该返回执行的地址为0x38,故ra存储的即是0x38。
(5) 运行以下代码:
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
输出是什么?输出取决于 RISC-V是 little-endian的。如果 RISC-V 是 big-endian,怎样设置来产生相同的输出?是否需要更改i和57616为不同的值?
如果 RISC-V 是 big-endian:
57616不需要更改,因为它是作为一个整体的进制转换进行输出的
但i需要更改为726c6400
(6) 在下面的代码中,会打印出什么?(注意:答案不是特定值)为什么会发生这种情况?printf("x=%d y=%d", 3);
先看正常的代码,printf("x=%d y=%d",3,3);
,具体是将两个3分别放在a1和a2寄存器中,printf再去读寄存器里的值。但如果后面一个3缺省,但a2寄存器仍然可读取,所以y读的便是a2寄存器里的值,因为a2寄存器没有给定值,所以会打印一个不确定的值,当时a2寄存器是什么就打印什么。
三、问题解决:
【问题1】在Backtrace实验中,最开始对于提示中“指向**返回地址加上8的地址”理解有误,导致输出的保存地址不正确
【解决】画了一张简图(图太丑,就不放了)来理解,指针与指向具体内容的关系。文字说明就是,s0放的是fp的地址,该地址-8即是返回地址(打印内容)的地址,-16就是上一个fp的地址。
【问题2】完成实验进行make grade后,发现usertests失败,显示丢失了一些free page,应该是有些东西在结束后未被free掉。
【解决】经过漫长的排查,发现是save_trapframe与trapframe之间的拷贝造成的。最开始,是直接将两个进行复制来拷贝。后来才想起来这两个是指针,只拷贝指针的话,确实指针之前指向的内容没被释放掉。
此处改之前是p->save_trapframe = p->trapframe
四、实验感想:
- 这个实验因为第一周事情比较多,导致拖到第二周已经忘了之前是咋理解的。alarm实验不亏是hard,虽然做完了回想一下也没有那么多复杂的代码,但写的时候就有种拆东墙补西墙的感觉。先写test0的时候很顺手,而且看着后面的提示也没有很多。结果后面的测试大概就是test1能过了test3不能过,test3能过了test1又出问题,最后以为成功了,usertests出问题。
- 这次实验的提示相比于之前的保姆式提示,略过了一些具体实现的介绍,比如添加一个新的.c文件到makefile里以及后面的一系列操作,还比如要保存和恢复寄存器并没有告诉你使用什么样的数据结构,这正是实验逐步深入的体现。
- 通过具体的函数跳转并返回这一机制的实验,我捡回了很多关于计算机组成的知识,也对这些知识有了更深刻的理解,比如程序计数器的执行原理、寄存器的使用等等。