MIT 6.S081 Lab4 Traps
课程知识
RISC和CISC
xv6运行在RISC-V处理器上,RISC-V是精简指令集(RISC),和传统以x86-64为代表的复杂指令集(CISC)存在一下区别:
RISC拥有更少的指令数量,CISC需要向后兼容,指令数目会不断变大RISC指令功能简单,可以减少CPU执行时间,CISC指令功能复杂,指令周期较长
栈
xv6每次调用函数,系统都会创建一个栈帧Stack Frame,系统通过移动栈指针Stack Pointer来完成Stack Frame的空间分配。栈从高地址向低地址增长,Stack Poiner需要向下移动来创建一个新的Stack Frame。xv6栈结构图如下所示:

每个Stack Frame均包含Retrun Address和指向前一个Frame的指针,同时会保存寄存器和一些本地变量,系统维护两个寄存器SP(Stack Pointer)和FP(Frame Pointer),SP指向当前Frame的底部,FP指向当前Frame的顶部,可以通过FP找到每个栈中的固定位置的Return Address和指向前一个Frame的指针,保证函数正确调用和返回。
xv6的trap机制
从用户空间到内核空间的切换被称为陷入trap,trap是异常控制流的一种,trap分为三类 系统调用,异常和设备中断。
xv6的trap处理过程,以系统调用为例:

