阅读材料
- Xv6代码:trampoline.S、trap.c
- 教材第4章
trapframe页
对于每个用户进程来说,都有一个trapframe页。当用户异常发生时,trapframe页的作用有两个:
- 保存用户进程32个通用寄存器的值
- 恢复内核寄存器的值
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;/* 48 */ uint64 sp;/* 56 */ uint64 gp;/* 64 */ uint64 tp;/* 72 */ uint64 t0;/* 80 */ uint64 t1;/* 88 */ uint64 t2;/* 96 */ uint64 s0;/* 104 */ uint64 s1;/* 112 */ uint64 a0;/* 120 */ uint64 a1;/* 128 */ uint64 a2;/* 136 */ uint64 a3;/* 144 */ uint64 a4;/* 152 */ uint64 a5;/* 160 */ uint64 a6;/* 168 */ uint64 a7;/* 176 */ uint64 s2;/* 184 */ uint64 s3;/* 192 */ uint64 s4;/* 200 */ uint64 s5;/* 208 */ uint64 s6;/* 216 */ uint64 s7;/* 224 */ uint64 s8;/* 232 */ uint64 s9;/* 240 */ uint64 s10;/* 248 */ uint64 s11;/* 256 */ uint64 t3;/* 264 */ uint64 t4;/* 272 */ uint64 t5;/* 280 */ uint64 t6;
};
uservec汇编函数
当U模式下发生异常(中断/异常/系统调用),内核将跳转到uservec汇编函数,注意此时虽然处于S模式下,但地址空间仍是用户地址空间,uservec汇编函数的责任之一就是手动切换到内核地址空上去
载入TRAPFRAME地址
首先保存a0寄存器到sscratch寄存器当中,接着将TRAPFRAME的虚拟地址载入a0寄存器。
sscratch寄存器是S模式CSR寄存器,用于内核程序临时保存数据
csrw sscratch, a0
li a0, TRAPFRAME
TRAOFRAME页通过宏定义在memlayout.h文件当中,对于每个用户进程都一样,是Sv39最大虚拟地址下的第二个页(第一个页是TRAMPOLINE)
#define TRAMPOLINE (MAXVA - PGSIZE)
#define TRAPFRAME (TRAMPOLINE - PGSIZE)
保存用户通用寄存器
注意:这里只保存了30个用户进程的通用寄存器,除了x0寄存器(值衡为0,不用保存)和a0寄存器(稍后保存)
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 t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
sd a3, 136(a0)
sd a4, 144(a0)
sd a5, 152(a0)
sd a6, 160(a0)
sd a7, 168(a0)
sd s2, 176(a0)
sd s3, 184(a0)
sd s4, 192(a0)
sd s5, 200(a0)
sd s6, 208(a0)
sd s7, 216(a0)
sd s8, 224(a0)
sd s9, 232(a0)
sd s10, 240(a0)
sd s11, 248(a0)
sd t3, 256(a0)
sd t4, 264(a0)
sd t5, 272(a0)
sd t6, 280(a0)
保存a0寄存器
用户进程a0寄存器的原始值保存在了sscratch寄存器当中,此时的a0寄存器的值其实是TRAPFRAME,因此我们将sscratch寄存器的值保存到栈上
csrr t0, sscratch
sd t0, 112(a0)
恢复内核寄存器
现在,用户进程32个通用寄存器的值都已经保存到用户栈上了,我们可以随意使用这32个寄存器了。首先恢复内核寄存器:
- 内核栈指针(sp寄存器)
- 内核HART ID(tp寄存器)
-
usertrap()函数地址
ld sp, 8(a0)
ld tp, 32(a0)
ld t0, 16(a0)
切换到内核地址空间
接着切换地址空间到内核的地址空间。注意切换前后都要使用sfence.vma指令冲刷TLB
ld t1, 0(a0)
sfence.vma zero, zerocsrw satp, t1
sfence.vma zero, zero
跳转到内核中用户异常处理程序处理异常
jr t0
用户异常处理程序
该函数负责处理用户异常
- 判断上一个处理器模式是否为U模式,如果不是的话报错
- 保存kernelvec函数地址到stvec寄存器
- 保存发生异常的用户PC值
- 根据scause寄存器的值进行判断
- 如果是系统调用,则调用syscall()函数进一步处理
- 如果是外部中断,则调用devintr()函数进一步处理
- 如果是异常,则报错并kill该进程
- 调用usertrapret()函数,执行返回程序
//
// 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 callif (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 0x%lx pid=%d\n", r_scause(), p->pid);printf(" sepc=0x%lx stval=0x%lx\n", r_sepc(), r_stval());setkilled(p);}if (killed(p))exit(-1);// give up the CPU if this is a timer interrupt.if (which_dev == 2)yield();usertrapret();
}
从内核异常处理程序返回
- 将uservec汇编函数地址写入stvec寄存器中
- 保存内核信息:内核根页表地址、内核栈指针、usertrap函数地址、内核HART ID
- 设置返回的处理器模式为U模式,并且返回后开启外部中断
- 设置返回后的PC值
- 保存用户根页表基地址到a0寄存器中
- 调用userret汇编函数
void usertrapret(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.Suint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);w_stvec(trampoline_uservec);// set up trapframe values that uservec will need when// the process next traps into the kernel.p->trapframe->kernel_satp = r_satp(); // kernel page tablep->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stackp->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 modex |= SSTATUS_SPIE; // enable interrupts in user modew_sstatus(x);// set S Exception Program Counter to the saved user pc.w_sepc(p->trapframe->epc);// 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);
}
userret汇编函数
userret汇编函数与uservec汇编函数做的工作相反:
- 切换到用户地址空间
- 恢复用户32个通用寄存器
- 执行sret指令返回
.globl userret
userret:# userret(pagetable)# called by usertrapret() in trap.c to# switch from kernel to user.# a0: user page table, for satp.# switch to the user page table.sfence.vma zero, zerocsrw satp, a0sfence.vma zero, zeroli a0, TRAPFRAME# restore all but a0 from TRAPFRAMEld ra, 40(a0)ld sp, 48(a0)ld gp, 56(a0)ld tp, 64(a0)ld t0, 72(a0)ld t1, 80(a0)ld t2, 88(a0)ld s0, 96(a0)ld s1, 104(a0)ld a1, 120(a0)ld a2, 128(a0)ld a3, 136(a0)ld a4, 144(a0)ld a5, 152(a0)ld a6, 160(a0)ld a7, 168(a0)ld s2, 176(a0)ld s3, 184(a0)ld s4, 192(a0)ld s5, 200(a0)ld s6, 208(a0)ld s7, 216(a0)ld s8, 224(a0)ld s9, 232(a0)ld s10, 240(a0)ld s11, 248(a0)ld t3, 256(a0)ld t4, 264(a0)ld t5, 272(a0)ld t6, 280(a0)# restore user a0ld a0, 112(a0)# return to user mode and user pc.# usertrapret() set up sstatus and sepc.sret
参考资料
The xv6 Kernel-14 Trap Handling_哔哩哔哩_bilibili