6.S081-Lab 4 traps

前言

本篇博客是6.S081课程的第四次Lab分析,做这次Lab之前,需要理解Xv6操作系统系统调用时用户空间和内核空间之间的切换过程,还需要清楚函数嵌套调用时运行时栈的栈帧的结构。Lab的地址:https://pdos.csail.mit.edu/6.828/2021/labs/traps.html

RISC-V assembly

Xv6操作系统的系统调用通过trampoline来实现用户空间和内核空间之间的跳转,trampoline页面是在内核地址空间和用户地址空间均有相同的地址映射,也就是说:trampoline页面在物理地址空间只有一份(且是可读可执行的代码),而在内核地址空间和用户地址空间中,均将MAXVA - PGSIZE的地址(也就是第一个页面)映射到了这段代码。

这样做的好处就是,在执行trampoline中的代码时,执行内核页表和用户进程页表不会使得程序崩溃,这也就是为什么这个页面叫做trampoline(蹦床)的原因了。

为了理解Xv6的代码,需要了解部分risc-v的汇编,如果不深究的话,我认为只需要知道函数调用参数传递通过的是a0-a7寄存器,函数调用返回值通过a0寄存器返回。其他很多指令和x86汇编类似。

Backtrace

这道题要求打印出当前函数调用栈的情况,这里需要知道在Xv6中,函数调用的栈帧如下图所示:

我们知道函数的调用栈是向低地址增长,在每一个函数调用的栈帧中,首先是函数调用的返回地址,然后是一个指向上一个栈帧的指针fp,然后是一些需要被保护的寄存器(callee saved register)。这个栈帧和别的操作系统可能会有些区别,比如我之前有一篇文章说函数调用过程:机器级解释函数调用过程

因此这里为了将函数调用栈所有的返回地址Return Address打印出来,只需要遍历当前的栈就行了。

首先需要在kernel/riscv.h中加入获取当前栈指针fp的函数:

1
2
3
4
5
6
7
8
9
// get current frame pointer from s0 register
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x));
return x;
}

然后在kernel/defs.h中添加backtrace函数声明:

1
void backtrace(void);

最重要的是在kernel/printf.c中添加函数实现,系统级编程中存在大量的强制类型转换,需要头脑清醒的知道自己在干什么!这里是遍历当前的栈,通过fp指针在栈帧之间跳转,并打印出栈帧中的函数返回地址。

1
2
3
4
5
6
7
8
9
10
// print current function stack
void backtrace(void)
{
printf("backtrace:\n");
uint64 fp = r_fp();
while (fp < PGROUNDUP(fp)) {
printf("%p\n", *(uint64*)(fp-8)); // got return address
fp = *(uint64*)(fp - 16);
}
}

最后不要忘了在kernel/sysproc.c中的sys_sleep函数中调用backtrace函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint64
sys_sleep(void)
{
int n;
uint ticks0;

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);
backtrace();
return 0;
}

Alarm

我们知道,操作系统每隔一个时间片(tick)就会触发定时器中断,从而操作系统重新获得对CPU的控制权,这样可以防止恶意程序一直占着CPU不释放。这道题要求我们实现一个简易的周期执行函数。

我们通过系统调用sigalarm(interval, handler)通知内核,内核每interval个ticks之后调用handler函数。 当程序调用sigalarm(0, 0)时,内核需要终止上述行为。

此外,需要要求handler函数在返回时调用sigreturn()系统调用,我们需要借助这一点来实现断点的恢复,也就是恢复到执行handler之前的地方。

最后还要避免handler函数的重入,因此我们也需要采取相应的措施来判断当前是否在执行handler函数。

首先添加系统调用,在user/usys.pl中加入:

1
2
entry("sigalarm");
entry("sigreturn");

user/user.h中加入函数声明:

1
2
3
// add two new syscall for Lab:Alarm
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

kernel/syscall.h中加入系统调用号:

1
2
#define SYS_sigalarm 22
#define SYS_sigreturn 23

kernel/syscall.c中加入以下代码:

1
2
3
4
5
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);

[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,

为了实现上面的功能,需要在进程结构中存储相关的状态,在kernel/proc.h中加入以下5个变量:

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
// Per-process state
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID

// wait_lock must be held when using this:
struct proc *parent; // Parent process

// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
/*** for lab ***/
int alarm_interval; // every alarm_interval call alarm handler
void (*alarm_handler)(); // alarm handler
int ticks; // timer ticks
struct trapframe *saved_trapframe; // save trapframe when exec alarm handler
int alarm_handling; // if non-zero, alarm handler is being exec
};

