lab 3

Part A: User Environments and Exception Handling


在 JOS 中,Environment 等同于 Process。

struct Env *envs = NULL;        // All environments
struct Env *curenv = NULL;      // The current env
static struct Env *env_free_list;   // Free environment list

用这三个变量来保存 user environment 的相关信息。
envs 储存目前活跃的 environment ,最大支持 NENV 这么多个,在初始化时数组大小就为 NENV
curenv 是当前执行的 environment。
env_free_list 是目前不活跃的 environment。

注:JOS 并不像 linux 和 xv6 一样,每个进程(environment)都有自己的 kernel stack,JOS 只有一个全局的 kernel stack。
// TODO:在 lab 2 中实现的虚拟地址空间,kernel space 是在 user space 上面。那如何解释只有一个全局 kernel stack? 切换的时候发生什么?

  • Exercise 1:
    修改 kern/pmap.c 中的 mem_init(),为 envs 申请空间并完成虚拟地址的映射。

    注:这里申请的这块空间的 perm 为 PTE_U,即用户可读,但之后真正分配出去的 envs 应该是用户无权访问的(一个进程不应该能访问到其他进程的信息)。

接下来要创建并运行 environment 。由于目前还没有文件系统,所以我们运行的都是嵌在 kernel 中的 ELF 文件。

  • Exercise 2
    1. env_init():
      初始化 env 结构体链,全部初始化成空的。形成顺序的链。

    2. env_setup_vm():
      初始化进程的页表,并赋值给 env_pgdir。先申请一个 Page 的空间。接下来由注释知,需要讲 p->pp_ref 加一,然后赋值 kern_pgdir 中 UTOP 以上的内容到当前的 env_pgdir。

    3. region_alloc():
      为当前的进程申请一块物理内存空间(参数 len 这么大),然后映射到参数 va 指定的虚拟地址。按照注释提示将 va 做 ROUNDDOWN,将 va + len 做 ROUNDUP,然后从 beg 到 end 申请个 Page,如果申请失败则 panic。对每块申请到的 page,调用 lab2 中写的 page_insert 函数形成虚实映射。

    4. load_icode():
      这个函数将 ELF 文件分段分析并载入内存,一一申请物理空间并形成虚实映射(使用前面写的 region_alloc 函数) 。写法可类比 boot/main.c 。需要注意的是,根据注释提示:

      // Loading the segments is much simpler if you can move data
      // directly into the virtual addresses stored in the ELF binary.
      // So which page directory should be in force during
      // this function?

      我们应该把这个 ELF 文件加载到用户的地址空间里(目前只有一个 user space),所以在 for 循环之前我们应该切换 cr3e->env_pgdir,在 for 循环结束后应该切换 cr3 回到 kern_pgdir

      注意:这里不要忘记设置当前 env 的 tf 的 eip 为当前处理的 elf 文件的 entry point:

      e->env_tf.tf_eip = elf->e_entry;
      

      另: 这个函数的实现里好像并没有用到参数 size

    5. env_create():
      这个函数将调用 env_alloc() ,申请一个新的 environment 。我们需要设置该 env 类型为传入的类型并且调用 load_icode() 载入 ELF 文件。

    6. env_run():
      这个函数将完成进程切换。我们需要填入的是更改新旧进程状态,切换 cr3 ,并且调用 env_pop_tf 来完成对 trapfram 的保存。

  • Basics of Protected Control Transfer
    接下来要做的是处理 interrupt 和 exception(统称为 protected control transfer),其中 interrupt 为异步,exception 为同步。

    为了确保 protected control transfer 是安全的,在任一个进程发生 interrupt 要进入 kernel 的时候不是在任意地方以任意方式进入,而是要通过统一的入口进入。在 X86 架构下,有两个机制实现了这一点:

    1. IDT(Interrupt Descriptor Table)
      不同来源不同情况的中断会带有不同的 interrupt vector ([0,255] ,X86 最高支持 256 个),可以理解为不同种类的 interrupt 的 ID。CPU 在收到中断后,以 interrupt vector 的值作为索引从 IDT 里去查找对应的条目。找到对应条目读取:
      1)对应的中断 handler 在内核中的代码位置,来装入 EIP。
      2)将要加载到 CS(代码段)寄存器里的值。
    2. TSS(Task State Segment)
      interrupt 发生时,cpu 需要保存当前马上要被切走的进程的信息,保存的位置应该在内核里,以免邪恶的程序改其他进程的信息。
      所以在 interrupt 发生时,cpu 会将旧进程的 SS, ESP, EFLAGS, CS, EIP, and an optional error code 等信息 push 到内核某处的一个栈上。而 TSS 保存的信息就是如何定位这个栈。然后 cpu 才会去读 IDT 改 EIP。

    所有同步的 exception 的 vector 序号都在 [0,31] 之间;所有异步的 interrupt 的 vector序号都大于 31。

    有的 interrupt 发生的时候还会往栈上 push 一个独特的 error code,具体参考。

    当前在内核态也可能触发 interrupt/exception,这时不会再换栈(因为已经在内核态了),直接把必要的信息 push 在当前的栈上。可嵌套,但嵌套能力有限。

  • Exercise 4
    硬编码编到头皮发麻两眼昏花

  • Question

    1. What is the purpose of having an individual handler function for each exception/interrupt? (i.e., if all exceptions/interrupts were delivered to the same handler, what feature that exists in the current implementation could not be provided?)

    每种 interrupt 需要不同的处理(主要是否返回原程序继续执行),然而对于一部分 interrupt ,cpu 是不会 push error code 的,如果不分开多个 handler 处理就无法清到底是哪种。

    1. Did you have to do anything to make the user/softint program behave correctly? The grade script expects it to produce a general protection fault (trap 13), but softint's code says int $14. Why should this produce interrupt vector 13? What happens if the kernel actually allows softint's int $14 instruction to invoke the kernel's page fault handler (which is interrupt vector 14)?

    因为如果系统运行在用户态,权限级别为 3,而 INT 指令是系统指令,权限级别为 0,因此会首先引发 Gerneral Protection Excepetion(即 trap 13)。由 SETGATE 函数定义上方注释可知,通过改变参数 dpl 可以改变调用该 interrupt 需要的权限等级。通过把原来 dpl = 0 的改成 dpl = 3,就可以让用户态程序也可以调用。

