MIT 6.S081入门lab4 陷阱

MIT 6.S081入门lab4 trap

本lab主要共分为两个部分,一个是对RISC-V调用约定的介绍(LEC5),一个是针对系统调用和系统陷阱的介绍和实验,因此分为两部分完成

一、参考资料阅读与总结

1.risc-v手册阅读(Calling Convention)

介绍了RV32和RV64的C编译器标准和调用约定,包括基本ISA和标准通用扩展
RISV-V C数据类型总结:

C type Description Bytes in RV32 Bytes
char Character value/byte 1 1
short Short integer 2 2
int Integer 4 4
long Long integer 4 8
long long Long long integer 8 8
void* Pointer 4 8
float Single-precision float 4 4
double Double-precision float 8 8
long double Extended-precision float 16 16

RISC-V调用参数寄存器:a0-a7整数寄存器、fa0-fa7浮点寄存器
注:联合体/数组的浮点数指针使用整数寄存器传递、变量函数的浮点参数在整数寄存器中传递
参数传递:
RISC-V采用小端存储,因此小于参数位的在低地址传递
两倍参数传递方法:偶数寄存器低位,奇数寄存器高位;
超过两倍使用引用传递;
结构体使用堆传递,sp指向其第一个参数;
函数返回值:
传递a0 a1/fa0 fa1;浮点返回条件:浮点至为基元/结构体只存在1-2个浮点数;大返回值在内存中传递,使用地址进行传递
堆栈的生长: 向下生长+16字节对齐;
临时寄存器:调用过程易失性存储:t0-t6整数寄存器;ft0-ft11浮点数寄存器;调用期间易失性存储:s0-s11整数寄存器;fs0-fs11浮点数寄存器

Register ABI Name Description Saver
x0 zero Hard-wired zero
x1 ra Return address Caller
x2 sp Stack pointer Callee
x3 gp Global pointer
x4 tp Thread pointer
x5–7 t0–2 Temporaries Caller
x8 s0/fp Saved register/frame pointer Callee
x9 s1 Saved register Callee
x10–11 a0–1 Function arguments/return values Caller
x12–17 a2–7 Function arguments Caller
x18–27 s2–11 Saved registers Callee
x28–31 t3–6 Temporaries Caller
f0–7 ft0–7 FP temporaries Caller
f8–9 fs0–1 FP saved registers Callee
f10–11 fa0–1 FP arguments/return values Caller
f12–17 fa2–7 FP arguments Caller
f18–27 fs2–11 FP saved registers Callee
f28–31 ft8–11 FP temporaries Caller

软浮点调用:避免使用F、D、Q标准扩展指令、从而避免使用f寄存器
软浮点方法:第一个参数a0、第二个参数a2-a3、第三个参数a4引用;返回值使用a0和a1;
例程: fenv.h

2.xv6 book书籍阅读(chapter 4 trap and system calls 4.1 - 4.5)

总述

  • CPU暂停当前普通指令的执行,进行对特殊事件的跳转的三种情况(trap):
    系统调用: ecall指令
    异常: 用户/内核指令的非法操作;
    设备中断: 外部设备的中断请求;
  • trap原则: 对用户进程透明,即保存和恢复相关寄存器和状态
  • xv6
    处理逻辑:内核处理所有trap,系统调用;设备中断(基于隔离机制);异常(杀死用户进程)。
    处理阶段:CPU硬件操作 -> 汇编程序异常向量处理表 ->c陷阱处理程序 -> 服务程序
    服务程序分类:用户空间陷阱、内核空间陷阱、定时器中断;
    用户空间trap为例:uservec(保存)->usertrap(处理;trap handler)->usertrapret(内核恢复)->userret(用户恢复)

1.RISC-V陷入机制

  • 内核通过对CPU的控制寄存器写入指令告诉CPU如何处理陷阱(kernel/riscv.h:1);
  • 重要寄存器概述:
    stvec:内核写入陷阱处理程序的地址(在用户空间下的trap,是trampoline的uservec;在内核空间下的trap,是kernel/kernelvec.S中的kernelvec),RISC-V跳入此处处理陷阱。
    sepc:RISC-V在此处保存陷阱发生时候的PC
    scause:RISC-V放置表述陷阱原因的数字
    sscratch:通常用来装载指向进程trapframe的指针,便于寄存器保存和恢复;
    sstatus:SIE位控制设备中断是否开启;SPP位指示trap来源空间(用户/内核)
    注: 多核CPU每个相应的CPU都有在自己的相应寄存器,且这些寄存器只能在监管者模式下访问,机器模式也有对应的寄存器(xv6计时器中断使用);
  • 硬件处理流程(RISC-V):
    1. 如果造成trap的是设备中断,将sstatus中的SIE位清0,然后跳过以下步骤。
    2. (如果trap的原因不是设备中断)将sstatus中的SIE位清0,关闭设备中断。
    3. 将pc的值复制到sepc中。
    4. 将发生trap的当前模式(用户模式或监管者模式)写入sstatus中的SPP位。
    5. 设置scause的内容,反映trap的起因。
    6. 设置模式为监管者模式。
    7. 将stvec的值复制到pc中。
    8. 从新的pc值开始执行。
  • 内核操作: 切换内核页表、切换内核栈、保存寄存器 ->内核的灵活性。
  • 切换PC->保证用户/内核的隔离基础

