MIT-6.S081实验四学习记录
这个实验不是很复杂,但是需要搞清楚trap的工作原理,也就是通过ECALL从用户态进入内核态后,操作系统做了哪些事。一是为了恢复用户态ECALL之前的状态,并保证其能执行调用前的下一条语句,使用SEPC寄存器存了PC值,然后使用了trapframe保存用户态切换到内核态前的寄存器值。
首先把分支切换过来。
git fetch
git checkout traps
make clean
1、一些Q&A
首先按照要求,把fs.img文件编译一下,然后得到了 /kernel/call.asm 汇编文件,如下所示:
## /kernel/call.asm
0000000000000000 <g>:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int g(int x) {
0: 1141 addi sp,sp,-16
2: e422 sd s0,8(sp)
4: 0800 addi s0,sp,16
return x+3;
}
6: 250d addiw a0,a0,3
8: 6422 ld s0,8(sp)
a: 0141 addi sp,sp,16
c: 8082 ret
000000000000000e <f>:
int f(int x) {
e: 1141 addi sp,sp,-16
10: e422 sd s0,8(sp)
12: 0800 addi s0,sp,16
return g(x);
}
14: 250d addiw a0,a0,3
16: 6422 ld s0,8(sp)
18: 0141 addi sp,sp,16
1a: 8082 ret
000000000000001c <main>:
void main(void) {
1c: 1141 addi sp,sp,-16
1e: e406 sd ra,8(sp)
20: e022 sd s0,0(sp)
22: 0800 addi s0,sp,16
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0
2c: 7b050513 addi a0,a0,1968 # 7d8 <malloc+0xea>
30: 00000097 auipc ra,0x0
34: 600080e7 jalr 1536(ra) # 630 <printf>
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 27e080e7 jalr 638(ra) # 2b8 <exit>
...
...
0000000000000630 <printf>:
void
printf(const char *fmt, ...)
{
630: 711d addi sp,sp,-96
632: ec06 sd ra,24(sp)
634: e822 sd s0,16(sp)
636: 1000 addi s0,sp,32
638: e40c sd a1,8(s0)
63a: e810 sd a2,16(s0)
63c: ec14 sd a3,24(s0)
63e: f018 sd a4,32(s0)
640: f41c sd a5,40(s0)
642: 03043823 sd a6,48(s0)
646: 03143c23 sd a7,56(s0)
Q1:Which registers contain arguments to functions?
For example, which register holds 13 in main's call to printf?
A1:a0-a7; a2;
Q2:Where is the call to function f in the assembly code for main?
Where is the call to g? (Hint: the compiler may inline functions.)
A2:可以看到,在main里,下面这段代码:
26: 45b1 li a1,12
应该就是调用了f和g后的返回值,说明编译的时候,编译器将其作为inline函数来优化了
因此g是内联到f的,然后f内联到main。
Q3:At what address is the function printf located?
A3:jalr 1536(ra) # 630 <printf> 这一句可以看到是跳转到630 处调用printf
然后发现在0000000000000630 <printf>处定义了printf,所以地址显然。
Q4:What value is in the register ra just after the jalr to printf in main?
A4:ra寄存器里的值一定是完成printf函数调用后的下一条指令的值,这样才能跳转回来继续执行代码
因此,ra的值是38.
Q5:Run the following code.
unsigned int i = 0x00646c72;
printf("H%x Wo%s",57616, &i);
What is the output?
A5:这段代码,可以看到有两个类型的变量需要解析,一个是%x,这是十六进制,所以将57616转换为
16进制,为E110,然后是无符号整型 i,按照ASCII码,00:NULL,64:d;6c:l;72:r,
但是这里也提示了,RISC-V 是大端序的,所以输出应该是"HE110, W0rld".
Q6:In the following code, what is going to be printed after ?
(note: the answer is not a specific value.) Why does this happen? 'y='
printf("x=%d y=%d", 3);
A6:这个问题比较好解释,因为只给a1寄存器赋值3,所以a2寄存器的值就是上一次被用完保留下来的值
因此x=3,y=一个不确定的数。
2、backtrace()
这个实验主要是写一个打印栈的函数,然后按照要求定义在 /kernel/printf.c里,然后提示说要在 kernel/riscv.h 里面添加一个结构体,可以用于获取stack frames的地址。
打印格式如下:
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
然后将结构体添加到 /kernel/riscv.h中
## /kernel/riscv.h
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
然后定义backtrace函数。从上面结构体获取到的frame pointer指向当前栈帧的开始地址,sp 指向当前栈帧的结束地址。 栈从高地址往低地址拓展,所以 fp 虽然是帧开始地址,但是地址比 sp 高。
栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
就是说当前frame的返回地址是fp -8,然后下一个frame的地址是fp -16。
void
backtrace()
{
printf("backtrace:\n");
uint64 fp = r_fp();
while(fp != PGROUNDUP(fp)){
uint64 ra = *(uint64*)(fp - 8);
printf("%p\n", ra);
fp = *(uint64*)(fp - 16);
}
}
然后在sys_sleep中调用即可
uint64
sys_sleep(void)
{
int n;
uint ticks0;
backtrace(); //加在这里
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
然后使用脚本测试:
jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ ./grade-lab-traps backtrace test
make: 'kernel/kernel' is up to date.
== Test backtrace test == backtrace test: OK (1.1s)
3、Alarm
这个实验是要添加两个系统调用(sigalarm和sigreturn)。前者主要作用就是当CPU过了n个时钟周期,内核应该发起一个时钟中断,比如调用sigalarm(2,periodic) 就意味着每过两个时钟周期,就调用periodic,然后返回到中断前的地方。所以可以猜测,sigalarm(2,periodic) 负责发起定时器中断,然后做一些处理,而sigreturn 负责还原中断前的状态并返回。
test0、test1、test2
这一部分主要是添加好这两个系统调用,细节可以看实验二。
##/user/user.h
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
然后把其他的文件像实验二一样配置好,接下来在 proc.h 里进程结构体中加一些变量:间隔n、alarm函数fn、还有当前嘀嗒数ticks、用于恢复的trapframe、还有一个flag(用于记录有没有最近调用过,如果有就不要重复调用)
struct proc {
......
int alarm_interval; // 间隔n
uint64 alarm_handler; // alarm函数fn
int alarm_ticks; // 当前嘀嗒数ticks
struct trapframe *alarm_trapframe; //用于恢复的trapframe
int alarm_goingoff; //标志
};
然后 /kernel/sysproc.c 里添加这两个函数,这里sys_sigalarm类似于做一个赋值操作,真正发挥作用是在定时器发生中断,也就是 ***if(which_dev == 2)***时。
## /kernel/sysproc.c
//将间隔设置为n,回调函数设置为fn,嘀嗒数置0
uint64 sys_sigalarm(void){
int n;
uint64 fn;
if(argint(0, &n) < 0 || argaddr(0, &fn) < 0)
return -1;
struct proc *p = myproc();
p->alarm_interval = n;
p->alarm_handler = fn;
p->alarm_ticks = 0;
return 0;
}
//将当前trapframe修改为中断前的trapframe,然后嘀嗒数归0,调用flag归0
uint64 sys_sigreturn(void) {
struct proc *p = myproc();
*p->trapframe = *p->alarm_trapframe;
//memmove(p->trapframe, p->alarm_trapframe, sizeof(struct trapframe));
p->alarm_ticks = 0;
p->alarm_goingoff = 0;
return 0;
}
因为这个是进程变量,所以需要在进程创建时初始化,所以在 /kernel/proc.c 中进行初始化与销毁。
## /kernel/proc.c
static struct proc*
allocproc(void)
{
...
found:
...
if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
p->alarm_interval = 0;
p->alarm_handler = 0;
p->alarm_ticks = 0;
p->alarm_goingoff = 0;
...
return p;
}
既然分配内存时初始化,那么释放时也要记得销毁重置。
## /kernel/proc.c
static void
freeproc(struct proc *p)
{
......
if(p->alarm_trapframe)
kfree((void*)p->alarm_trapframe);
......
p->alarm_trapframe = 0;
p->alarm_interval = 0;
p->alarm_handler = 0;
p->alarm_ticks = 0;
p->alarm_goingoff = 0;
}
接下来就要在定时器发生中断时,执行alarm 函数以及记录中断前的状态(trapframe)。
## /kernel/trap.c
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
//说明调用了sisalarm函数,
if(!(p->alarm_interval == 0 && p->alarm_handler == 0)) {
//消耗完了n个CPU的嘀嗒数
if(p->alarm_ticks <= 0){
p->alarm_ticks--; //减少一个嘀嗒数,当n次中断后,查看之前有没有被调用过(标志位为0)
if(!(p->alarm_goingoff)) {
// 记录好中断前的trapframe
*p->alarm_trapframe = *p->trapframe;
// 将当前程序计数器设置为alarm函数fn
p->trapframe->epc = p->alarm_handler;
p->alarm_goingoff = 1;
}
}
}
yield();
}
到这里代码就写完了,虽然不多,但是需要深刻理解工作原理,尤其是trap的工作机制,比如说进入后恢复中断前状态、保证不被重复调用的标志位等等。
最后用脚本测试一下功能是否ok即可。
jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ make grade
== Test answers-traps.txt == answers-traps.txt: OK
== Test backtrace test ==
$ make qemu-gdb
backtrace test: OK (6..s)
== Test running alarmtest ==
$ make qemu-gdb
(1.9s)
== Test alarmtest: test0 ==
alarmtest: test0: OK
== Test alarmtest: test1 ==
alarmtest: test1: OK
== Test alarmtest: test2 ==
alarmtest: test2: OK
== Test usertests ==
$ make qemu-gdb
usertests: OK (78.2s)
== Test time ==
time: OK
Score: 85/85
总结
这个实验重在理解trap机制,如果有基础的一定知道,最重要的就是进入内核后,要能够恢复中断前的状态继续执行,然后这个实验也帮助回顾了一下系统调用怎么写,可以说收获颇丰。一定要看lecture 6的视频,Robert讲的非常好,手把手教你如何gdb调试。
[1]: https://pdos.csail.mit.edu/6.S081/2020/labs/traps.html
[2]: https://zhuanlan.zhihu.com/p/626826395
[3]: https://blog.miigon.net/posts/s081-lab4-traps/