Part B: Page Faults, Breakpoints Exceptions, and System Calls


  • Handling Page Faults
    trap_dispatch 函数中加一个 switch 语句,检查 tf->tf_trapnoT_PGFLT 的话就掉用 page_fault_handler 函数。

  • Exercise 6
    这部分需要实现 system call,代码比较分散比较烦,具体要做的有:

    • 完成 kern/trapentry.S 里的 sysenter_handler,主要需要按顺序把装有参数的寄存器 push 到栈上并掉用 syscall:

      pushl %edi
      pushl %ebx
      pushl %ecx
      pushl %edx
      pushl %eax
      call syscall
      movl %ebp, %ecx
      movl %esi, %edx
      sysexit
      
    • kern/syscall.c 中实现不同编号的 syscall 的分发:

      switch(syscallno){
        case SYS_cputs:
            sys_cputs((char* a1), (size_t)a2);
            return 0;
        case SYS_cgetc:
            return sys_cgetc();
        case SYS_getenvid:
            return sys_getenvid();
        case SYS_env_destroy:
            return sys_env_destroy((envid_t)a1);
        case SYS_map_kernel_page:
            return sys_map_kernel_page((void *)a1, (void*)a2);
        case SYS_sbrk:
            return sys_sbrk((uint32_t) a1);
      
        default:
            return -E_INVAL;
      }
      
    • inc/x86.h 中加入 wrmsr 的代码:

      /* If your binutils don't accept this: upgrade! */
      #define rdmsr(msr,val1,val2) \
      __asm__ __volatile__("rdmsr" \
      : "=a" (val1), "=d" (val2) \
      : "c" (msr))
      
      #define wrmsr(msr,val1,val2) \
      __asm__ __volatile__("wrmsr" \
      : /* no outputs */ \
      : "c" (msr), "a" (val1), "d" (val2))
      
    • kern/trap.ctrap_init_percpu 函数中加上 sysenter_handler 的声明和 MSR 的注册:

      /*Lab3 code :*/
      // set MSR for sysenter
      extern void sysenter_handler();
      wrmsr(0x174, GD_KT, 0);     /* SYSENTER_CS_MSR */
      wrmsr(0x175, KSTACKTOP, 0); /* SYSENTER_ESP_MSR */
      wrmsr(0x176, (uint32_t)sysenter_handler, 0);            /* SYSENTER_EIP_MSR */
      
    • 最后实现 lib/syscall.c 中的汇编代码 syscall(通过 push 和 pop 避免直接对 ebp 操作):

      //Lab 3: Your code here
       "pushl %%esp\n\t"
       "popl %%ebp\n\t"
       "leal after_sysenter_label%=, %%esi\n\t"
       "sysenter\n\t"
       "after_sysenter_label%=:\n\t"
      
  • Exercise 7:

    thisenv = &envs[ENVX(sys_getenvid())];
    
  • Exercise 8:
    这部分要实现扩充堆容量的 sys_sbrk 函数。首先需要在 Env 结构体加一个变量记录堆顶位置:

    // LAB3: might need code here for implementation of sbrk
    uintptr_t env_heaptop;
    

    然后需要在 kern/env.c 中的 load_icode 函数中对这个变量做初始化:

    e->env_heaptop = UTEXT;
    
      for(ph; ph < eph; ph++){
          if(ph->p_type == ELF_PROG_LOAD){
              if (ph->p_va + ph->p_memsz > e->env_heaptop) {
                  e->env_heaptop = ROUNDUP(ph->p_va + ph->p_memsz, PGSIZE);
              }
              region_alloc(e, (void *)ph->p_va, ph->p_memsz);
              memset((void *)ph->p_va, 0,ph->p_memsz);
              memmove((void *)ph->p_va, binary + ph->p_offset, ph->p_filesz);
          }
      }
    

    在拷贝 ELF 文件的时候一旦发现超出 env_heaptop 的范围立马扩充。

    最后实现 kern/syscall.c 里的 sys_sbrk 函数:

      static int
      sys_sbrk(uint32_t inc)
      {
          // LAB3: your code sbrk here...
    
          uint32_t norm_inc = (uint32_t)ROUNDUP(inc, PGSIZE);
          region_alloc(curenv, (void *)curenv->env_heaptop, norm_inc);
          curenv->env_heaptop += norm_inc;
          return curenv->env_heaptop;
      } 
    
  • Exercise 9:
    这里要实现 breakpoint exception 的 dispatch 注册和类似于 GDB 中的 c,si,x 指令。
    首先,在 trap_dispatch 函数中加上:

      case T_BRKPT:
          monitor(tf);
          return;
    

    当触发 T_BRKPT 这个 trap 的时候,系统调用 monitor。(这里的 monitor 可以理解为一个类似于 GDB 的 Debugger)

    接下来要添加三个新指令,流程和之前 lab2 做 challenge 时候一样,在 kenr/monitor.h 中先声明,再去 kern/monitor.c 中注册。

    从维基查到 EFLAGS 中有一位 TF(Trap Flag)位专门控制 single step,那我们在 mon_cmon_si 函数中就要来回修改这一位, 并且在 mon_si 中按照例子中给出的格式打印信息。

    kern/kdebug.h 这个文件中找到了我们需要的得到信息的数据结构 Eipdebuginfo ,实现如下:

    int mon_c(int argc, char **argv, struct Trapframe *tf)
    {
        tf->tf_eflags &= ~FL_TF;
        return -1;
    }
    
     
    int mon_si(int argc, char **argv, struct Trapframe *tf){
        struct Eipdebuginfo info;
        tf->tf_eflags |= FL_TF;
        uint32_t eip = tf->eip;
        debuginfo_eip(eip, &info);
        cprintf("tf_eip=%08x\n%s:%u %.*s+%u\n",
          eip,info.eip_file, info.eip_line, info.eip_fn_namelen, info.eip_fn_name,
             eip - (uint32_t)info.eip_fn_addr);
        return -1;
    }
    

    最后的 mon_x ,寻址的这一步用 asm 代码实现,省去 16 进制向 2 进制转换的麻烦:

      int mon_x(int argc, char **argv, struct Trapframe *tf){
    
          if (argc != 2) {
            cprintf("Usage: \n");
            return 0;
          }
          uintptr_t address = (uintptr_t)strtol(argv[1], NULL, 16);
          uint32_t value;
          __asm __volatile("movl (%0), %0" : "=r" (value) : "r" (address));
          cprintf("%d\n", value);
          return 0;
      }
    

    实现了三个函数后发现没法通过 make grade 的测试,好在有之前同学踩坑的经验,想起来在 trap_dispatch 里要把 T_DEBUG 也 dispatch 到 monitor:

      case T_DEBUG:
      case T_BRKPT:
          monitor(tf);
          return;
    

    改后通过测试。

  • Questions

    1. The break point test case will either generate a break point exception or a general protection fault depending on how you initialized the break point entry in the IDT (i.e., your call to SETGATE from trap_init). Why? How do you need to set it up in order to get the breakpoint exception to work as specified above and what incorrect setup would cause it to trigger a general protection fault?

    kern/trap.ctrap_init 函数中,我们设置 GATE 的时候的最后一个参数决定了触发这个 trap 所需要的 privilege level (在 inc/mmu.hSETGATE 宏的注释里有写)。如果这个 trap 我们希望从 user mode 触发(比如 break point exception),那就设置成 3,这样就不会因为权限不够而先触发了 general protection fault。

    1. What do you think is the point of these mechanisms, particularly in light of what the user/softint test program does?

    可能会对 kernel 造成严重应像的 trap 应该严格限制权限,让用户态无法触发;不会对 kernel 造成严重应像且有必要让用户态触发的 trap 应该赋予用户权限。

  • Exercise 10:
    首先修改 kern/trap.c 中的 page_fault_handler 函数使得它能检查出如果当前 page fault 来自 kernel,就 panic:

      // LAB 3: Your code here.
      if(!(tf->tf_cs & 0x3)){
              panic("page falut happens in kernel mode.\n");
      }
    

    然后实现 kern/pmap.c 中的 user_mem_check 函数。检查用户试图访问的地址是否在 ULIM 之下且那个 page 的权限可以让用户访问。

    需要注意的是:传入的地址没做对其,需要手动检查附近的每个 Page。需要用到之前 lab 写的 pgdir_walk 函数。

    最后在 kern/syscall.c 中的 sys_cputs 函数填入刚刚实现的函数:

    user_mem_assert(curenv, (void*)s, len, PTE_U);
    
  • Exercise 12:
    这部分自己不是很理解,在看了网上的攻略之后总算磕磕绊绊地完成了。最终也只是似懂非懂的样子。

你可能感兴趣的:(lab 3)