alarm_intervalalarm_handler存储用户sigalarm调用的参数,ticks记录当前进程使用的时间片总数;saved_trapframe用于在执行alarm_handler函数之前,保存当前的寄存器,便于后面断点恢复。最后一个变量alarm_handling用于标志当前是否正在执行alarm_handler,以防止函数重入。

不要忘了在进程初始化时对这些变量初始化,在kernel/proc.c中的allocproc函数中加入:

1
2
3
4
5
6
7
8
9
10
// initialize alarm varibles
p->alarm_handler = 0;
p->alarm_interval = 0;
p->ticks = 0;
p->alarm_handling = 0;
if ((p->saved_trapframe = (struct trapframe *)kalloc()) == 0) {
freeproc(p);
release(&p->lock);
return 0;
}

kernel/proc.cpreeproc函数中加入以下代码以销毁saved_trapframe:

1
2
if (p->saved_trapframe)
kfree((void*)p->saved_trapframe);

最后修改kernel/trap.c中的usertrap函数:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
int which_dev = 0;

if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");

// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);

struct proc *p = myproc();

// save user program counter.
p->trapframe->epc = r_sepc();

if(r_scause() == 8){
// system call

if(p->killed)
exit(-1);

// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;

// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on();

syscall();
} else if((which_dev = devintr()) != 0){
// ok
if (which_dev == 2) {
// timer interrupt
p->ticks ++;
if (p->alarm_interval != 0 && p->ticks % p->alarm_interval == 0) {
if (p->alarm_handling == 0) {
p->alarm_handling = 1;
// save origin trapframe
cpytrapframe(p->saved_trapframe, p->trapframe);
// if arrival time interval, set return address to alarm_handler
p->trapframe->epc = (uint64)p->alarm_handler;
}
}
}
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}

if(p->killed)
exit(-1);

// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();

usertrapret();
}

基本逻辑是,首先每次定时器中断我们都让p->ticks ++,当用户设置了定时器任务并到期之后,设置p->alarm_handling并将trapframe备份至saved_trapframe,然后将epc寄存器的值改为用户提供的handler,这样系统调用返回的时候,就会去执行用户提供的handler函数了。

上面的cpytrapframe函数实现如下,就是将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
31
32
33
34
35
36
37
38
void cpytrapframe(struct trapframe* dst, struct trapframe* src) {
dst->kernel_hartid = src->kernel_hartid;
dst->kernel_satp = src->kernel_satp;
dst->kernel_sp = src->kernel_sp;
dst->kernel_trap = src->kernel_trap;
dst->a0 = src->a0;
dst->a1 = src->a1;
dst->a2= src->a2;
dst->a3 = src->a3;
dst->a4 = src->a4;
dst->a5 = src->a5;
dst->a6 = src->a6;
dst->a7 = src->a7;
dst->epc = src->epc;
dst->ra = src->ra;
dst->sp = src->sp;
dst->gp = src->gp;
dst->tp = src->tp;
dst->t0 = src->t0;
dst->t1 = src->t1;
dst->t2 = src->t2;
dst->s0 = src->s0;
dst->s1 = src->s1;
dst->s2 = src->s2;
dst->s3 = src->s3;
dst->s4 = src->s4;
dst->s5 = src->s5;
dst->s6 = src->s6;
dst->s7 = src->s7;
dst->s8 = src->s8;
dst->s9 = src->s9;
dst->s10 = src->s10;
dst->s11 = src->s11;
dst->t3 = src->t3;
dst->t4 = src->t4;
dst->t5 = src->t5;
dst->t6 = src->t6;
}

最后补上两个系统调用的实现,在kernel/sysproc.c中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint64
sys_sigalarm(void)
{
int interval;
uint64 handler;
if (argint(0, &interval) < 0)
return -1;
if (argaddr(1, &handler) < 0)
return -1;
struct proc* proc = myproc();
proc->alarm_interval = interval;
proc->alarm_handler = (void (*)(void))handler;

return 0;
}

uint64
sys_sigreturn(void)
{
struct proc* p = myproc();
cpytrapframe(p->trapframe, p->saved_trapframe);
p->alarm_handling = 0;
return 0;
}

其中sigalarm只是简单的将用户的参数保存到proc中,sigreturn将trapframe恢复,然后将handler执行标志赋0。

测试结果

2022/6/14日测试通过

参考文献

  1. https://pdos.csail.mit.edu/6.828/2021/schedule.html