找回密码
 立即注册
首页 业界区 安全 Lab4 trap

Lab4 trap

周冰心 8 小时前
目录

  • 预备知识

    • trap机制

      • RISC-V CPU硬件操作
      • 来自用户空间的trap
      • 来自内核空间的trap

    • tramponline.S

      • uservec函数
      • userret函数


  • backtrace

    • 获取当前的调用函数栈帧
    • 实现backtrace()

  • Alarm

    • 系统调用准备
    • 实现alarm


预备知识

trap机制

导致CPU将指令的正常执行转到处理某些类型事件的特殊代码,这些事件统称为traps。

  • 系统调用,执行ecall指令
  • 异常:程序执行了非法操作,比如除以0或者使用无效的虚拟地址
  • 设备中断
对于Xv6来说,异常将由内核来进行处理,通常的顺序是trap强制将控制转移到内核;内核保存寄存器和其它状态,使得可以恢复执行;内核执行适当的处理程序代码(例如,系统调用实现或设备驱动程序);内核恢复所保存的状态并从陷阱返回;并且原始代码在其停止的地方恢复。
Xv6的trap处理分为四个阶段:RISC-V CPU执行的硬件操作,为内核C代码准备的汇编向量,决定如何处理trap的C处理程序,以及系统调用或设备驱动程序服务例程。
Xv6为三种不同的trap准备了单独的汇编向量和trap处理程序:来自用户空间的trap,来自内核空间的trap和定时器中断。
RISC-V CPU硬件操作

由于trap处理程序是不同于之前执行的代码,因此需要切换硬件的状态。用户应用程序可以使用全部的32个寄存器,下面是一些重要的寄存器的作用:

  • 程序计数寄存器(PC):指向CPU将要执行的下一条代码的地址
  • MODE标志位:表明当前是supervisor mode 还是 user mode
  • SATP寄存器:低44位存放了当前使用的根页表所在的PPN
  • STVEC寄存器:保存内核中处理trap的指令地址
  • SEPC寄存器:在trap过程中保存PC的内容,用于恢复
  • SCAUSE寄存器:保存trap的原因
  • SSCRATCH寄存器:保存了指向进程trapframe的指针
  • SSTATUS寄存器:SIE位控制是否启用设备中断。如果SIE为0,RISC-V将延迟设备中断,直到内核设置SIE。SPP位指示trap是来自user模式(0)还是其他模式(1);而当执行sret从trap返回时,如果SPP为0,返回到U-mode,为1返回到S-mode
具体来说,RISC-V 硬件对除了定时器中断外的所有trap执行以下操作:

  • 如果造成trap的原因是硬件中断,并且SSTATUS中的SIE位清0,然后跳过以下步骤
  • 将SIE位清0,关闭设备中断
  • 将PC的值复制到SEPC中
  • 在SSTATUS的SPP位保存当前模式
  • 设置SCAUSE以保存trap的原因
  • 设置模式为supervisor
  • 将STVEC中的值复制到PC中
  • 从新的PC值开始执行
CPU不负责切换内核页表、不切换内核栈、不保存PC以为的任何寄存器,这些将由内核来进行处理。
来自用户空间的trap

这类trap的高级路径是:uservec(kernel/tramponline.S:16) -> usertrap(kernel/trap.c:37) -> usertapret(kernel/trap.c:90) -> userret(kernel/tramponline.S:16). 由于此时SATP中指向的是用户页表,而RISC-V硬件不负责切换页表,因此用户页表必须包含uservec的映射,然后由uservec来切换SATP以使用内核页表. 而为了在切换后不影响uservec的执行,uservec在用户页表和内核页表中必须使用相同的地址.Xv6使用tramponline page来实现这一功能, tramponlie page被映射到内核页表和所有用户页表的同一虚拟地址, 具体内容也就是tramponline.S.当执行用户代码时,STVEC设置为uservec。执行完uservec(具体看tramponline部分)后,接下就是usertrap,主要功能是确定trap原因,并处理返回。
  1. //kernel/trap.c
  2. void
  3. usertrap(void)
  4. {
  5.   int which_dev = 0;
  6.   
  7. //SSP保存了发生trap的模式,不为0说明不是来自用户空间的trap
  8.   if((r_sstatus() & SSTATUS_SPP) != 0)
  9.     panic("usertrap: not from user mode");
  10. //更改STVEC寄存器,使其指向kernelvec
  11. //这是内核空间中处理trap代码的地址
  12. //因为此时处于内核态
  13.   w_stvec((uint64)kernelvec);
  14.   struct proc *p = myproc();
  15.   
  16. //保存SPEC中的用户程序计数器
  17. //这是为了防止发生了进程切换导致覆盖了SEPC
  18. //因此将其保存到与进程关联的trapframe中
  19.   p->trapframe->epc = r_sepc();
  20.   
  21. //接下来根据trap的原因进行处理
  22. //系统调用
  23.   if(r_scause() == 8){
  24.     if(p->killed)
  25.       exit(-1);
  26. //应该恢复在下一条指令,因为当前触发trap的指令已经执行了
  27. //也就是ecall的下一条,因此+4
  28.     p->trapframe->epc += 4;
  29. //此时已经不会改变寄存器的状态了
  30. //修改SSTATUS的SIE位,开中断
  31.     intr_on();
  32. //转移给系统调用
  33.     syscall();
  34.   }
  35. //设备中断
  36.   else if((which_dev = devintr()) != 0){
  37.     // ok
  38.   } else {
  39.     printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
  40.     printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
  41.     p->killed = 1;
  42.   }
  43.   if(p->killed)
  44.     exit(-1);
  45. //放弃CPU时间
  46.   if(which_dev == 2)
  47.     yield();
  48. //跳转入usertrapret
  49.   usertrapret();
  50. }
