目录

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机制

从用户空间到内核空间的切换被称为陷入traptrap是异常控制流的一种,trap分为三类 系统调用,异常和设备中断。

xv6的trap处理过程,以系统调用为例:

6.S081-lab4
  1. 用户请求系统调用,将系统调用号写入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, 即蹦床页面很形象,即通过该页面从用户空间跳到了内核空间。

  2. 执行uservec函数

    uservec执行的第一步是交换a0SSCARTCH的值。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的idtp,用来获取当前进程;向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						
    
  3. 执行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会处理它,根据系统调用号查找相应的系统调用函数,执行真正的系统调用,将返回值保存在tramframea0寄存器中;如果是设备中断,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);
    
  4. 执行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的地址和用户页表的地址作为参数存储在a0a1寄存器中,返回到用户空间的时候才能完成page table的切换。

    1
    2
    
    uint64 fn = TRAMPOLINE + (userret - trampoline);
    ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
    
  5. 最后执行userret

    首先切换页表

    1
    2
    3
    4
    5
    6
    7
    
    csrw 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
    

    接着恢复用户寄存器,最后调用sretsret会打开中断,更改内核态为用户态,跳转pc指向的指令执行用户程序。

 

实验内容

RISC-V assembly (easy)

  1. RISC-V使用a0~a78个寄存器存储函数参数,若函数参数超过8个则需要存储在内存中,通过代码可以看出,13保存在a2寄存器中

    1
    2
    
    # user/call.asm
    24:	4635                	li	a2,13
    
  2. 通过代码看出,函数并没有对f调用

    1
    2
    
    # user/call.asm
    26:	45b1                	li	a1,12
    
  3. printf的函数地址是0x640

    auipc指令将高20位立即数左移12位加上pc后,存入ra寄存器,0x00000097对应的高20位左移12位之后为0x0pc寄存器为0x30,因此ra寄存器的值为0x30,加上偏移量即为0x30 + 0x600(1536) = 0x630

    1
    2
    3
    
    # user/call.asm
    30: 00000097       auipc ra,0x0
    34: 600080e7       jalr  1536(ra) # 640 <printf>
    
  4. printfjalr之后的寄存器ra值是0x38

    jalr指令当前PC+4保存在rd中,因此jalr之后寄存器ra值为0x34 + 0x4 = 0x38

  5. 程序输出HE110 World,大端需改成0x726c6400 57616不需要改

    大端将高位存放在低地址,小段将高位存放在高地址

    大端存储0x00646c72 00-64-6c-72

    小段存储0x00646c72 72-6c-64-00

  6. 没有参数传a2寄存器,y显示的值是原来a2寄存器的值。

Backtrace(moderate)

backtrace实验需要打印函数执行过程中每个stack frame的地址。stack frame的格式已经介绍。

  1. 首先根据提示添加函数添加到kernel/riscv.h中,获取保存在s0寄存器的帧指针fp

    1
    2
    3
    4
    5
    6
    7
    
    static inline uint64
    r_fp()
    {
      uint64 x;
      asm volatile("mv %0, s0" : "=r" (x) );
      return x;
    }
    
  2. 实现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定期向进程发出警报。

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

    1
    2
    3
    
    // user/user.h
    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
    
  2. 首先在进程结构体中添加字段,并在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;
    }
    
  3. 实现真正的sys_sigalarm函数和sys_sigreturn函数

    1
    2
    3
    
    // kernel/syscall.h
    #define SYS_sigalarm 22
    #define SYS_sigreturn 23
    
     1
     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;
    }
    
  4. 修改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();
    }
    
  5. 我们在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;
        ...
    }
    
  6. 添加保存进程陷阱帧p->trapframep->alarm_trapframe,并置flag1,防止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();
    }
    
  7. 实现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;
    }