-
用户请求系统调用,将系统调用号写入
a7寄存器,并调用ecall指令。1 2 3 4 5 6 7 8# kernel/usys.S ... .global write write: li a7, SYS_write ecall ret ...ecall指令执行内容:ecall将状态从用户态设置为内核态- 将
pc值保存在sepc寄存器中, - 设置好
stvec,即trapline page的起始位置uservec函数,跳转stvec寄存器指向的指令
ecall指令并不会切换页表,为了能在用户页表下可以执行uservec,用户页表必须包含uservec的映射。xv6利用trampoline page实现,trampoline page在用户空间和内核空间都映射到了相同的虚拟地址。trampoline page, 即蹦床页面很形象,即通过该页面从用户空间跳到了内核空间。 -
执行
uservec函数uservec执行的第一步是交换a0和SSCARTCH的值。SSCARTCH寄存器保存trapframe page的地址,这样我们就得到了trapframe page的地址,可以将32个寄存器保存在trapframe page中了。xv6就每一个进程的
trapframe分配一个页面,并安排它映射在用户虚拟地址的固定位置,位于trampoline page的下一个页面。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20# kernel/trampoline.S # swap a0 and sscratch # so that a0 is TRAPFRAME csrrw a0, sscratch, a0 # save the user registers in TRAPFRAME sd ra, 40(a0) sd sp, 48(a0) sd gp, 56(a0) sd tp, 64(a0) sd t0, 72(a0) sd t1, 80(a0) ... sd t6, 280(a0) # save the user a0 in p->trapframe->a0 csrr t0, sscratch sd t0, 112(a0)接下来加载内核栈到
sp,确保内核程序正常运行;保存CPU的id到tp,用来获取当前进程;向t0写入usertrap的指针;向t1写入内核页表的地址并与SATP寄存器进行交换,完成页表的切换,跳转到usertrap执行1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20# kernel/trampoline.S # restore 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(), p->trapframe->kernel_trap ld t0, 16(a0) # restore kernel page table from p->trapframe->kernel_satp ld t1, 0(a0) csrw satp, t1 sfence.vma zero, zero # a0 is no longer valid, since the kernel page # table does not specially map p->tf. # jump to usertrap(), which does not return jr t0 -
执行
usertrap函数首先设置寄存器,更改
STVEC寄存器,指向内核空间trap处理代码的位置;将SEPC保存到trampframe中,防止程序执行过程中切换到另一个程序,另一个程序再调用系统调用导致SEPC数据被覆盖。1 2 3 4 5 6 7// kernel/trap.c w_stvec((uint64)kernelvec); struct proc *p = myproc(); // save user program counter. p->trapframe->epc = r_sepc();针对
trap的来源进行分析,如果陷阱来自系统调用syscall会处理它,根据系统调用号查找相应的系统调用函数,执行真正的系统调用,将返回值保存在tramframe的a0寄存器中;如果是设备中断,devintr会处理;否则它是一个异常,内核会杀死错误进程。最后调用usertrapret函数。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// kernel/trap.c 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 } 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); -
执行
usertrapret,内核需要为返回内核空间做准备关闭中断,更新
STVEC寄存器,指向uservec;设置kernel page table的指针,内核栈指针,usertrap函数的指针到trapframe中;设置SSTATUS寄存器,便于下一次从用户空间到内核空间的跳转,并恢复程序计数器pc的值。1 2 3 4 5 6 7// kernel/trap.c 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() ... w_sepc(p->trapframe->epc);最后通过调用
userret函数,将trapframe page的地址和用户页表的地址作为参数存储在a0,a1寄存器中,返回到用户空间的时候才能完成page table的切换。1 2uint64 fn = TRAMPOLINE + (userret - trampoline); ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp); -
最后执行
userret首先切换页表
1 2 3 4 5 6 7csrw satp, a1 sfence.vma zero, zero # put the saved user a0 in sscratch, so we # can swap it with our a0 (TRAPFRAME) in the last step. ld t0, 112(a0) csrw sscratch, t0接着恢复用户寄存器,最后调用
sret,sret会打开中断,更改内核态为用户态,跳转pc指向的指令执行用户程序。
实验内容
RISC-V assembly (easy)
-
RISC-V使用a0~a7共8个寄存器存储函数参数,若函数参数超过8个则需要存储在内存中,通过代码可以看出,13保存在a2寄存器中1 2# user/call.asm 24: 4635 li a2,13 -
通过代码看出,函数并没有对f调用
1 2# user/call.asm 26: 45b1 li a1,12 -
printf的函数地址是0x640auipc指令将高20位立即数左移12位加上pc后,存入ra寄存器,0x00000097对应的高20位左移12位之后为0x0,pc寄存器为0x30,因此ra寄存器的值为0x30,加上偏移量即为0x30 + 0x600(1536) = 0x6301 2 3# user/call.asm 30: 00000097 auipc ra,0x0 34: 600080e7 jalr 1536(ra) # 640 <printf> -
printf的jalr之后的寄存器ra值是0x38jalr指令当前PC+4保存在rd中,因此jalr之后寄存器ra值为0x34 + 0x4 = 0x38 -
程序输出
HE110 World,大端需改成0x726c640057616不需要改大端将高位存放在低地址,小段将高位存放在高地址
大端存储
0x00646c7200-64-6c-72小段存储
0x00646c7272-6c-64-00 -
没有参数传
a2寄存器,y显示的值是原来a2寄存器的值。
Backtrace(moderate)
backtrace实验需要打印函数执行过程中每个stack frame的地址。stack frame的格式已经介绍。
-
首先根据提示添加函数添加到
kernel/riscv.h中,获取保存在s0寄存器的帧指针fp。1 2 3 4 5 6 7static inline uint64 r_fp() { uint64 x; asm volatile("mv %0, s0" : "=r" (x) ); return x; } -
实现
backtrace函数,首先获取帧指针fp,并获取页栈页面的顶部地址,接着就打印fp的地址,并通过frame中指向前一个frame的指针获取前一个frame。直到到达页顶部,即已经打印了所有stack frame的地址。(需要在kernel/defs.h中声明)1 2 3 4 5 6 7 8 9 10 11// kernel/print.c void backtrace() { uint64 fp = r_fp(); uint64 stack_base = PGROUNDUP(fp); printf("backtrace:\n"); while(fp < stack_base) { printf("%p\n", *((uint64*)(fp - 8))); fp = *((uint64*)(fp - 16)); } }
Alarm(Hard)
alarm实验要求我们在进程使用CPU的时间内,xv6定期向进程发出警报。
-
首先添加系统调用,并在
user/usys.pl中添加1 2 3// user/user.h int sigalarm(int ticks, void (*handler)()); int sigreturn(void); -
首先在进程结构体中添加字段,并在
allocproc进行初始化和freeproc回收1 2 3 4 5 6 7 8// kernel/proc.h struct proc { ... int interval; //警报间隔 int tickcnt; //距离上一次调用经过了多少个时钟 uint64 handler; //警报处理函数 ... }1 2 3 4 5 6 7 8 9 10 11 12 13// kernel/proc.c static struct proc* allocproc(void) { ... p->interval = 0; p->tickcnt = 0; p->handler = 0; // An empty user page table. p->pagetable = proc_pagetable(p); ... }1 2 3 4 5 6 7 8 9// kernel/proc.c static void freeproc(struct proc *p) { ... p->interval = 0; p->tickcnt = 0; p->handler = 0; } -
实现真正的
sys_sigalarm函数和sys_sigreturn函数1 2 3// kernel/syscall.h #define SYS_sigalarm 22 #define SYS_sigreturn 231 2 3 4 5 6 7 8 9 10// kernel/syscall.c ... extern uint64 sys_sigalarm(void); extern uint64 sys_sigreturn(void); static uint64 (*syscalls[])(void) = { ... [SYS_sigalarm] sys_sigalarm, [SYS_sigreturn] sys_sigreturn, };1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16// kernel/sysproc.c uint64 sys_sigalarm(void) { if(argint(0, &myproc()->interval) < 0) return -1; if(argaddr(1, &myproc()->handler) < 0) return -1; return 0; } uint64 sys_sigreturn(void) { return 0; } -
修改
usertrap,进程警报间隔期满时,设置pc值为警报处理函数handler的地址,同时清除时钟计数器1 2 3 4 5 6 7 8 9 10// kernel/trap.c if(which_dev == 2) { p->tickcnt++; if(p->flag == 0 && p->tickcnt == p->interval) { p->tickcnt = 0; p->trapframe->epc = p->handler; } yield(); } -
我们在
test0测试中系统调用返回时返回的handler的地址,会导致无法回到用户程序之中,我们需要在test1/test2中处理正确返回到用户代码和防止重复调用的问题。我们需要在进程结构体中再加入两个字段,并在allocproc进行初始化和freeproc回收。1 2 3 4 5 6 7 8 9 10// kernel/proc.h struct proc { ... int interval; //警报间隔 int tickcnt; //距离上一次调用经过了多少个时钟 uint64 handler; //警报处理函数 int flag; struct trapframe *alarm_trapframe; ... } -
添加保存进程陷阱帧
p->trapframe到p->alarm_trapframe,并置flag为1,防止handler重复调用1 2 3 4 5 6 7 8 9 10 11 12// kernel/trap.c if(which_dev == 2) { p->tickcnt++; if(p->flag == 0 && p->tickcnt == p->interval) { memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe)); p->tickcnt = 0; p->trapframe->epc = p->handler; p->flag = 1; } yield(); } -
实现
sys_sigreturn函数,当hanler调用sigreturn()时恢复陷阱帧,同时将flag置零,保证下一个handler能够调用。1 2 3 4 5 6 7 8// kernel/sysproc.c uint64 sys_sigreturn(void) { memmove(myproc()->trapframe, myproc()->alarm_trapframe, sizeof(struct trapframe)); myproc()->flag = 0; return 0; }