复制代码
usertrapret的作用是准备内核态到用户态切换。这个函数还在第一次进入用户态时被调用,具体路径:userinit()->alloccproc()->forkret()->usertrapret()
  1. //kernel/usertrapret
  2. void
  3. usertrapret(void)
  4. {
  5.   struct proc *p = myproc();
  6. //关中断,防止出错
  7.   intr_off();
  8.   
  9. //这里是返回用户态
  10. //设置STVEC寄存器指向tramponline中的代码,这里就是uservec
  11.   w_stvec(TRAMPOLINE + (uservec - trampoline));
  12. //设置trapframe的数据,下一次从用户空间转换到内核空间可能会使用
  13.   p->trapframe->kernel_satp = r_satp();         // kernel page table
  14.   p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
  15.   p->trapframe->kernel_trap = (uint64)usertrap;
  16.   p->trapframe->kernel_hartid = r_tp();         // hartid for cpuid()
  17. //设置SSTATUS寄存器
  18.   unsigned long x = r_sstatus();
  19.   x &= ~SSTATUS_SPP; // 置SPP位为0控制sret返回到用户模式,实际上的切换在tramponline.S中
  20.   x |= SSTATUS_SPIE; // 开中断
  21.   w_sstatus(x);
  22.   //设置SEPC寄存器,确认返回后执行地址
  23.   w_sepc(p->trapframe->epc);
  24.   //准备好用户页表的satp值,实际山的切换在tramponline.S中
  25.   uint64 satp = MAKE_SATP(p->pagetable);
  26. //跳转进入userret
  27.   uint64 fn = TRAMPOLINE + (userret - trampoline);//计算跳转地址
  28.   ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);//传入fn,satp
  29. }
复制代码
来自内核空间的trap

这类trap的路径:kernelvec(kernel/kernelvec.S:10)->kerneltrap(kernel/trap.c:134)->kernelvec(kernel/kernelvec.S:48)
这时

  • 当内核在CPU上执行时,不同于在用户空间,直接使用内核页表和内核栈指针即可。因kernelvec会在被中断的内核进程的内核栈上分配空间直接保存寄存器。
  • 接下来kernelvec会调用kerneltrap
  • kerneltrap
  1. //从内核代码的中断和异常都会经由kernelvec到达这里
  2. void
  3. kerneltrap()
  4. {
  5. // 保存当前CPU的一些寄存器
  6. // 因为有可能当前处理的是一个时钟中断,进而会导致CPU的调度
  7. // 导致这些寄存器被覆盖
  8. // 所以必须保留下来以备将来恢复
  9.   int which_dev = 0;
  10.   uint64 sepc = r_sepc();
  11.   uint64 sstatus = r_sstatus();
  12.   uint64 scause = r_scause();
  13.   
  14. //检查是否是来自内核态的trap
  15.   if((sstatus & SSTATUS_SPP) == 0)
  16.     panic("kerneltrap: not from supervisor mode");
  17. //检查中断是否关闭
  18.   if(intr_get() != 0)
  19.     panic("kerneltrap: interrupts enabled");
  20. //使用devintr来确定中断的类型,它会去检查SCAUSE寄存器的值并进行处理
  21. //返回值表明中断的类型,0表明是异常
  22.   if((which_dev = devintr()) == 0){
  23.     printf("scause %p\n", scause);
  24.     printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
  25.     panic("kerneltrap");
  26.   }
  27. //时钟中断则放弃CPU
  28.   if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
  29.     yield();
  30. //恢复寄存器
  31.   w_sepc(sepc);
  32.   w_sstatus(sstatus);
  33. }
  34. //返回kernelvec
复制代码

  • 恢复寄存器,销毁新增的栈内存
  • 执行sret:复制SEPC的值到PC,开中断
tramponline.S

uservec函数

uservec是tramponline.S的第一部分。由于主要是汇编,这里就不贴源码了

  • 首先执行csrrw指令,这会交换a0和SSCRATCH两个寄存器寄存器的内容。交换后,a0寄存器中保存的就是tranpframe(kernel/proc.c)的地址,然后通过a0寄存器保存其他用户寄存器的数据。每个进程在创建时都会为trapframe分配一个页,并始终映射到TRAPFRAMKE虚拟地址,内核可以通过p->trapframe获得其物理地址来使用,这样trapframe page中就备份好了所有寄存器的数据。
  • 保存原来a0的值(现在是在SSCRATCH中),同样是保存到trapframe page中
  • 切换内核栈。trapframe中的kernel_sp保存了这个进程的kernel stack的栈顶,只需要将这里的值读入Stack Pointer寄存器(SP寄存器)即可
  • 写入CUP的hartid到tp寄存器,这是因为每个核都有独立的寄存器
  • 向t0寄存器写入将要执行的函数指针
  • 向SATP寄存器写入内核页表地址(实际上是先写入t1,然后交换t1和SATP),清空TLB。
  • 最后跳入t0(一般就是usertrap)。
userret函数

userret是tramponline.S的最后部分,也就是负责切换回用户空间。

  • 设置SATP寄存器,切换为用户页表,并清空TLB
  • 恢复SSCRATCH寄存器为traponline page中a0寄存器的值,这个值实际上已经被系统调用的返回值覆盖;
  • 恢复寄存器。此时还剩下a0寄存器:trapframe的地址;SSCRACH寄存器:返回值
  • 交换a0和SSCRATCH
  • 调用sret:切换为user mode,将SEPC寄存器的值拷贝回PC,重新打开中断
这样就回到了用户空间。
backtrace

在kernel/printfc.中实现一个backtrace()函数,要求能够打印调用过的函数地址。
没想到在Lab3debug的时候写的代码在这里也有。
获取当前的调用函数栈帧

1.png

如图所示,每次函数调用的时候都会在栈上创建一个栈帧。fp寄存器指向当前栈帧的开始地址,sp寄存器指向当前栈帧的结束地址,注意栈是从高地址往低地址增长的,因此fp的地址是比sp高的。fp(-8字节)处是当前栈帧的返回地址,fp(-16字节)是上一个栈帧的起始地址。根据提示通过如下代码获取当前栈帧的fp。
  1. //kernel/riscv.h
  2. static inline uint64
  3. r_fp()
  4. {
  5.   uint64 x;
  6.   asm volatile("mv %0,s0" : "=r" (x));
  7.   return x;
  8. }
复制代码
实现backtrace()
  1. //kernel/printf.c
  2. void
  3. backtrace()
  4. {
  5.   uint64 fp = r_fp();
  6.   //只会为栈分配一页
  7.   uint64 top = PGROUNDUP(fp);
  8.   uint64 bottom = PGROUNDDOWN(fp);
  9.   printf("backtrace:\n");
  10.   while( fp < top && fp > bottom ){
  11.     uint64 ra = *(uint64*)(fp-8);
  12.     printf("%p\n",ra);
  13.     fp = *(uint64*)(fp-16);
  14.   }
  15. }
  16. //声明
  17. //kernel/defs.h
  18. void            backtrace();
  19. //最后在kernel/sysproc.c的sys_sleep中调用即可
  20. uint ticks0;
  21. backtrace();
复制代码
运行qemu,然后运行bttest。验证后确实是对应的函数。
  1. 0x0000000080002cdc
  2. 0x0000000080002bb6
  3. 0x00000000800028a0
复制代码
Alarm

实现sigalarm(interval,handler)和sigreturn系统调用,为使用CPU的用户进程实现定期通知功能。sigalarm的功能是当进程使用interval个tick后,调用一次handler。
sigreturn()的功能是从handler返回原来中断的位置。
系统调用准备

模仿Lab2的步骤,准备系统调用的相关代码。
  1. //user/user.h
  2. //syscall.c
  3. int sigalarm(int ticks, void(*handler)() );
  4. int sigreturn(void);
  5. //系统调用入口
  6. //user/usys.pl
  7. entry("sigalarm");
  8. entry("sigreturn");
  9. //系统调用号
  10. //kernel/syscall.h
  11. #define SYS_sigalarm 22
  12. #define SYS_sigreturn 23
  13. //声明处理函数,并与系统调用号关联
  14. //kernel/syscall.c
  15. extern uint64 sys_sigalarm(void);
  16. extern uint64 sys_sigreturn(void);
  17. [SYS_sigalarm] sys_sigalarm,
  18. [SYS_sigreturn] sys_sigreturn,
复制代码
实现alarm
  1. //修改进程结构体,添加必要的属性
  2. //kernel/proc.h:proc
  3.   char name[16];               // Process name (debugging)
  4.   //新增
  5.   int alarm_interval;          //报警间隔
  6.   void (*alarm_handeler)();    //相应的处理函数
  7.   int alarm_ticks_count;       //自从上次调用经过的ticks
  8.   struct trapframe* alarm_trapframe; //保存中断之前的trapframe,用于恢复
  9.   int alarm_on;                       //是否已有alarm在处理中
复制代码
  1. //初始化
  2. //kernel/proc.c:allocproc()
  3.   // Allocate a trapframe page.
  4.   if((p->trapframe = (struct trapframe *)kalloc()) == 0){
  5.     release(&p->lock);
  6.     return 0;
  7.   }
  8.   //新增
  9.   if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
  10.     release(&p->lock);
  11.     return 0;
  12.   }
  13.   p->alarm_interval = 0;
  14.   p->alarm_handeler = 0;
  15.   p->alarm_ticks_count = 0;
  16.   p->alarm_on = 0;
复制代码
  1. //释放
  2. //kernel/proc.c:freeproc()
  3.   if(p->trapframe)
  4.     kfree((void*)p->trapframe);
  5.   p->trapframe = 0;
  6.   //新增
  7.   if(p->alarm_trapframe)
  8.     kfree((void*)p->alarm_trapframe);
  9.   p->alarm_trapframe = 0;
  10.   
  11.   p->xstate = 0;
  12.   //新增
  13.   p->alarm_interval = 0;
  14.   p->alarm_interval = 0;
  15.   p->alarm_ticks_count = 0;
  16.   p->alarm_on = 0;
复制代码
实现sigalarm和sigreturn
  1. //kernel/sysproc.c
  2. uint64
  3. sys_sigalarm(void)
  4. {
  5.   int interval;   //报警间隔
  6.   uint64 handler; //处理函数地址
  7.   if( argint(0, &interval) < 0)
  8.     return -1;
  9.   if( argaddr(1, &handler) < 0)
  10.     return -1;
  11.   struct proc* p = myproc();
  12.   p->alarm_interval = interval;
  13.   p->alarm_handeler = (void(*)())handler;
  14.   p->alarm_ticks_count = 0;
  15.   p->alarm_on = 0;
  16. return 0;
  17.   
  18. }
  19. uint64
  20. sys_sigreturn(void)
  21. {
  22.   struct proc* p = myproc();
  23.   *p->trapframe = *p->alarm_trapframe;
  24.   p->alarm_on = 0;
  25.   return 0;
  26. }
复制代码
在usertrap中实现该时钟中断的代码
  1.   // give up the CPU if this is a timer interrupt.
  2.   if(which_dev == 2){
  3.     struct proc* p = myproc();
  4. //如果设置了时钟
  5.     if( p->alarm_interval > 0){
  6.       p->alarm_ticks_count++;
  7. //到达设定好的时钟计时,并且没有其他时钟在运行
  8. //如果有则重置计时,延迟触发
  9.       if(p->alarm_ticks_count >= p->alarm_interval && p->alarm_on == 0){
  10.         p->alarm_ticks_count = 0;
  11.         p->alarm_on = 1;
  12.         *p->alarm_trapframe = *p->trapframe;
  13.         p->trapframe->epc = (uint64)p->alarm_handeler;
  14.       }
  15.     }
  16.     yield();
  17.   }
复制代码
最后修改Makefile,添加$U/_alarmtest
测试结果:
  1. $ alarmtest
  2. test0 start
  3. .................................................alarm!
  4. test0 passed
  5. test1 start
  6. ......alarm!
  7. ......alarm!
  8. ......alarm!
  9. .......alarm!
  10. .......alarm!
  11. ......alarm!
  12. ......alarm!
  13. ......alarm!
  14. .......alarm!
  15. .......alarm!
  16. test1 passed
  17. test2 start
  18. .............................................................alarm!
  19. test2 passed
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册