2.从用户空间陷入

  • 用户陷入的棘手处:跳转时satp指向的是用户页表,因此需要通过陷阱向量跳转至内核页表,因此uservec必须在内核页表中和用户页表使用相同的地址
  • 用户空间陷入过程:uservec (kernel/trampoline.S:16) -> usertrap (kernel/trap.c:37) -> usertrapret (kernel/trap.c:90) -> userret (kernel/trampoline.S:16)
  • 实现方式: 蹦床界面(trampoline page),当执行用户代码时,stvec设置为uservec (kernel/trampoline.S:16)。
  • uservec过程(kernel/trampoline.S:16),保存当前状态,准备处理:
    1.使用csrrw 命令sscratch交换a0和sscratch,保存a0寄存器并调用sscratch中内容(trapframe)(kernel/proc.h:44);
    2.保护用户寄存器:使用p->trapframe保存用户寄存器
    3.跳转至内核处理trap:切换内核帧、包括当前CPU的hartid、usertrap的地址和内核页表的地址等,将satp切换到内核页表,并跳转至usertrap;
  • usertrap过程(kernel/trap.c:37)->确定陷阱原因,处理并返回:
    1.改变stvec为kernelvec,代表处理内核空间中的trap;
    2.保存sepc(用户空间pc)避免usertrap中的上下文切换导致sepc被覆写;
    3.根据不同的类型处理:系统调用syscall、设备中断devintr、其他异常杀死用户进程;
    注:系统调用时需要将保存起来的PC + 4,因为保存的是ecall;
    4.检查相应状态,进行操作(杀死进程/时间片到其调用yield让出CPU/跳转入usertrapret);
  • usertrapret过程 (kernel/trap.c:90) ->准备内核到用户态的切换:
    1.关闭中断,将stvec修改为指向uservec;
    2.保存内核相关参数:包括当前CPU的hartid、usertrap的地址和内核页表的地址等;
    3.清空SPP位同时允许中断;
    4.设置sepc为之前的用户PC;
    5.切换好用户页表,在蹦床页面上调用userret,参数为trapframe的虚拟地址和用户页表satp;
  • userret过程:(kernel/trampoline.S:88)->保存相应数据,跳回用户空间:
    1.切换为用户页表并刷新TLB;
    2.复制trapframe保存的用户a0到sscratch;
    3.恢复寄存器;
    4.使用csrrw交换a0和sscratch -> a0为用户的a0,sscratch为陷阱帧;
    5.调用sret完成陷阱,跳回用户空间;
  • sret工作: 将pc的值保存到sepc中,从用户模式切换到监管者模式,跳转到stvec中指向的指令。

3.代码:调用系统调用(exec)

  • exec 参数放入a0和a1,系统调用号放入a7,ecall触发用户空间下trap

    usys.S(通过user.pl生成)
    .global exec
    exec:
     li a7, SYS_exec
     ecall
     ret
    
  • 执行uservec、usertrap,usertrap发现起因是系统调用,因此执行syscall(kernel/syscall.c:133)
    syscall代码可见lab2:https://www.cnblogs.com/David-Dong/p/17987356

  • 找到syscall读取trapframe中保存的a7检索相对应的系统调用号,根据syscalls数组,找到对应的系统调用(sys_exec)
    sys_exec代码见lab3:https://www.cnblogs.com/David-Dong/p/17996820
    系统调用syscall将返回值放入trapframe中a0,userret将返回值取出作为返回用户空间的a0;如系统调用号无效,syscall打印错误并返回-1。

