MIT 6.1810 Lab: traps
lab网址:https://pdos.csail.mit.edu/6.828/2022/labs/traps.html
xv6Book:https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf
写前思考
我们都知道,在用户进程执行系统调用时,内核代替用户进程执行,那么这种代替是一种什么样的代替。这个过程是还属于用户进程,只是执行的是内核态的内核代码;还是说创造了一个内核线程,执行代码;还是只有一个内核进程,执行为每个进程提供服务。希望我通过这一节的学习,明白这个问题。
trampoline.S 文件解读
从文件的命名来看,.S
,.s
,.asm
都是汇编语言文件。C语言在编译过程中会生成,预处理文件.i
, 汇编语言文件.s
, 目标文件.o
。这里的.s
是编译C语言得到的汇编语言文件,不包含预处理语句。而.S
、.asm
包含预处理语句。
.section trampsec
是一个汇编器指令,用于将接下来的指令和数据放入名为 trampsec 的节(section)中。.globl trampoline
定义全局符号,将 trampoline
标记为一个全局可见的符号。这样,trampoline
可以在链接阶段被其他文件引用。trampoline
标志着接下来的指令是 trampoline
的开始。.align 4
确保接下来的指令和数据在内存中按照 4 字节对齐。.globl uservec
和uservec
同上。
.section trampsec
.globl trampoline
trampoline:
.align 4
.globl uservec
uservec:
stvec
是一个寄存器,由内核写入(kernel/trap.c
)处理中断的程序入口地址。使用用户的页表意味着接下来执行的每条指令的地址都是用户的虚拟地址空间。sscratch
用来保存必要寄存器,因为中断需要保护现场,保护现场的操作也需要寄存器来协助完成,如果所有的寄存器都需要保护,那么保护现场的操作就没办法进行,因此需要用sscratch
来保存必要寄存器。TRAPFRAME
是一个固定的宏值,每个进程虚拟地址空间中此处都是TRAPFRAME
。
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# save user a0 in sscratch so
# a0 can be used to get at TRAPFRAME.
csrw sscratch, a0
# each process has a separate p->trapframe memory area,
# but it's mapped to the same virtual address
# (TRAPFRAME) in every process's user page table.
li a0, TRAPFRAME
# save the user registers in TRAPFRAME
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
......
......
sd t6, 280(a0)
user a0
在t0
中,括号的a0
跟上文相同是p->trapframe
。接下来sp
变成了kernel_sp
,上文将user_sp
放到了a0+48
,这里kernel_sp
从a0+8
读出。
关于tp
,tp
是RISC-V中的一个寄存器,xv6保证其在内核模式中保存hartid
(这是CPU核心的唯一编号),这是由于当用户进程陷入内核后,内核需要获取用户进程的struct proc
。然而这不能通过在内核代码中设置一个全局变量,然后在陷入内核时向该变量赋值。这是因为内核的代码是共用的,而且由多个cpu核心同时运行,相互独立的部分只有寄存器和内核栈。因此一个合适的方法,就是用一个寄存器保存CPU核心的编号,当该进程运行时,struct cpu
中会保存当前进程的struct proc
(由调度程序设置),在内核程序的任何位置遍历struct cpu
,与hartid
比较就能获取正确的struct cpu
,此时struct cpu
中struct proc
就是当前进程的标识结构体,因为当前进程此刻正在这个CPU上执行。使用tp
的原因是RISC-V只允许hartid
的读取在机器模式,因此tp
在内核空间做此固定用处,在用户程序部分不做这个限制。于是hartid
保存在进程虚拟空间的trapframe
中,当陷入内核时,会读出hartid
保存在tp
中。
usertrap
是使用内核地址空间后的中断程序,sfence.vma
用于刷新TLB
,最后跳转到usertrap()
。
# save the user a0 in p->trapframe->a0
csrr t0, sscratch
sd t0, 112(a0)
# initialize kernel stack pointer, from p->trapframe->kernel_sp
ld sp, 8(a0)
# make tp hold the current hartid, from p->trapframe->kernel_hartid
ld tp, 32(a0)
# load the address of usertrap(), from p->trapframe->kernel_trap
ld t0, 16(a0)
# fetch the kernel page table address, from p->trapframe->kernel_satp.
ld t1, 0(a0)
# wait for any previous memory operations to complete, so that
# they use the user page table.
sfence.vma zero, zero
# install the kernel page table.
csrw satp, t1
# flush now-stale user entries from the TLB.
sfence.vma zero, zero
# jump to usertrap(), which does not return
jr t0
我们可以在kernel/proc.h
中看到trapframe
的定义。
// per-process data for the trap handling code in trampoline.S.
// sits in a page by itself just under the trampoline page in the
// user page table. not specially mapped in the kernel page table.
// uservec in trampoline.S saves user registers in the trapframe,
// then initializes registers from the trapframe's
// kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
// usertrapret() and userret in trampoline.S set up
// the trapframe's kernel_*, restore user registers from the
// trapframe, switch to the user page table, and enter user space.
// the trapframe includes callee-saved user registers like s0-s11 because the
// return-to-user path via usertrapret() doesn't return through
// the entire kernel call stack.
struct trapframe {
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // saved user program counter
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 40 */ uint64 ra;
......//All kinds of registers
/* 280 */ uint64 t6;
};
在用户进程时,发生中断的程序流程如下图
RISC-V assembly
题目要求我们阅读user/call.c
和user/call.asm
回答问题。
Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
risc-v常用a0-a7
保存参数,printf使用了a0-a2
,其中a2
保存13
。
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: 7b850513 addi a0,a0,1976 # 7e0 <malloc+0xe6>
30: 00000097 auipc ra,0x0
34: 612080e7 jalr 1554(ra) # 642 <printf>
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 28e080e7 jalr 654(ra) # 2c8 <exit>
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.)
main函数没有调用f,g函数,因为编译器直接计算出了,f(8)+1=12。
At what address is the function printf located?
ra+1554或者642
What value is in the register ra just after the jalr to printf in main?
main
函数jalr
后的下一条指令,即li a0,0
Run the following code.What is the output?If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
会输出E110
,0x00646c72
。大小端不会影响57616
的顺序,但会影响字符串的顺序。因设置为0x726c6400
。
In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?
printf("x=%d y=%d", 3);
printf 将尝试解释堆栈上的下一个值作为第二个整数参数
Backtrace
这部分希望我们实现backtrace
,可以通过s0
寄存器获得栈底,我们熟知一般栈底的加地址为上一个调用者传递的参数,栈底的减地址为该函数定义的变量,其中在risc-v中,s0-8
为返回地址,s0-16
为上一个栈的栈底指针。由于xv6内核栈只占一页,可以判断是否指针位于同一页作为循环结束条件。
diff --git a/kernel/defs.h b/kernel/defs.h
index a3c962b..4b82fc2 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -80,6 +80,7 @@ int pipewrite(struct pipe*, uint64, int);
void printf(char*, ...);
void panic(char*) __attribute__((noreturn));
void printfinit(void);
+void backtrace(void);
// proc.c
int cpuid(void);
diff --git a/kernel/printf.c b/kernel/printf.c
index 1a50203..5e0e535 100644
--- a/kernel/printf.c
+++ b/kernel/printf.c
@@ -122,6 +122,7 @@ panic(char *s)
printf("panic: ");
printf(s);
printf("\n");
+ backtrace();
panicked = 1; // freeze uart output from other CPUs
for(;;)
;
@@ -133,3 +134,14 @@ printfinit(void)
initlock(&pr.lock, "pr");
pr.locking = 1;
}
+
+void
+backtrace(void)
+{
+ uint64 traceptr = r_fp();
+ uint64 begin = PGROUNDDOWN(traceptr);
+ while(PGROUNDDOWN(traceptr) == begin){
+ printf("%p\n",*(uint64*)(traceptr-8));
+ traceptr = *(uint64*)(traceptr-16);
+ }
+}
diff --git a/kernel/riscv.h b/kernel/riscv.h
index 20a01db..7313868 100644
--- a/kernel/riscv.h
+++ b/kernel/riscv.h
@@ -361,3 +361,11 @@ typedef uint64 *pagetable_t; // 512 PTEs
// Sv39, to avoid having to sign-extend virtual addresses
// that have the high bit set.
#define MAXVA (1L << (9 + 9 + 9 + 12 - 1))
+
+static inline uint64
+r_fp()
+{
+ uint64 x;
+ asm volatile("mv %0, s0" : "=r" (x) );
+ return x;
+}
\ No newline at end of file
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index 3b4d5bd..bc705a3 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -67,6 +67,7 @@ sys_sleep(void)
sleep(&ticks, &tickslock);
}
release(&tickslock);
+ backtrace();
return 0;
}
Alarm
这部分需要我们添加两个系统调用,sigalarm
和sigreturn
。一个难点,当进程从内核返回时,如何使进程执行注册的函数,执行完该函数后程序应怎样继续。观察测试程序,就可以发现sigreturn
就是执行该函数的返回操作。于是可以想到一个流程,当usertrap
发现计时完成后,将程序的执行流转到通过sigalarm
注册的handler
,handler
最后一个语句使用sigreturn
返回原先的执行流。
如何在不破坏源现场的情况下,让用户程序执行handler
呢?当usertrap
发现计时完成时,内核有一个现场,用户程序的现场保存在TRAPFRAME
中,如果现在把内核的现场保存在内核栈中,转而执行handler
,那么sigreturn
执行时,又是一个系统调用,会破坏内核栈。内核栈的问题可以让内核无需返回,问题不大。考虑到sigreturn
执行时,必会破坏TRAPFRAME
,因此可以让内核将原先的TRAPFRAME
保存在用户栈中,当sigreturn
执行时,内核将用户栈中的数据恢复到TRAPFRAME
中。不能放入内核栈,内核栈每次中断都会被重写。最后sigreturn
通过usertrapret
返回原执行流。另外作者觉得可以通过在用户的虚拟地址空间中再开辟一块来保存现场,作者不做这个尝试。可以使用一个变量,防止嵌套调用handler
,这里不需要加锁,因为从用户空间到内核空间的中断,在用户空间时是不可能嵌套发生的,只有usertrap
处理调用handler
,而kerneltrap
不处理。
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(killed(p))
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 sepc, scause, and sstatus,
// so enable only now that we're done with those registers.
intr_on();
syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
setkilled(p);
}
if(killed(p))
exit(-1);
// handle interval
if(which_dev == 2 && p->interval != 0){
p->left_ticks--;
if(p->left_ticks == 0){
p->left_ticks = p->interval;
if(p->handling == 0){
p->handling = 1;//no need to lock
exechandler();
}
}
}
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}
void
exechandler(void){
struct proc *p = myproc();
// we're about to switch the destination of traps from
// kerneltrap() to usertrap(), so turn off interrupts until
// we're back in user space, where usertrap() is correct.
intr_off();
// send syscalls, interrupts, and exceptions to uservec in trampoline.S
uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
w_stvec(trampoline_uservec);
//save trapframe in userstack
p->trapframe->sp -= sizeof(*(p->trapframe));
p->savedsp = p->trapframe->sp;
copyout(p->pagetable,p->trapframe->sp,(char *)(p->trapframe),sizeof(*(p->trapframe)));
// set up trapframe values that uservec will need when
// the process next traps into the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
// set up the registers that trampoline.S's sret will use
// to get to user space.
// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
//there is no use to disable it
w_sstatus(x);
// set S Exception Program Counter to the saved user pc.
w_sepc((uint64)p->interval_handler);
// tell trampoline.S the user page table to switch to.
uint64 satp = MAKE_SATP(p->pagetable);
// jump to userret in trampoline.S at the top of memory, which
// switches to the user page table, restores user registers,
// and switches to user mode with sret.
uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64))trampoline_userret)(satp);
}
uint64 sys_sigalarm(void){
//store the alarm interval and the pointer to the handler function in new fields in the proc structure
struct proc* p = myproc();
int interval;
uint64 handler;
argint(0, &interval);
argaddr(1,&handler);
p -> interval = interval;
p -> left_ticks = interval;
p -> interval_handler = handler;
return 0;
}
uint64 sys_sigreturn(void){
struct proc* p = myproc();
p->handling = 0;
copyin(p->pagetable,(char *)(p->trapframe),p->savedsp,sizeof(*(p->trapframe)));
p->trapframe->sp += sizeof(*(p->trapframe));
//systemcall return value is stored in a0
return p->trapframe->a0;
}
diff --git a/Makefile b/Makefile
index ded5bc2..c0a984c 100644
--- a/Makefile
+++ b/Makefile
@@ -200,7 +200,8 @@ endif
ifeq ($(LAB),traps)
UPROGS += \
$U/_call\
- $U/_bttest
+ $U/_bttest\
+ $U/_alarmtest
endif
ifeq ($(LAB),lazy)
diff --git a/kernel/defs.h b/kernel/defs.h
index 4b82fc2..0903771 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -148,6 +148,7 @@ void trapinit(void);
void trapinithart(void);
extern struct spinlock tickslock;
void usertrapret(void);
+void exechandler(void);
// uart.c
void uartinit(void);
diff --git a/kernel/proc.c b/kernel/proc.c
index 959b778..6d0506f 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -146,6 +146,11 @@ found:
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
+ p->interval = 0;
+ p->left_ticks = 0;
+ p->interval_handler = 0;
+ p->savedsp = 0;
+ p->handling = 0;
return p;
}
diff --git a/kernel/proc.h b/kernel/proc.h
index d021857..05b5631 100644
--- a/kernel/proc.h
+++ b/kernel/proc.h
@@ -104,4 +104,10 @@ struct proc {
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
+
+ uint interval; //interval
+ uint left_ticks; //ticks left until the next call
+ uint64 interval_handler; //interval handler
+ uint64 savedsp;
+ int handling;
};
diff --git a/kernel/syscall.c b/kernel/syscall.c
index ed65409..c34b372 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -101,6 +101,8 @@ extern uint64 sys_unlink(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_close(void);
+extern uint64 sys_sigalarm(void);
+extern uint64 sys_sigreturn(void);
// An array mapping syscall numbers from syscall.h
// to the function that handles the system call.
@@ -126,6 +128,8 @@ static uint64 (*syscalls[])(void) = {
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
+[SYS_sigalarm] sys_sigalarm,
+[SYS_sigreturn] sys_sigreturn,
};
void
diff --git a/kernel/syscall.h b/kernel/syscall.h
index bc5f356..7b88b81 100644
--- a/kernel/syscall.h
+++ b/kernel/syscall.h
@@ -20,3 +20,5 @@
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
+#define SYS_sigalarm 22
+#define SYS_sigreturn 23
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index bc705a3..606e1b1 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -92,3 +92,24 @@ sys_uptime(void)
release(&tickslock);
return xticks;
}
+uint64 sys_sigalarm(void){
+//store the alarm interval and the pointer to the handler function in new fields in the proc structure
+ struct proc* p = myproc();
+ int interval;
+ uint64 handler;
+ argint(0, &interval);
+ argaddr(1,&handler);
+ p -> interval = interval;
+ p -> left_ticks = interval;
+ p -> interval_handler = handler;
+ return 0;
+}
+
+uint64 sys_sigreturn(void){
+ struct proc* p = myproc();
+ p->handling = 0;
+ copyin(p->pagetable,(char *)(p->trapframe),p->savedsp,sizeof(*(p->trapframe)));
+ p->trapframe->sp += sizeof(*(p->trapframe));
+ //systemcall return value is stored in a0
+ return p->trapframe->a0;
+}
diff --git a/kernel/trap.c b/kernel/trap.c
index 512c850..070309a 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c
@@ -76,6 +76,18 @@ usertrap(void)
if(killed(p))
exit(-1);
+ // handle interval
+ if(which_dev == 2 && p->interval != 0){
+ p->left_ticks--;
+ if(p->left_ticks == 0){
+ p->left_ticks = p->interval;
+ if(p->handling == 0){
+ p->handling = 1;//no need to lock
+ exechandler();
+ }
+ }
+ }
+
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
@@ -83,6 +95,54 @@ usertrap(void)
usertrapret();
}
+void
+exechandler(void){
+ struct proc *p = myproc();
+
+ // we're about to switch the destination of traps from
+ // kerneltrap() to usertrap(), so turn off interrupts until
+ // we're back in user space, where usertrap() is correct.
+ intr_off();
+
+ // send syscalls, interrupts, and exceptions to uservec in trampoline.S
+ uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
+ w_stvec(trampoline_uservec);
+
+ //save trapframe in userstack
+ p->trapframe->sp -= sizeof(*(p->trapframe));
+ p->savedsp = p->trapframe->sp;
+ copyout(p->pagetable,p->trapframe->sp,(char *)(p->trapframe),sizeof(*(p->trapframe)));
+ // set up trapframe values that uservec will need when
+ // the process next traps into the kernel.
+ p->trapframe->kernel_satp = r_satp(); // kernel page table
+ p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
+ p->trapframe->kernel_trap = (uint64)usertrap;
+ p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
+
+ // set up the registers that trampoline.S's sret will use
+ // to get to user space.
+
+ // set S Previous Privilege mode to User.
+ unsigned long x = r_sstatus();
+ x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
+ x |= SSTATUS_SPIE; // enable interrupts in user mode
+ //there is no use to disable it
+ w_sstatus(x);
+
+ // set S Exception Program Counter to the saved user pc.
+ w_sepc((uint64)p->interval_handler);
+
+ // tell trampoline.S the user page table to switch to.
+ uint64 satp = MAKE_SATP(p->pagetable);
+
+ // jump to userret in trampoline.S at the top of memory, which
+ // switches to the user page table, restores user registers,
+ // and switches to user mode with sret.
+ uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
+ ((void (*)(uint64))trampoline_userret)(satp);
+}
+
+
//
// return to user space
//
diff --git a/user/user.h b/user/user.h
index 4d398d5..9426153 100644
--- a/user/user.h
+++ b/user/user.h
@@ -22,6 +22,8 @@ int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
+int sigalarm(int ticks, void (*handler)());
+int sigreturn(void);
// ulib.c
int stat(const char*, struct stat*);
diff --git a/user/usys.pl b/user/usys.pl
index 01e426e..fa548b0 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -36,3 +36,5 @@ entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
+entry("sigalarm");
+entry("sigreturn");
最后
个人觉得这一节的内容相当精彩,通过这次实验,对系统调用、中断的底层实现有了非常细致、深刻的了解。对于写前的问题,内核可以理解是一个与进程无关的程序,每个进程共用一份内核代码。内核在启动时,由bootloader装载进内存,这一部分内存就被内核永久占用,内核初始化过程中使用kalloc
获取的空间是不包含内核初始装载的空间的。每个进程对于内核使用的区别只有内核栈,当进程陷入内核时,依然是这个进程在执行内核代码,只不过进入了特权模式,而且内核代码的执行不会受到用户程序的控制,内核中不能随意使用全局变量,因为全局变量是共用的。
最后也是顺利通过所有测试。