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
用這三個(gè)變量來(lái)保存 user environment 的相關(guān)信息。
envs 儲(chǔ)存目前活躍的 environment ,最大支持 NENV 這么多個(gè),在初始化時(shí)數(shù)組大小就為 NENV。
curenv 是當(dāng)前執(zhí)行的 environment。
env_free_list 是目前不活躍的 environment。
注:JOS 并不像 linux 和 xv6 一樣,每個(gè)進(jìn)程(environment)都有自己的 kernel stack,JOS 只有一個(gè)全局的 kernel stack。
// TODO:在 lab 2 中實(shí)現(xiàn)的虛擬地址空間,kernel space 是在 user space 上面。那如何解釋只有一個(gè)全局 kernel stack? 切換的時(shí)候發(fā)生什么?
-
Exercise 1:
修改kern/pmap.c中的mem_init(),為 envs 申請(qǐng)空間并完成虛擬地址的映射。注:這里申請(qǐng)的這塊空間的 perm 為 PTE_U,即用戶可讀,但之后真正分配出去的 envs 應(yīng)該是用戶無(wú)權(quán)訪問(wèn)的(一個(gè)進(jìn)程不應(yīng)該能訪問(wèn)到其他進(jìn)程的信息)。
接下來(lái)要?jiǎng)?chuàng)建并運(yùn)行 environment 。由于目前還沒(méi)有文件系統(tǒng),所以我們運(yùn)行的都是嵌在 kernel 中的 ELF 文件。
-
Exercise 2:
env_init():
初始化 env 結(jié)構(gòu)體鏈,全部初始化成空的。形成順序的鏈。env_setup_vm():
初始化進(jìn)程的頁(yè)表,并賦值給 env_pgdir。先申請(qǐng)一個(gè) Page 的空間。接下來(lái)由注釋知,需要講 p->pp_ref 加一,然后賦值 kern_pgdir 中 UTOP 以上的內(nèi)容到當(dāng)前的 env_pgdir。region_alloc():
為當(dāng)前的進(jìn)程申請(qǐng)一塊物理內(nèi)存空間(參數(shù) len 這么大),然后映射到參數(shù) va 指定的虛擬地址。按照注釋提示將 va 做 ROUNDDOWN,將 va + len 做 ROUNDUP,然后從 beg 到 end 申請(qǐng)個(gè) Page,如果申請(qǐng)失敗則 panic。對(duì)每塊申請(qǐng)到的 page,調(diào)用 lab2 中寫的page_insert函數(shù)形成虛實(shí)映射。-
load_icode():
這個(gè)函數(shù)將 ELF 文件分段分析并載入內(nèi)存,一一申請(qǐng)物理空間并形成虛實(shí)映射(使用前面寫的region_alloc函數(shù)) 。寫法可類比boot/main.c。需要注意的是,根據(jù)注釋提示:// 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?我們應(yīng)該把這個(gè) ELF 文件加載到用戶的地址空間里(目前只有一個(gè) user space),所以在 for 循環(huán)之前我們應(yīng)該切換
cr3到e->env_pgdir,在 for 循環(huán)結(jié)束后應(yīng)該切換cr3回到kern_pgdir。注意:這里不要忘記設(shè)置當(dāng)前 env 的 tf 的 eip 為當(dāng)前處理的 elf 文件的 entry point:
e->env_tf.tf_eip = elf->e_entry;另: 這個(gè)函數(shù)的實(shí)現(xiàn)里好像并沒(méi)有用到參數(shù)
size。 env_create():
這個(gè)函數(shù)將調(diào)用env_alloc(),申請(qǐng)一個(gè)新的 environment 。我們需要設(shè)置該 env 類型為傳入的類型并且調(diào)用load_icode()載入 ELF 文件。env_run():
這個(gè)函數(shù)將完成進(jìn)程切換。我們需要填入的是更改新舊進(jìn)程狀態(tài),切換 cr3 ,并且調(diào)用env_pop_tf來(lái)完成對(duì) trapfram 的保存。
-
Basics of Protected Control Transfer
接下來(lái)要做的是處理 interrupt 和 exception(統(tǒng)稱為 protected control transfer),其中 interrupt 為異步,exception 為同步。為了確保 protected control transfer 是安全的,在任一個(gè)進(jìn)程發(fā)生 interrupt 要進(jìn)入 kernel 的時(shí)候不是在任意地方以任意方式進(jìn)入,而是要通過(guò)統(tǒng)一的入口進(jìn)入。在 X86 架構(gòu)下,有兩個(gè)機(jī)制實(shí)現(xiàn)了這一點(diǎn):
- IDT(Interrupt Descriptor Table)
不同來(lái)源不同情況的中斷會(huì)帶有不同的interrupt vector([0,255] ,X86 最高支持 256 個(gè)),可以理解為不同種類的 interrupt 的 ID。CPU 在收到中斷后,以 interrupt vector 的值作為索引從 IDT 里去查找對(duì)應(yīng)的條目。找到對(duì)應(yīng)條目讀取:
1)對(duì)應(yīng)的中斷 handler 在內(nèi)核中的代碼位置,來(lái)裝入 EIP。
2)將要加載到 CS(代碼段)寄存器里的值。 - TSS(Task State Segment)
interrupt 發(fā)生時(shí),cpu 需要保存當(dāng)前馬上要被切走的進(jìn)程的信息,保存的位置應(yīng)該在內(nèi)核里,以免邪惡的程序改其他進(jìn)程的信息。
所以在 interrupt 發(fā)生時(shí),cpu 會(huì)將舊進(jìn)程的 SS, ESP, EFLAGS, CS, EIP, and an optional error code 等信息 push 到內(nèi)核某處的一個(gè)棧上。而 TSS 保存的信息就是如何定位這個(gè)棧。然后 cpu 才會(huì)去讀 IDT 改 EIP。
所有同步的 exception 的 vector 序號(hào)都在 [0,31] 之間;所有異步的 interrupt 的 vector序號(hào)都大于 31。
有的 interrupt 發(fā)生的時(shí)候還會(huì)往棧上 push 一個(gè)獨(dú)特的 error code,具體參考。
當(dāng)前在內(nèi)核態(tài)也可能觸發(fā) interrupt/exception,這時(shí)不會(huì)再換棧(因?yàn)橐呀?jīng)在內(nèi)核態(tài)了),直接把必要的信息 push 在當(dāng)前的棧上??汕短?,但嵌套能力有限。
- IDT(Interrupt Descriptor Table)
Exercise 4:
硬編碼編到頭皮發(fā)麻兩眼昏花-
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 需要不同的處理(主要是否返回原程序繼續(xù)執(zhí)行),然而對(duì)于一部分 interrupt ,cpu 是不會(huì) push error code 的,如果不分開多個(gè) handler 處理就無(wú)法清到底是哪種。
- 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)?
因?yàn)槿绻到y(tǒng)運(yùn)行在用戶態(tài),權(quán)限級(jí)別為 3,而 INT 指令是系統(tǒng)指令,權(quán)限級(jí)別為 0,因此會(huì)首先引發(fā) Gerneral Protection Excepetion(即 trap 13)。由
SETGATE函數(shù)定義上方注釋可知,通過(guò)改變參數(shù)dpl可以改變調(diào)用該 interrupt 需要的權(quán)限等級(jí)。通過(guò)把原來(lái) dpl = 0 的改成 dpl = 3,就可以讓用戶態(tài)程序也可以調(diào)用。
Part B: Page Faults, Breakpoints Exceptions, and System Calls
Handling Page Faults
在trap_dispatch函數(shù)中加一個(gè) switch 語(yǔ)句,檢查tf->tf_trapno是T_PGFLT的話就掉用page_fault_handler函數(shù)。-
Exercise 6:
這部分需要實(shí)現(xiàn) system call,代碼比較分散比較煩,具體要做的有:-
完成
kern/trapentry.S里的sysenter_handler,主要需要按順序把裝有參數(shù)的寄存器 push 到棧上并掉用 syscall:pushl %edi pushl %ebx pushl %ecx pushl %edx pushl %eax call syscall movl %ebp, %ecx movl %esi, %edx sysexit -
在
kern/syscall.c中實(shí)現(xiàn)不同編號(hào)的 syscall 的分發(fā):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函數(shù)中加上sysenter_handler的聲明和 MSR 的注冊(cè):/*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 */ -
最后實(shí)現(xiàn)
lib/syscall.c中的匯編代碼 syscall(通過(guò) push 和 pop 避免直接對(duì) 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:
這部分要實(shí)現(xiàn)擴(kuò)充堆容量的sys_sbrk函數(shù)。首先需要在 Env 結(jié)構(gòu)體加一個(gè)變量記錄堆頂位置:// LAB3: might need code here for implementation of sbrk uintptr_t env_heaptop;然后需要在
kern/env.c中的load_icode函數(shù)中對(duì)這個(gè)變量做初始化: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 文件的時(shí)候一旦發(fā)現(xiàn)超出 env_heaptop 的范圍立馬擴(kuò)充。
最后實(shí)現(xiàn)
kern/syscall.c里的sys_sbrk函數(shù):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:
這里要實(shí)現(xiàn)breakpoint exception的 dispatch 注冊(cè)和類似于 GDB 中的c,si,x指令。
首先,在trap_dispatch函數(shù)中加上:case T_BRKPT: monitor(tf); return;當(dāng)觸發(fā)
T_BRKPT這個(gè) trap 的時(shí)候,系統(tǒng)調(diào)用 monitor。(這里的 monitor 可以理解為一個(gè)類似于 GDB 的 Debugger)接下來(lái)要添加三個(gè)新指令,流程和之前 lab2 做 challenge 時(shí)候一樣,在
kenr/monitor.h中先聲明,再去kern/monitor.c中注冊(cè)。從維基查到 EFLAGS 中有一位 TF(Trap Flag)位專門控制 single step,那我們?cè)?
mon_c和mon_si函數(shù)中就要來(lái)回修改這一位, 并且在mon_si中按照例子中給出的格式打印信息。在
kern/kdebug.h這個(gè)文件中找到了我們需要的得到信息的數(shù)據(jù)結(jié)構(gòu)Eipdebuginfo,實(shí)現(xiàn)如下: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 代碼實(shí)現(xiàn),省去 16 進(jìn)制向 2 進(jìn)制轉(zhuǎn)換的麻煩:int mon_x(int argc, char **argv, struct Trapframe *tf){ if (argc != 2) { cprintf("Usage: <addr>\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; }實(shí)現(xiàn)了三個(gè)函數(shù)后發(fā)現(xiàn)沒(méi)法通過(guò) make grade 的測(cè)試,好在有之前同學(xué)踩坑的經(jīng)驗(yàn),想起來(lái)在
trap_dispatch里要把T_DEBUG也 dispatch 到 monitor:case T_DEBUG: case T_BRKPT: monitor(tf); return;改后通過(guò)測(cè)試。
-
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函數(shù)中,我們?cè)O(shè)置 GATE 的時(shí)候的最后一個(gè)參數(shù)決定了觸發(fā)這個(gè) trap 所需要的 privilege level (在inc/mmu.h的SETGATE宏的注釋里有寫)。如果這個(gè) trap 我們希望從 user mode 觸發(fā)(比如 break point exception),那就設(shè)置成 3,這樣就不會(huì)因?yàn)闄?quán)限不夠而先觸發(fā)了 general protection fault。- What do you think is the point of these mechanisms, particularly in light of what the user/softint test program does?
可能會(huì)對(duì) kernel 造成嚴(yán)重應(yīng)像的 trap 應(yīng)該嚴(yán)格限制權(quán)限,讓用戶態(tài)無(wú)法觸發(fā);不會(huì)對(duì) kernel 造成嚴(yán)重應(yīng)像且有必要讓用戶態(tài)觸發(fā)的 trap 應(yīng)該賦予用戶權(quán)限。
-
Exercise 10:
首先修改kern/trap.c中的page_fault_handler函數(shù)使得它能檢查出如果當(dāng)前 page fault 來(lái)自 kernel,就 panic:// LAB 3: Your code here. if(!(tf->tf_cs & 0x3)){ panic("page falut happens in kernel mode.\n"); }然后實(shí)現(xiàn)
kern/pmap.c中的user_mem_check函數(shù)。檢查用戶試圖訪問(wèn)的地址是否在 ULIM 之下且那個(gè) page 的權(quán)限可以讓用戶訪問(wèn)。需要注意的是:傳入的地址沒(méi)做對(duì)其,需要手動(dòng)檢查附近的每個(gè) Page。需要用到之前 lab 寫的
pgdir_walk函數(shù)。最后在
kern/syscall.c中的sys_cputs函數(shù)填入剛剛實(shí)現(xiàn)的函數(shù):user_mem_assert(curenv, (void*)s, len, PTE_U); Exercise 12:
這部分自己不是很理解,在看了網(wǎng)上的攻略之后總算磕磕絆絆地完成了。最終也只是似懂非懂的樣子。