4.代码:系统调用参数

  • 内核使用trapfram读取trap保存的用户寄存器参数,其使用的函数为artint、artaddr和artfd
    其检索第n个系统调用参数,并以整数、指针或文件描述符的形式保存
    调用argraw检索相应的保存的用户寄存器

    kernel/syscall.c:35 argraw
    // argraw
    static uint64
    argraw(int n)
    {
      struct proc *p = myproc();
      switch (n) {
      case 0:
    	return p->trapframe->a0;
      case 1:
    	return p->trapframe->a1;
      case 2:
    	return p->trapframe->a2;
      case 3:
    	return p->trapframe->a3;
      case 4:
    	return p->trapframe->a4;
      case 5:
    	return p->trapframe->a5;
      }
      panic("argraw");
      return -1;
    }
    
    
  • 当传递指针作为参数时,内核需要读取用户内存:
    带来的挑战:
    1.用户进程可能是有漏洞或者有恶意的:无效指针、其他用户内核进程/内核指针;
    2.内核页表和用户页表的映射不同
    内核实现了用户与内核地址之间的双向传输(fetchaddr和fetchstr),其调用copyin和copyinstr实现

    kernel/syscall.c:10 fetchaddr和fetchstr)
    int
    fetchaddr(uint64 addr, uint64 *ip)
    {
      struct proc *p = myproc();
      if(addr >= p->sz || addr+sizeof(uint64) > p->sz) //判定大小正确
    	return -1;
      if(copyin(p->pagetable, (char *)ip, addr, sizeof(*ip)) != 0) //调用copyin实现
    	return -1;
      return 0;
    }
    
    // Fetch the nul-terminated string at addr from the current process.
    // Returns length of string, not including nul, or -1 for error.
    int
    fetchstr(uint64 addr, char *buf, int max)
    {
      struct proc *p = myproc();
      int err = copyinstr(p->pagetable, buf, addr, max); //调用copyinstr实现
      if(err < 0)
    	return err;
      return strlen(buf);
    }
    
  • copyinstr和copyin的作用:从用户虚拟地址srcva拷贝最多max字节到内核的dst位置中
    使用walkaddr(调用walk)找到srcva的物理地址,由于内核为的物理-虚拟地址为直接映射,因此直接拷贝数据从pa0到dst
    walkaddr用来检查用户输入的虚拟地址的权限是否正确(用户权限检查)

    kernel/vm.c:376 copyin和copyinstr
    // Copy from user to kernel.
    // Copy len bytes to dst from virtual address srcva in a given page table.
    // Return 0 on success, -1 on error.
    int
    copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
    {
      uint64 n, va0, pa0;
    
      while(len > 0){
    	va0 = PGROUNDDOWN(srcva);
    	pa0 = walkaddr(pagetable, va0); //获取物理地址
    	if(pa0 == 0)
    	  return -1;
    	n = PGSIZE - (srcva - va0); //获取拷贝页数
    	if(n > len)
    	  n = len;
    	memmove(dst, (void *)(pa0 + (srcva - va0)), n); 拷贝
    
    	len -= n;
    	dst += n;
    	srcva = va0 + PGSIZE;
      }
      return 0;
    }
    
    // Copy a null-terminated string from user to kernel.
    // Copy bytes to dst from virtual address srcva in a given page table,
    // until a '\0', or max.
    // Return 0 on success, -1 on error.
    int
    copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
    {
      uint64 n, va0, pa0;
      int got_null = 0;
    
      while(got_null == 0 && max > 0){
    	va0 = PGROUNDDOWN(srcva);
    	pa0 = walkaddr(pagetable, va0); //获取物理地址
    	if(pa0 == 0)
    	  return -1;
    	n = PGSIZE - (srcva - va0); //获取拷贝页数
    	if(n > max)
    	  n = max;
    
    	char *p = (char *) (pa0 + (srcva - va0)); //拷贝
    	while(n > 0){
    	  if(*p == '\0'){
    		*dst = '\0';
    		got_null = 1;
    		break;
    	  } else {
    		*dst = *p;
    	  }
    	  --n;
    	  --max;
    	  p++;
    	  dst++;
    	}
    
    	srcva = va0 + PGSIZE;
      }
      if(got_null){
    	return 0;
      } else {
    	return -1;
      }
    }
    
    
    kernel/vm.c:91 walkaddr
    // Look up a virtual address, return the physical address,
    // or 0 if not mapped.
    // Can only be used to look up user pages.
    uint64
    walkaddr(pagetable_t pagetable, uint64 va)
    {
      pte_t *pte;
      uint64 pa;
    
      if(va >= MAXVA)
    	return 0;
    
      pte = walk(pagetable, va, 0);
      if(pte == 0) //判断页表存在
    	return 0;
      if((*pte & PTE_V) == 0) //判断页表是否存在于用户地址
    	return 0;
      if((*pte & PTE_U) == 0) //判断用户权限
    	return 0;
      pa = PTE2PA(*pte);
      return pa;
    }
    
    walk函数请移步lab3:https://www.cnblogs.com/David-Dong/p/17996820
  • 类似的将内核数据复制到用户提供的地址的函数copyout:,其需要目的地址为用户进程自己拥有的物理内存页。

    kernel/vm.c:351 copyout
    // Copy from kernel to user.
    // Copy len bytes from src to virtual address dstva in a given page table.
    // Return 0 on success, -1 on error.
    int
    copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
    {
      uint64 n, va0, pa0;
    
      while(len > 0){
    	va0 = PGROUNDDOWN(dstva);
    	pa0 = walkaddr(pagetable, va0);
    	if(pa0 == 0)
    	  return -1;
    	n = PGSIZE - (dstva - va0);
    	if(n > len)
    	  n = len;
    	memmove((void *)(pa0 + (dstva - va0)), src, n);
    
    	len -= n;
    	src += n;
    	dstva = va0 + PGSIZE;
      }
      return 0;
    }
    

5.从内核空间陷入

  • 对于两种模式下的trap,其寄存器配置略有不同,内核空间时,stvec被设置为指向kernelvec(kernel/kernelvec.S),处理内核空间下的trap。
  • kernelvec(kernel/kernelvec.S:10)->kerneltrap(kernel/trap.c:134)->kernelvec(kernel/kernelvec.S:10)
  • kernelvec(kernel/kernelvec.S:10)->直接在运行中的内核线程的内核栈中保存相关的寄存器,调用kerneltrap;
  • kerneltrap(kernel/trap.c:134)->处理设备中断和异常:
    1.保存sepc、sstatus;
    2.调用devintr处理设备中断;调用painc并停止执行处理异常;
    注:如果为计时器中断,放弃cpu并让其他线程运行,由于寄存器已经入栈,因此如果轮转到了会继续执行;
  • 3.恢复sepc和sstatus寄存器,返回kernelvec;
  • kernelvec(kernel/kernelvec.S:10)->弹出栈恢复寄存器,执行sret恢复pc为sepc;
    注意: 在CPU改变空间即修改stvec时候需要禁用中断,trap时候RISC-V自动关闭了中断,因此在完成后打开就好了,但是恢复时候需要手动关闭。
  • 特殊的计时器中断:在机器模式下处理
    在启动阶段使用timerinit(kernel/start.c:52)初始化计时器,CLINT负责产生计时器中断,并通过改写mtvec寄存器的值
    ,将机器模式下trap handler设置为timervec;
    timervec(kernel/kernelvec.S:)作用:计时器芯片重新编程,开始下一轮计时并发出软件中断(喂狗),最后使用mret跳转回到监管者模式

