6.s081 xv6-Trap实现

xv6-源码解析-Trap

为了实现安全和隔离,操作系统分为了用户态和内核态

trap 主要描述了发生中断时,用户态到内核态的切换,以及执行对应的中断处理程序之后,返回到用户态之间的一系列操作。

几个关键的寄存器

PC 指向当前指令的地址(用户态和内核态不同)

SATP 指向pageTable(内核或用户进程)

STVEC 指向内核处理指令的起始地址,trampoline的uservec函数的起始位置

SEPC 进入内核后保存用户态的PC值

SSRATCH 用来和另外一个寄存器值做交换

Trap过程

image-20240427143035383

ecall

ecall 是一条指令,做以下事情,是进入内核的入口

  1. user模式切换到supervisor模式(修改寄存器)

  2. 把当前PC保存在SEPC,PC切换成trampoline的代码开始地址(保存在STEVC中的),从内核回来时,使用SEPC恢复用户态的代码位置
  3. 跳转进PC(trampoline)的代码

trampoline

目的是为了用户态切换内核态,内核切换用户态之间,保存两者其一的上下文信息(pc,寄存器,页表等)

每一个用户进程都有(page table里有映射)与内核 page table的trampoline的映射完全一样,用户与内核页表切换是在trampoline完成的,所以切换前后不会发生异常。PTE_U=0,用户代码不能写,保证了trap过程的安全

uservec

trampoline需要调用uservec函数做以下事情

  1. 获取trapframe,trapframe地址保存在SSRATCH 中的,会和a0做一个交换

  2. 使用a0的偏移,一个一个保存用户态的31个寄存器的值到trapframe页上(每个进程都有一个trapframe),保存用户经常的a0变量

  3. 从trapframe恢复内核栈到sp,恢复usertrap函数的指针到t0,恢复内核页表到t1

  4. 跳转到t0(也就是执行内核的usertrap函数)

uservec:    


        # 1.获取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
        
        # 2. save the user registers in TRAPFRAME
        sd ra, 40(a0)
        sd sp, 48(a0)
       	# .... 省略一些保存的寄存
        sd t6, 280(a0)
		
		#3. 恢复内核寄存器
        # initialize kernel stack pointer, from p->trapframe->kernel_sp
        ld sp, 8(a0)
        # 内核cpuid, from p->trapframe->kernel_hartid
        ld tp, 32(a0)
        # 内核 usertrap() 地址, from p->trapframe->kernel_trap
        ld t0, 16(a0)
        # 恢复内核页表, from p->trapframe->kernel_satp.
        ld t1, 0(a0)
        csrw satp, t1


        # 4.跳转 usertrap()
        jr t0

usertrap

根据cause执行不同到trap处理,然后调用usertrapret()返回

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

  struct proc *p = myproc();
  
  // 保存第32个寄存器到trapframe
  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);

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

    //返回
  usertrapret();
}

usertrapret

  1. 重新设置stvec到 tramoline的uservec指令处
  2. 保存内核的sp栈指针,usertrap函数地址,satp页表寄存器到trampoline
  3. 为了返回用户空间,设置sstatus寄存器,控制sret行为
  4. 从trampoline的sepc 恢复用户的 pc,恢复satp为用户页表
  5. 跳转到userret
void
usertrapret(void)
{
  struct proc *p = myproc();

  //关中断
  //防止此时又重新进入了中断处理程序,导致内核出错
  intr_off();

  // 1. stvec 保存 trampoline的uservec函数地址
  uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
  w_stvec(trampoline_uservec);

  //2. 保存内核寄存器到trapframe,用来下一次用户到内核的切换,恢复内核
  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.
  
  //3. SPP 控制 sret的行为,保证正常回到用户态
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
  x |= SSTATUS_SPIE; // enable interrupts in user mode
  w_sstatus(x);
	
  //4. sepc 设置为用户态之前的pc值
  w_sepc(p->trapframe->epc);

  // 恢复用户页表指针
  uint64 satp = MAKE_SATP(p->pagetable);
	
    //回到trampoline的userret
  // 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.
    //5.计算出userret函数地址的,再调用,相当于userret(satp);
  uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
  ((void (*)(uint64))trampoline_userret)(satp);
}

userret

与trampoline的uservec函数做的事情相反

  1. 恢复用户态页表设置satp
  2. 恢复用户态寄存器
  3. 跳转回用户态之前的pc所指的程序
userret:
        # userret(pagetable)
        # called by usertrapret() in trap.c to
        # switch from kernel to user.
        # a0: user page table, for satp.

        #1. switch to the user page table.
        csrw satp, a0

        li a0, TRAPFRAME
        
        # 2. restore all but a0 from TRAPFRAME
        ld ra, 40(a0)
        ld sp, 48(a0)
        ld gp, 56(a0)
  		#......从trapframe恢复用户态的寄存器
        ld t5, 272(a0)
        ld t6, 280(a0)

		# restore user a0
        ld a0, 112(a0)
        
        #3. return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret