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:
env_init()
:
初始化 env 结构体链,全部初始化成空的。形成顺序的链。env_setup_vm()
:
初始化进程的页表,并赋值给 env_pgdir。先申请一个 Page 的空间。接下来由注释知,需要讲 p->pp_ref 加一,然后赋值 kern_pgdir 中 UTOP 以上的内容到当前的 env_pgdir。region_alloc()
:
为当前的进程申请一块物理内存空间(参数 len 这么大),然后映射到参数 va 指定的虚拟地址。按照注释提示将 va 做 ROUNDDOWN,将 va + len 做 ROUNDUP,然后从 beg 到 end 申请个 Page,如果申请失败则 panic。对每块申请到的 page,调用 lab2 中写的page_insert
函数形成虚实映射。-
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 循环之前我们应该切换
cr3
到e->env_pgdir
,在 for 循环结束后应该切换cr3
回到kern_pgdir
。注意:这里不要忘记设置当前 env 的 tf 的 eip 为当前处理的 elf 文件的 entry point:
e->env_tf.tf_eip = elf->e_entry;
另: 这个函数的实现里好像并没有用到参数
size
。 env_create()
:
这个函数将调用env_alloc()
,申请一个新的 environment 。我们需要设置该 env 类型为传入的类型并且调用load_icode()
载入 ELF 文件。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 架构下,有两个机制实现了这一点:
- IDT(Interrupt Descriptor Table)
不同来源不同情况的中断会带有不同的interrupt vector
([0,255] ,X86 最高支持 256 个),可以理解为不同种类的 interrupt 的 ID。CPU 在收到中断后,以 interrupt vector 的值作为索引从 IDT 里去查找对应的条目。找到对应条目读取:
1)对应的中断 handler 在内核中的代码位置,来装入 EIP。
2)将要加载到 CS(代码段)寄存器里的值。 - 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 在当前的栈上。可嵌套,但嵌套能力有限。
- IDT(Interrupt Descriptor Table)
Exercise 4:
硬编码编到头皮发麻两眼昏花-
Question:
- 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 处理就无法清到底是哪种。
- 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_trapno
是T_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.c
的trap_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_c
和mon_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:
- 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.c
的trap_init
函数中,我们设置 GATE 的时候的最后一个参数决定了触发这个 trap 所需要的 privilege level (在inc/mmu.h
的SETGATE
宏的注释里有写)。如果这个 trap 我们希望从 user mode 触发(比如 break point exception),那就设置成 3,这样就不会因为权限不够而先触发了 general protection fault。- 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:
这部分自己不是很理解,在看了网上的攻略之后总算磕磕绊绊地完成了。最终也只是似懂非懂的样子。