二、涉及函数

  • 用户空间陷入

    kernel/trampoline.S:16 uservec
    .globl uservec
    uservec:    
    	#
    		# trap.c sets stvec to point here, so
    		# traps from user space start here,
    		# in supervisor mode, but with a
    		# user page table.
    		#
    		# sscratch points to where the process's p->trapframe is
    		# mapped into user space, at TRAPFRAME.
    		#
    
    	# swap a0 and sscratch
    		# so that a0 is TRAPFRAME 交换 a0 和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 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)
    
    	# save the user a0 in p->trapframe->a0 保存a0
    		csrr t0, sscratch
    		sd t0, 112(a0)
    
    		# 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 跳转usertrap
    		jr t0
    
    
    kernel/trap.c:32 usertrap
    //
    // 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 call 系统调用
    
    	if(p->killed)
    	  exit(-1);
    
    	// sepc points to the ecall instruction,
    	// but we want to return to the next instruction. pc指针操作
    	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);
    
      // give up the CPU if this is a timer interrupt. 任务轮转
      if(which_dev == 2)
    	yield();
    
      usertrapret();
    }
    
    risv.h 260 intr_on intr_off
    // enable device interrupts
    static inline void
    intr_on()
    {
      w_sstatus(r_sstatus() | SSTATUS_SIE); //写入寄存器
    }
    
    // disable device interrupts
    static inline void
    intr_off()
    {
      w_sstatus(r_sstatus() & ~SSTATUS_SIE); //写入寄存器
    }
    
    kernel/trap.c:86 usertrapret
    //
    // return to user space
    //
    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 trampoline.S 修改stvec
      w_stvec(TRAMPOLINE + (uservec - trampoline));
    
      // set up trapframe values that uservec will need when
      // the process next re-enters 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 开启中断
      w_sstatus(x);
    
      // set S Exception Program Counter to the saved user pc. 调整pc
      w_sepc(p->trapframe->epc);
    
      // tell trampoline.S the user page table to switch to.切换页表
      uint64 satp = MAKE_SATP(p->pagetable);
    
      // jump to trampoline.S at the top of memory, which 
      // switches to the user page table, restores user registers,
      // and switches to user mode with sret.调用userret
      uint64 fn = TRAMPOLINE + (userret - trampoline);
      ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp); 
    }
    
    kernel/trampoline.S:87 userret
    .globl userret
    userret:
    		# userret(TRAPFRAME, pagetable)
    		# switch from kernel to user.
    		# usertrapret() calls here.
    		# a0: TRAPFRAME, in user page table.
    		# a1: user page table, for satp.
    
    		# switch to the user page table. 切换页表,清空TLB
    		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. 复制a0到sscratch
    		ld t0, 112(a0)
    		csrw sscratch, t0
    
    		# restore all but a0 from TRAPFRAME 恢复寄存器
    		ld 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 a0, and save TRAPFRAME in sscratch 恢复a0
    		csrrw a0, sscratch, a0
    
    		# return to user mode and user pc.
    		# usertrapret() set up sstatus and sepc. 跳转回用户空间
    		sret
    
  • 内核空间陷入:

    kernel/kernelvec.S:8 kernelvec
    .globl kernelvec
    .align 4
    kernelvec:
    		// make room to save registers.推栈
    		addi sp, sp, -256
    
    		// save the registers.寄存器入栈
    		sd ra, 0(sp)
    		sd sp, 8(sp)
    		sd gp, 16(sp)
    		sd tp, 24(sp)
    		sd t0, 32(sp)
    		sd t1, 40(sp)
    		sd t2, 48(sp)
    		sd s0, 56(sp)
    		sd s1, 64(sp)
    		sd a0, 72(sp)
    		sd a1, 80(sp)
    		sd a2, 88(sp)
    		sd a3, 96(sp)
    		sd a4, 104(sp)
    		sd a5, 112(sp)
    		sd a6, 120(sp)
    		sd a7, 128(sp)
    		sd s2, 136(sp)
    		sd s3, 144(sp)
    		sd s4, 152(sp)
    		sd s5, 160(sp)
    		sd s6, 168(sp)
    		sd s7, 176(sp)
    		sd s8, 184(sp)
    		sd s9, 192(sp)
    		sd s10, 200(sp)
    		sd s11, 208(sp)
    		sd t3, 216(sp)
    		sd t4, 224(sp)
    		sd t5, 232(sp)
    		sd t6, 240(sp)
    
    	// call the C trap handler in trap.c 调用处理函数
    		call kerneltrap
    
    		// restore registers. 恢复寄存器并弹栈
    		ld ra, 0(sp)
    		ld sp, 8(sp)
    		ld gp, 16(sp)
    		// not this, in case we moved CPUs: ld tp, 24(sp)
    		ld t0, 32(sp)
    		ld t1, 40(sp)
    		ld t2, 48(sp)
    		ld s0, 56(sp)
    		ld s1, 64(sp)
    		ld a0, 72(sp)
    		ld a1, 80(sp)
    		ld a2, 88(sp)
    		ld a3, 96(sp)
    		ld a4, 104(sp)
    		ld a5, 112(sp)
    		ld a6, 120(sp)
    		ld a7, 128(sp)
    		ld s2, 136(sp)
    		ld s3, 144(sp)
    		ld s4, 152(sp)
    		ld s5, 160(sp)
    		ld s6, 168(sp)
    		ld s7, 176(sp)
    		ld s8, 184(sp)
    		ld s9, 192(sp)
    		ld s10, 200(sp)
    		ld s11, 208(sp)
    		ld t3, 216(sp)
    		ld t4, 224(sp)
    		ld t5, 232(sp)
    		ld t6, 240(sp)
    
    		addi sp, sp, 256
    
    		// return to whatever we were doing in the kernel. 恢复原先状态
    		sret
    
    kernel/trap.c:134 kerneltrap
    // interrupts and exceptions from kernel code go here via kernelvec,
    // on whatever the current kernel stack is.
    void 
    kerneltrap()
    {
      int which_dev = 0;
      uint64 sepc = r_sepc();
      uint64 sstatus = r_sstatus(); //保存寄存器sepc、sstatus
      uint64 scause = r_scause();
    
      if((sstatus & SSTATUS_SPP) == 0) // 异常处理
    	panic("kerneltrap: not from supervisor mode");
      if(intr_get() != 0)
    	panic("kerneltrap: interrupts enabled");
    
      if((which_dev = devintr()) == 0){ /。设备中断处理
    	printf("scause %p\n", scause);
    	printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
    	panic("kerneltrap");
      }
    
      // give up the CPU if this is a timer interrupt.任务轮转处理
      if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING) 
    	yield();
    
      // the yield() may have caused some traps to occur,
      // so restore trap registers for use by kernelvec.S's sepc instruction.恢复寄存器
      w_sepc(sepc); 
      w_sstatus(sstatus);
    }
    
    kernel/trap.c:171 devintr
    // check if it's an external interrupt or software interrupt,
    // and handle it.
    // returns 2 if timer interrupt,
    // 1 if other device,
    // 0 if not recognized.
    int
    devintr()
    {
      uint64 scause = r_scause();
    
      if((scause & 0x8000000000000000L) &&
    	 (scause & 0xff) == 9){
    	// this is a supervisor external interrupt, via PLIC. 外部中断
    
    	// irq indicates which device interrupted.
    	int irq = plic_claim(); //读取终端设备编号并分类处理
    
    	if(irq == UART0_IRQ){
    	  uartintr();
    	} else if(irq == VIRTIO0_IRQ){
    	  virtio_disk_intr();
    	} else if(irq){
    	  printf("unexpected interrupt irq=%d\n", irq);
    	}
    
    	// the PLIC allows each device to raise at most one
    	// interrupt at a time; tell the PLIC the device is
    	// now allowed to interrupt again.允许中断
    	if(irq)
    	  plic_complete(irq);
    
    	return 1;
      } else if(scause == 0x8000000000000001L){
    	// software interrupt from a machine-mode timer interrupt, 定时器中断
    	// forwarded by timervec in kernelvec.S.
    
    	if(cpuid() == 0){
    	  clockintr(); //初始化时钟
    	}
    
    	// acknowledge the software interrupt by clearing
    	// the SSIP bit in sip.
    	w_sip(r_sip() & ~2);
    
    	return 2;
      } else {
    	return 0;
      }
    }
    
  • 计时器相关代码:

    kernel/start.c:52 timerinit
    // set up to receive timer interrupts in machine mode,
    // which arrive at timervec in kernelvec.S,
    // which turns them into software interrupts for
    // devintr() in trap.c.
    void
    timerinit()
    {
      // each CPU has a separate source of timer interrupts.
      int id = r_mhartid();
    
      // ask the CLINT for a timer interrupt.
      int interval = 1000000; // cycles; about 1/10th second in qemu.
      *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval; //设置定时器
    
      // prepare information in scratch[] for timervec.
      // scratch[0..3] : space for timervec to save registers.
      // scratch[4] : address of CLINT MTIMECMP register.
      // scratch[5] : desired interval (in cycles) between timer interrupts.
      uint64 *scratch = &mscratch0[32 * id];
      scratch[4] = CLINT_MTIMECMP(id);
      scratch[5] = interval;
      w_mscratch((uint64)scratch);
    
      // set the machine-mode trap handler.
      w_mtvec((uint64)timervec); //写入喂狗中断
    
      // enable machine-mode interrupts.
      w_mstatus(r_mstatus() | MSTATUS_MIE);
    
      // enable machine-mode timer interrupts. //enable
      w_mie(r_mie() | MIE_MTIE);
    }
    
    kernel/kernelvec.S:88 timervec
    		#
    		# machine-mode timer interrupt.
    		#
    .globl timervec
    .align 4
    timervec:
    		# start.c has set up the memory that mscratch points to:
    		# scratch[0,8,16] : register save area.
    		# scratch[32] : address of CLINT's MTIMECMP register.
    		# scratch[40] : desired interval between interrupts.
    
    		csrrw a0, mscratch, a0
    		sd a1, 0(a0)
    		sd a2, 8(a0)
    		sd a3, 16(a0)
    
    		# schedule the next timer interrupt
    		# by adding interval to mtimecmp.
    		ld a1, 32(a0) # CLINT_MTIMECMP(hart)
    		ld a2, 40(a0) # interval
    		ld a3, 0(a1)
    		add a3, a3, a2
    		sd a3, 0(a1)
    
    		# raise a supervisor software interrupt.
    	li a1, 2
    		csrw sip, a1
    
    		ld a3, 16(a0)
    		ld a2, 8(a0)
    		ld a1, 0(a0)
    		csrrw a0, mscratch, a0
    
    		mret
    
  • 其他位于trap.c的工具代码 trapinit、trapinithart、clockintr

    kernel/trap.c
    void
    trapinit(void)
    {
      initlock(&tickslock, "time");
    }
    
    // set up to take exceptions and traps while in the kernel.
    void
    trapinithart(void)
    {
      w_stvec((uint64)kernelvec);
    }
    
    //
    // handle an interrupt, exception, or system call from user space.
    // called from trampoline.S
    //
    
    ...
    
    void
    clockintr()
    {
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
    }
    
  • riscv.h中主要是汇编和c实现的寄存器修改代码和定义,这里就不详细列出了。

