快找工作了,一直没更新,放假一周的时间抽了点工夫做了LAB4的PART B,总体来说还是感觉比较难的,尤其是一段汇编代码和异常栈那乱七八糟的堆栈。
一、概述
本部分实验主要是实现一个copy on write的fork函数,第一步是实现一个用户态的page fault处理机制:首先用户态使用一个系统调用传递给内核态一个函数指针作为page fault的回调函数,接着当发生page fault时内核进行简单的判断将该函数需要的一个特殊数据结构压栈,再使用iret跳到用户态执行此回调函数,执行完之后接着继续执行原先的用户态函数。第二步是在此基础上实现一个copy on write的fork,首先复制父进程地址空间的映射(也就是页目录和页表),然后把对应的页表全部变成不可写,并在保留位加入特殊符号,因此当写操作时候会报page fault错,报错后转入用户态的,在用户态把出错的页面复制后进行重新映射,之后继续返回源程序执行。
看起来不复杂,实际调试起来非常繁琐,内核crash上百次后总算是调通了。
二、实验
Exercise 7
实现一个设置page_fault_upcall的系统调用,较为简单:
- static int
- sys_env_set_pgfault_upcall(envid_t envid, void *func)
- {
- // LAB 4: Your code here.
- struct Env* env;
- int ret=envid2env(envid,&env,1);
- if(ret<0)
- return ret;
- env->env_pgfault_upcall=func;
- cprintf("func :0x%x \r\n",func);
- return 0;
- //panic("sys_env_set_pgfault_upcall not implemented");
- }
Exercise 8
实现内核态的page_fault处理函数,该函数负责跳转到用户态的upcall(也就是pfentry.S),并为用户态的page_fault_handler设置好参数。
为什么要在用户态进行处理,即使用env_run进而调用而不是直接跳转到upcall函数指针处执行?个人认为,是因为直接在内核态处理过于危险,用户可以借此注入高权限的恶意代码。
- void
- page_fault_handler(struct Trapframe *tf)
- {
- uint32_t fault_va;
- fault_va = rcr2();
- if((tf->tf_cs & 3)==0)
- {
- //内核态的错误依然没法处理
- print_trapframe(tf);
- panic("kernel mode page faults!!");
- }
- //判断用户是否给异常栈进行了映射
- user_mem_assert(curenv,(void*)(UXSTACKTOP-PGSIZE),PGSIZE,0);
- if(curenv->env_pgfault_upcall==NULL )
- {
- //没有注册用户态的upcall函数
- cprintf("[%08x] user fault va %08x ip %08x\n",
- curenv->env_id, fault_va, tf->tf_eip);
- env_destroy(curenv);
- return ;
- }
- //构造数据结构,并复制,这个数据结构将传递给用户态的处理函数
- struct UTrapframe utf;
- memmove((void*)(&utf.utf_regs),(void*)(&(tf->tf_regs)),sizeof(tf->tf_regs));//复制寄存器
- (&utf)->utf_eflags=tf->tf_eflags;//复制flags
- (&utf)->utf_eip=tf->tf_eip;//复制eip
- (&utf)->utf_err=tf->tf_err;//复制err
- (&utf)->utf_esp=tf->tf_esp;//复制esp
- (&utf)->utf_fault_va=fault_va;
- int espaddr=0;
- if(tf->tf_esp>=UXSTACKTOP-PGSIZE && tf->tf_esp<=UXSTACKTOP-1)
- {
- //运行到这里说明是在用户态的异常处理函数里产生了异常
- struct Page* page=page_lookup(curenv->env_pgdir,(void*)(tf->tf_esp-4),0);
- if(page==NULL)
- {
- cprintf("non Page ...\r\n");
- page=page_alloc(ALLOC_ZERO);
- page_insert(curenv->env_pgdir,page,(void*)(tf->tf_esp-4),PTE_U|PTE_W);
- }
- memmove((void*)((tf->tf_esp)-4-sizeof(utf)),&utf,sizeof(utf));
- espaddr=tf->tf_esp-4-sizeof(utf);//新的栈顶
- }
- else
- {
- //将UTrapframe放到栈顶
- memmove((void*)(UXSTACKTOP-sizeof(utf)),&utf,sizeof(utf));
- espaddr=UXSTACKTOP-sizeof(utf);//改变栈指针,注意栈的生长是从高到底生长
- }
- struct Env *env=curenv;
- int calladdr=Paddr((int)env->env_pgfault_upcall);
- curenv->env_tf.tf_eip=(int)env->env_pgfault_upcall;//将eip设置为upcall
- curenv->env_tf.tf_esp=espaddr;//设置堆栈地址
- env_run(curenv);//返回用户态执行
- }
Exercise 9
完成pfentry.S,主要是在用户态的page_fault_handler结束后如何恢复现场并跳回原程序执行。
- .text
- .globl _pgfault_upcall
- _pgfault_upcall:
-
- pushl %esp
- movl _pgfault_handler, %eax
- call *%eax
- addl $4, %esp
-
-
- addl $8, %esp
- movl %esp,%eax
- addl $32,%esp
- popl %ebx
- addl $4,%esp
- popl %esp
- pushl %ebx
- movl %eax,%esp
- popal
- addl $4,%esp
- popf
- popl %esp
- subl $4,%esp
- ret
这段代码较为难以阅读,首先给出_pgfault_handler结束后的堆栈:
// trap-time esp
// trap-time eflags
// trap-time eip
// utf_regs.reg_eax
// ...
// utf_regs.reg_esi
// utf_regs.reg_edi
// utf_err (error code)
// utf_fault_va <-- %esp
然后按顺序汇编代码做了这么以下几件事:
首先esp+8,即跳过utf_fault_va和errcode,指向reg_edi。
然后把这个esp存放在eax中。
接着esp+32,即指向trap-time eip。
然后调用popl,此时eip存放在了ebx中,esp指向eflags
然后跳过eflags,指向trap-time esp
接着把这个esp出栈替代原先的esp。
把ebx里的内容,也就是trap-time eip压入新的堆栈里
将eax里的内容放入esp,此时esp又重新指向reg_edi
使用popal恢复所有寄存器
esp+4,跳过trap-time eip,然后popf恢复eflags。此时esp指向trap-time esp
接着此esp出栈并替换原esp。
然后esp-4,即指向我们之前压入的trap-time eip
调用ret,弹出指令后堆栈指向trap-time esp所指向的位置,程序能够正常执行。
Exercise 10
完成用户态的set_pgfault_handler函数,较为简单
- void
- set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
- {
- int r;
- if (_pgfault_handler == 0) {
- //如果是第一次赋值,则要先非配异常栈,然后再设置upcall
- int envid=sys_getenvid();
- int r=sys_page_alloc(envid,(void*)UXSTACKTOP-PGSIZE,PTE_U|PTE_W|PTE_P);
- if(r<0)
- {
- panic("alloc uxstack fail");
- }
- sys_env_set_pgfault_upcall(envid, (void*) _pgfault_upcall);
-
- }
-
- // Save handler pointer for assembly to call.
- _pgfault_handler = handler;
- }
Exercise 11
首先我发现了一个我在env.c中env_setup_vm中的一个错误,我只复制了页目录,没有复制页表导致所有进程共享了一个页表,一个修改导致其余的也修改。下面是改正后的函数:
- static int
- env_setup_vm(struct Env *e)
- {
- int i;
- struct Page *p = NULL;
- cprintf("env_setup_vm\r\n");
- // Allocate a page for the page directory
- if (!(p = page_alloc(ALLOC_ZERO)))
- return -E_NO_MEM;
- e->env_pgdir=page2kva(p);
- for(i=PDX(UTOP);i<1024;i++)
- {
- if(kern_pgdir[i]!=0)
- {
-
- struct Page* page=page_alloc(ALLOC_ZERO);
- e->env_pgdir[i]=(int)page2pa(page)|PTE_P|PTE_W|PTE_U;
- if(page==NULL)
- {
- return -E_NO_MEM;
- }
- struct Page* kernpage=pa2page(PTE_ADDR(kern_pgdir[i]));
- memmove(page2kva(page),page2kva(kernpage),PGSIZE);
- }
-
- }
- p->pp_ref++;
- page_insert(e->env_pgdir,p,(void*)UVPT,PTE_P|PTE_U);
- return 0;
- }
给出fork.c整个文件,较为简单,即使出错也是因为一些粗心导致的错误。
- // implement fork from user space
-
- #include <inc/string.h>
- #include <inc/lib.h>
-
- // PTE_COW marks copy-on-write page table entries.
- // It is one of the bits explicitly allocated to user processes (PTE_AVAIL).
- #define PTE_COW 0x800
-
- //
- // Custom page fault handler - if faulting page is copy-on-write,
- // map in our own private writable copy.
- //
- static void
- pgfault(struct UTrapframe *utf)
- {
- void *addr = (void *) utf->utf_fault_va;
- uint32_t err = utf->utf_err;
- int r;
- extern volatile pte_t vpt[];
- if((vpt[PDX(addr)] & (0 |PTE_W |PTE_COW))==0)
- {
- panic("PTE WRONG!!\r\n");
- }
-
- int envid=sys_getenvid();
- int result=sys_page_alloc(envid,PFTEMP,PTE_U|PTE_W|PTE_P);
-
- memmove(PFTEMP,ROUNDDOWN(addr,PGSIZE),PGSIZE);
- sys_page_map(envid,(void*) PFTEMP,envid,(void*)ROUNDDOWN(addr,PGSIZE), PTE_U|PTE_W|PTE_P);
- }
-
- //
- // Map our virtual page pn (address pn*PGSIZE) into the target envid
- // at the same virtual address. If the page is writable or copy-on-write,
- // the new mapping must be created copy-on-write, and then our mapping must be
- // marked copy-on-write as well. (Exercise: Why do we need to mark ours
- // copy-on-write again if it was already copy-on-write at the beginning of
- // this function?)
- //
- // Returns: 0 on success, < 0 on error.
- // It is also OK to panic on error.
- //
- static int
- duppage(envid_t envid, unsigned pn)
- {
- int r=sys_getenvid();
- int result=0;
- int perm=0;
- if(pn*PGSIZE==UXSTACKTOP-PGSIZE)
- return 0; //整个地址空间除异常栈之外全部进行重新映射
- perm = (perm |PTE_P| PTE_U|PTE_COW );
- result=sys_page_map(r, (void*)(pn*PGSIZE),envid, (void*)(pn*PGSIZE), perm);
- result=sys_page_map(r, (void*)(pn*PGSIZE),r, (void*)(pn*PGSIZE), perm);
- return 0;
- }
-
- //
- // User-level fork with copy-on-write.
- // Set up our page fault handler appropriately.
- // Create a child.
- // Copy our address space and page fault handler setup to the child.
- // Then mark the child as runnable and return.
- //
- // Returns: child's envid to the parent, 0 to the child, < 0 on error.
- // It is also OK to panic on error.
- //
- // Hint:
- // Use vpd, vpt, and duppage.
- // Remember to fix "thisenv" in the child process.
- // Neither user exception stack should ever be marked copy-on-write,
- // so you must allocate a new page for the child's user exception stack.
- //
- envid_t
- fork(void)
- {
- // LAB 4: Your code here.
- //panic("fork not implemented");
- //cprintf("this is Fork!\r\n");
- set_pgfault_handler(pgfault);
- envid_t envid;
- uint8_t *addr;
- int r;
- extern unsigned char end[];
- envid = sys_exofork();
- if (envid < 0)
- panic("sys_exofork: %e", envid);
- if (envid == 0) {
- thisenv = &envs[ENVX(sys_getenvid())];
- return 0;
- }
- // cprintf("user : create new env finish! %d\r\n",envid);
- sys_page_alloc(envid,(void*)UXSTACKTOP-PGSIZE,PTE_U|PTE_W|PTE_P);
- extern volatile pte_t vpt[];
- int i,j;
- for(i=0;i<=UTOP/PGSIZE-1;i++)
- {
- if((vpt[i/1024] &(0|PTE_P))!=0 )
- {
- duppage(envid,i);
- }
- }
- if ((r = sys_env_set_status(envid, ENV_RUNNABLE)) < 0)
- panic("sys_env_set_status: %e", r);
- cprintf("this is Fork finish!!\r\n");
- return envid;
-
- }
-
- // Challenge!
- int
- sfork(void)
- {
- panic("sfork not implemented");
- return -E_INVAL;
- }