Lab Traps
前置知识
一、stack
- stack储存方式和heap相反,它是向下增长的,有两个重要的寄存器,fp和sp。fp指向当前frame的顶(上),sp指向的是当前frame的底(下)。
- stack中函数的return address和上一个fp的地址储存在当前fp的固定偏移量上,通过lab hints我们可以知道一个是-8,一个是-16。
Task RISC-V assembly
根据实验提示,我们make fs.img出user/call.asm来分析。
1.
1 | 24: 4635 li a2,13 |
可以看出a2储存了13。
2.
1 | 26: 45b1 li a1,12 |
可以看出编译器使用了inline优化,直接算出了12并储存在a1中。
3.
1 | 30: 00000097 auipc ra,0x0 |
可以看出printf在640位置上。注意auipc这个指令,它把0这个立即数左移12位并加上pc(+4)的值后赋给了ra,然后加上了1552找到了printf
jalr这个指令会把当前pc加4(指向下一条pc指令)并赋给ra
1
2unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);输出的是HE110 World。前一个%x是十六进制转换,后一个因为riscv是”little-endian”,他会从后往前转换字符。
1
printf("x=%d y=%d", 3);
因为printf的format储存在a0,3存储在a1,所以当它试图print第二个数时,就会把a2中的无关值输出出来。
Task Backtrace
这个task要求我们实现一个回溯的syscall,它通过遍历整个stack来输出所有stack frame中的函数地址。
- 根据提示添加r_fp(),并在sys_sleep中添加backtrace()。
- 因为fp的存在和上一个fp地址的储存,我们可以通过类似遍历链表的方式将stack从下到上遍历一遍,每遍历到一个frame我们就print出它储存的return address。注意添加def.h
1
2
3
4
5
6
7
8
9
10
11
12
13// printf.c
void backtrace(void)
{
printf("backtrace:\n");
uint64 cur_fp = r_fp(); // 当前fp
uint64 top_fp = PGROUNDUP(cur_fp); // PGROUNDUP可以找到当前stack的顶
while(top_fp > cur_fp)
{
uint64 va = *(pte_t *)(cur_fp - 0x08);
printf("%p\n", va);
cur_fp = *(pte_t *)(cur_fp - 0x10); // 跳转到前一个fp
}
}
Task Alarm
这个task要求我们实现一个alarm功能,每当clock中断执行一定的次数后,就会调用这个alarm函数(注意这个函数是xv6自带的),输出一个alarm!。
- 首先,我们需要在proc结构体里保存三个需要的参数,分别是clock需要执行的次数,alarm函数的地址和clock已经执行的次数,并把它们在allocproc初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13// proc.h
...
char name[16]; // Process name (debugging)
int alarm_pe;
void (*alarm_handler)();
int ticks;
...
...
// proc.c/ allocproc()
p->alarm_handler = 0;
p->alarm_pe = 0;
p->ticks = 0;
... - 当一个用户函数要初始化alarm功能时,他需要呼叫sys_sigalarm。我们需要实现这个syscall。关于sys_sigreturn,你可以先写上它并只让它返回0。(还记得那些步骤吗,此处省略了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21uint64
sys_sigalarm(void)
{
struct proc *prc = myproc();
int period;
if (argint(0, &period) < 0) // 读入周期
return -1;
uint64 p;
if (argaddr(1, &p) < 0) // 读入alarm地址
return -1;
prc->alarm_pe = period;
prc->alarm_handler = (void(*)())p;
prc->ticks = 0;
return 0;
}
uint64
sys_sigreturn(void){
return 0;
} - 我们需要修改usertrap,每次发生clock interrupt的时候给ticks加一,当ticks等于alarm_pe的时候就将alarm地址写入异常中断寄存器epc中。然后系统就会中断并自动调用alarm。此时第一个小任务完成。
1
2
3
4
5
6if(which_dev == 2) {
p->ticks ++;
if (p->alarm_pe != 0 && p->ticks == p->alarm_pe && p->isalarm == 0) {
p->trapframe->epc = (uint64)p->alarm_handler;//set the address of syscall alarm
p->ticks = 0;//it can also be set in sys_sigreturn
} - 因为alarm调用被设为一个中断,所以必须将调用alarm前所有的user寄存器保存起来,防止中断后对用户寄存器的复写。同时,因为xv6并行,在处理完一个alarm之前不能进行另一个alarm的处理,因此我们需要一个标志来记录当前是否有alarm在处理。因此需要在proc里再添加两个参数。它们需要被初始化,trapframe用完要释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// proc.h
int isalarm;
struct trapframe *atrapframe;
// proc.c/ allocproc()
p->isalarm = 0;
// proc.c/ freeproc()
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
if(p->atrapframe)
kfree((void*)p->atrapframe); // 释放
p->trapframe = 0;
p->atrapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
p->isalarm = 0; // 恢复
} - 修改usertrap来保存寄存器并设置isalarm。
1
2
3
4
5
6
7
8
9
10if(which_dev == 2) {
p->ticks ++;
if (p->alarm_pe != 0 && p->ticks == p->alarm_pe && p->isalarm == 0) {
p->isalarm = 1;//set alarm flag
*p->atrapframe = *p->trapframe;//备份寄存器
p->trapframe->epc = (uint64)p->alarm_handler;//set the address of syscall alarm
p->ticks = 0;//it can also be set in sys_sigreturn
}
yield();
} - 最后,在sigreturn中,我们需要恢复所有的寄存器,恢复isalarm。至此,lab traps结束。
1
2
3
4
5
6
7
8
9uint64
sys_sigreturn(void){
struct proc* p = myproc();
if (p->isalarm){
p->isalarm = 0;
*p->trapframe = *p->atrapframe;//恢复寄存器
}
return 0;
}