三、课程视频观看笔记

1.LEC5 Calling conventions and stack frames RISC-V

C-> ASM过程/processor
每个处理器都有相关的ISA,即相应的汇编代码(二进制)
C -> asm(.S) -> Binary(.o)

RISC-V vs x86-84
精简指令集RISC <-> 复杂指令集CISC
更少的指令 指令更简单 <-> 指令增加,指令复杂(向后兼容)
开源 <-> 闭源

RISC的指令集是分开的,被分为多个部分,从而实现向后兼容
arm也使用的是RISC
汇编调试(GDB)
窗口1:make qemu-gdb
窗口2:risc64-unkown-elf-gdb ->tui enable ->b 设置断点
窗口1运行程序
可以利用.asm左边的PC指针定位异常
layout asm 查看汇编;layout reg查看寄存器;layout split查看汇编和c语言; layout source 查看c语言;focus xxx专注于xxx窗口
查看当前断点:info breakpoints
查看寄存器 info reg
删除所有断点 delete
查看当前栈帧信息 i frame
查看更多信息: i xxx
查看 栈帧 bt
查看frame + num -> i frame (0为刚才地址)
查看地址中元素 p *地址@长度 例:p *argv@argc
观察点:watch +变量名
条件断点 b 断点函数 if 判断逻辑
内置手册:apropos
tmux:
打开多个窗口: ctrl-b c;切换ctrl-b p/n or o; 垂直拆分ctrl-b %; 水平拆分ctrl-b "
寄存器表
汇编是基于寄存器的,因此汇编模式为加载->操作->保存:
保存机制:caller在函数调用期间不会保留;callee会保留、即caller需要调用者考虑保存、callee需要被调用者保存。
堆栈->编译器实现
堆栈是向下生长的
简化布局(栈帧[调用生成]):返回地址(第一个)、前一个fp、寄存器、变量...
栈指针sp:堆栈底部;栈指针fp,当前栈帧顶。
汇编函数构成:函数序言(保存相应寄存器(栈))->函数主体(包括调用) ->函数尾声(处理调用和栈);如果没有序言和尾声处理栈的话,子函数会一直被调用,因为相应的ra变为了子函数的栈帧
结构体在内存中的布局: 连续内存存储

2.LEC6 Isolation & system call entry/exit

  • 本质是用户空间和内核空间的转换。核心要求是trap是对用户空间是透明的。
    • CPU(硬件)要求:保存用户空间运行的寄存器,并将其内容切换为内核运行的寄存器内容;
    • 安全隔离要求:不能信任用户空间传递来的内容
    • 内核要求:内核代码需要安全
  • 管理员模式相较于用户模式的特权:
    读写控制寄存器,如SATP、STVEC、SEPC、SCRATCH等;可以设置PTE及其状态码
  • 管理员模式没有的功能:任意读写物理地址->管理员模式下只能通过页表访问其地址,且管理员模式不能读取用户模式下的PTE
  • 用户系统调用过程(以write()为例):用户空间【write()-> ecall】->内核空间【uservec -> usertrap -> syscall -> sys_wirte -> syscall -> usertrapret -> userret】-> 用户空间【ecall之后恢复】;
  • 细节:
    1.ecall后页表并未切换,需要操作系统内核进行切换其跳转,地址是stvec寄存器所指向的trapframe地址。具体执行为:保存之前pc为sepc, 切换模式,跳转;
    2.由于RISC-V的目标是让软件发挥最大效用,因此ecall只做了最小工作;
    3.xv6使用页表中的trapframe中保存寄存器,在跳转之前 sctatch寄存器中保存的为trapframe的首地址,其是在内核引导的时候确定的;
    4.由于trapframe部分在内核和用户空间都使用的是相同的映射,因此,在使用satp切换的时候trap不会崩溃;
    5.usertrap在处理前需要切换stvec至内内核kernelvec;
    6.在内核处理用户端的stvec时,需要关闭中断避免再次中断报错;
    7.由于汇编是对寄存器更精细的操作,因此在涉及寄存器操作(在trapframe上运行的代码)都需要使用汇编实现。
    8.sret:返回用户模式,恢复pc。
  • 系统调用核心:状态切换
  • 调试技巧(QWMU):
    • 查看当前页表: ctrl-a c 进入QEMU监视器/控制台 -> info mem 打印当前页表信息;a:是否使用过;d:是更改过

四、完成lab及其代码

  • RISC-V assembly

    answers-traps.txt
    
    1.哪些寄存器保存函数的参数?例如,在main对printf的调用中,哪个寄存器保存13?
    a0 - a7 .a2
    2,main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)
    没有call / j,所以就没有调用,g被内联入f、f被内联入main
    3.printf函数位于哪个地址?
    0000000000000630 <printf>:, 在main中使用了相对跳转实现了函数的调用(jalr)
    4.在main中printf的jalr之后的寄存器ra中有什么值?
    下一条指令的值0x38,即pc+4
    5.运行以下代码。
    unsigned int i = 0x00646c72;
    printf("H%x Wo%s", 57616, &i);
    程序的输出是什么?
    H110wrld
    输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i设置成什么?是否需要将57616更改为其他值?
    0x726c6400
    这里有一个小端和大端存储的描述和一个更异想天开的描述。
    在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
    printf("x=%d y=%d", 3);
    a2寄存器中的内容
    
  • Backtrace

    kernel/defs.h
    // printf.c
    ...
    void            backtrace(void);
    
    kernel/riscv.h
    //read cur frame pointer
    static inline uint64
    r_fp()
    {
      uint64 x;
      asm volatile("mv %0, s0" : "=r" (x) );
      return x;
    }
    
    
    kernel/printf.c
    void
    panic(char *s)
    {
      pr.locking = 0;
    
      printf("panic: ");
      printf(s);
      printf("\n");
      backtrace(); //加入相应调用方便调试
      panicked = 1; // freeze uart output from other CPUs
      for(;;)
    	;
    }
    
    ...
    
    void
    backtrace() {
      uint64 currfp = r_fp();
      uint64 stackbottom = PGROUNDUP(currfp); //取栈底(由于栈是向下生长的)
      printf("backtrace:\n");
      while (currfp < stackbottom) {
    	uint64 ret = *(pte_t*)(currfp - 0x8); //相应的编程号
    	uint64 prerfp = *(pte_t*)(currfp - 0x10); //下一位栈frame
    	printf("%p\n", ret);
    	currfp = prerfp;
      }
    }
    
    kernel/sysproc.c
    uint64
    sys_sleep(void)
    {
      int n;
      uint ticks0;
    
      backtrace(); //插入函数调用
    
      if(argint(0, &n) < 0)
    	return -1;
      acquire(&tickslock);
      ticks0 = ticks;
      while(ticks - ticks0 < n){
    	if(myproc()->killed){
    	  release(&tickslock);
    	  return -1;
    	}
    	sleep(&ticks, &tickslock);
      }
      release(&tickslock);
      return 0;
    }
    
  • Alarm

    Makefile
    UPROGS=\
    	$U/_cat\
    ...
    	$U/_alarmtest\ 添加测试程序
    
    user/user.h
    // system calls
    ...
    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void); //添加系统调用
    
    user/usys.pl
    ...
    entry("sigalarm");
    entry("sigreturn"); //使其能够生成相应的汇编代码
    
    
    kernel/syscall.h
    ...
    #define SYS_sigalarm  22
    #define SYS_sigreturn  23 //添加相应的系统调用编号
    
    kernel/syscall.c
    ...
    extern uint64 sys_sigalarm(void);
    extern uint64 sys_sigreturn(void); //声明外部系统调用函数
    
    static uint64 (*syscalls[])(void) = {
    [SYS_fork]    sys_fork,
    ...
    [SYS_sigalarm]   sys_sigalarm,
    [SYS_sigreturn] sys_sigreturn, //加入系统调用函数数组
    
    kernel/proc.h
    ...
    enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
    
    //new struct to record trick-alarm information
    struct trickalarm {
      struct trapframe *alarmframe; //test1和test2所用的保存trapframe的结构体
      void (*alarm_handler)(); //处理函数
      int alarm_period; //警报周期
      int tricks_since_last_alarm;  //计数器
      int alarm_states; // 1 is in alarm is runing; 0 is alarm is not running test1和test2所使用的保证alarm唯一性的标志位
    };
    
    // Per-process state
    struct proc {
      struct spinlock lock;
      ...
      struct trickalarm trickalarm; //用于实现trickalarm任务的结构体 
    };
    
    kernel/proc.c
    ...
    static struct proc*
    allocproc(void)
    {
    ...
    found:
    ...
      // Allocate a trapframe page.
      if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    	release(&p->lock);
    	return 0;
      }
    
      // Allocate a alarmframe page.分配test1和test2需要的保存trapframe的结构体空间
      if((p->trickalarm.alarmframe = (struct trapframe *)kalloc()) == 0){
    	release(&p->lock);
    	return 0;
      }
      ...
       p->trickalarm.alarm_period = 0;
      p->trickalarm.alarm_handler = 0;
      p->trickalarm.tricks_since_last_alarm = 0;
      p->trickalarm.alarm_states = 0; //结构体成员初始化
    
    	return p;
    }
    
    // free a proc structure and the data hanging from it,
    // including user pages.
    // p->lock must be held.
    static void
    freeproc(struct proc *p)
    {
    ...
      if(p->trickalarm.alarmframe)
    	kfree((void*)p->trickalarm.alarmframe);
      p->trickalarm.alarmframe = 0;//释放申请的空间
      ...
       p->trickalarm.alarm_period = 0;
      p->trickalarm.alarm_handler = 0;
      p->trickalarm.tricks_since_last_alarm = 0;
      p->trickalarm.alarm_states = 0; //清除结构体数组
      p->state = UNUSED;
    }
    
    kernel/trap.c
    ...
    //
    void
    usertrap(void)
    {
      int which_dev = 0;
      ...
    	// give up the CPU if this is a timer interrupt.
      if(which_dev == 2) //定时器通过机器模式触发中断
      if (p->trickalarm.alarm_period != 0){ //判断定时器是否使能
    	p->trickalarm.tricks_since_last_alarm = (p->trickalarm.tricks_since_last_alarm + 1) %  p->trickalarm.alarm_period ; //通过取余操作避免溢出
    	if (p->trickalarm.alarm_states == 0 && p->trickalarm.tricks_since_last_alarm == 0) { //判断是否触发报警操作并满足test2要求防止多次调用
    	  p->trickalarm.alarm_states = 1; //使用位置1
    	  *p->trickalarm.alarmframe = *p->trapframe; //满足test1回调要求,将trapframe进行保存
    	  p->trapframe->epc = (uint64)p->trickalarm.alarm_handler; //准备跳转相应程序
    	}
      }
    	yield(); 切换进程
    
      usertrapret();
    }
    
    kernel/sysproc.c
    ...
    uint64
    sys_sigalarm(void) //报警函数
    {
      struct proc *p = myproc(); //读取当前进程
      int period;
      if(argint(0, &period) < 0) //从用户空间读取周期数
    	return -1;
      uint64 handler;
      if(argaddr(1, &handler) < 0) //从用户空间读取函数地址
    	return -1;
      p->trickalarm.alarm_period = period;
      p->trickalarm.alarm_handler = (void (*)())handler;
      p->trickalarm.tricks_since_last_alarm = 0; //初始化相应的结构体参数
      return 0;
    }
    
    uint64
    sys_sigreturn(void)
    {
      struct proc *p = myproc();
      if (p->trickalarm.alarm_states) {
    	p->trickalarm.alarm_states = 0; //结束调用
    	*p->trapframe = *p->trickalarm.alarmframe; //恢复原始进程的trapframe
      }
      return 0;
    }
    
    

参考文献

RISC-V手册呼叫约定章节:https://pdos.csail.mit.edu/6.S081/2020/readings/riscv-calling.pdf
2020版xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
xv6手册与代码笔记:https://zhuanlan.zhihu.com/p/351939252
xv6阅读笔记:https://ghostasky.github.io/2022/07/12/XV6/
xv6手册中文版:http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c4/s4.html
28天速通MIT 6.S081操作系统公开课:https://zhuanlan.zhihu.com/p/625962093
MIT6.s081操作系统笔记:https://juejin.cn/post/7008487319976017928
MIT 6.S081 Lab4: traps:https://zhuanlan.zhihu.com/p/440454679

posted @ 2024-02-29 19:19  David_Dong  阅读(68)  评论(0编辑  收藏  举报