現在,JOS具有了基本的例外處理功能,我們將對其進行優化,以提供用於處理異常或中斷的重要操作系統原語。
頁面錯誤異常(T_PGFLT)是一個特別重要的異常,當處理器發生頁面故障時,引起故障的線性(即虛擬)地址會被儲存在特殊的處理器控制暫存器CR2中。在trap.c中,我們提供了一個函數page_fault_handler()(僅編寫了開頭的程式碼),用於處理頁面錯誤異常。
練習5:修改trap_dispatch()來將頁面錯誤異常排程到page_fault_handler()。修改後,makegrade測試一下,看看是否通過了faultread, faultreadkernel, faultwrite, and faultwritekernel的檢查。如果其中任何一個不起作用,找出原因並修復。我們可以使用make run-x或make run-x-nox將JOS引導到特定的使用者程式中。 例如,make run-hello-nox執行hello使用者程式。
斷點異常(T_BRKPT)通常用於允許偵錯程式通過用特殊的int 3軟體中斷指令臨時替換相關的程式指令,從而在程式程式碼中插入斷點。
練習6:修改trap_dispatch()使斷點異常能夠呼叫內核監視器。要求:通過breakpoint的測試。
上面兩個練習都比較簡單,程式碼如下:
成功通過測試:
練習6還有個挑戰,要求修改jos的內核監視器程式碼來實現單步偵錯功能,嘗試寫了一下,還是有點問題,這
裡先留個坑,相關參考資料:x86—EFLAGS暫存器詳解。
問題3:斷點測試用例將生成斷點異常或常規保護錯誤,具體生成哪個錯誤取決於您如何初始化IDT中的
中斷描述符(就是trap_init呼叫SETGATE那一段程式碼),這是爲什麼? 爲了使斷點異常按上述規定工作,您需要如何對其進行設定?什麼不正確的設定將導致它觸發常規保護故障?
答:在trap_init中,我們會呼叫SETGATE函數來設定各個中斷描述符,而設定的DPL位爲描述符特權等級(
即存取該描述符對應的中斷處理程式應有的特權等級),當我們將斷點異常對應的DPL位設定爲0時,使用者
程式由於特權等級低,存取斷點例外處理程式則會產生保護錯誤,而DPL設定爲3時,會正確生成斷點異常。
問題4:尤其考慮到user/softint測試程式,你認爲這些機制 機製的要點是什麼?
答:通過設定DPL來將程式碼分級,實現一個隔離保護的作用,參考資料:詳解 RPL、DPL、CPL 的關係。
使用者進程通過系統呼叫請求內核服務,當使用者進程使用系統呼叫時,處理器進入內核模式,處理器和內核
會共同儲存使用者進程的狀態,然後會執行內核程式碼來爲使用者進程提供服務,執行完後,恢復環境,返回到用
戶進程,使用者進程請求系統呼叫的方法和細節因系統而異。
在JOS內核中,我們將使用Int指令,該指令會產生一個處理器中斷,特別地,我們將用int $30作爲系統調
用中斷,inc/trap.h中已經定義了T_SYSCALL,我們現在要設定該中斷描述符以允許使用者進程產生該中斷,
注意:中斷0x30不能由硬體產生,因此不會因爲允許使用者程式碼產生中斷而有歧義。
應用程式使用暫存器來傳遞系統呼叫號和系統呼叫參數,因此內核無需搜尋使用者進程的堆疊或指令流.系統呼叫號儲存在eax中,參數(最多五個)將分別儲存在edx,ecx,ebx,edi,esi中,系統呼叫完成後,其返回
值也會儲存在eax中,請求系統呼叫的彙編程式碼已經在lib/syscall.c中的syscall()函數中編寫好,閱讀
並理解這段原始碼。
練習7:
爲中斷向量T_SYSCALL在內核中新增一箇中斷處理常式,需要編輯 kern/trapentry.S及kern/trap.c中的
trap_init(),還需要更改trap_dispatch()來處理系統呼叫中斷,方法是用適當的參數呼叫syscall()(在kern/syscall.c中定義),然後在%eax中將返回值傳遞迴使用者進程。最後,需要在kern/syscall.c中實現syscall()。閱讀並理解lib/syscall.c(尤其是內聯程式集例程),通過爲每個請求呼叫相應的內核函數來處理inc/syscall.h中列出的所有系統呼叫。
要求:
在內核下執行user/hello程式(make run-hello)。它應該在控制檯上列印「hello world」,然後在使用者模式下導致頁面錯誤。如果這種情況沒有發生,可能意味着編寫的系統呼叫處理程式不太正確。make grade也要通過testbss的測試.
lib/syscall.c解析:
有了PartA在JOS中設定IDT,中斷入口及前面做的在XV6中新增系統呼叫的HomeWork的積累,這個練習做起來就很簡單了。貼上修改後的kern/syscall.c和kern/trap.c的核心程式碼:
trap.c/trap_dispatch():
syscall.c/syscall():
測試通過:
最後以sys_cputs爲例走一遍使用者程式請求系統呼叫的流程:
lib/syscall.c/sys_cputs(s,len)-> lib/syscall.c/syscall(SYS_cputs, 0, (uint32_t)s, len, 0,
0, 0)->觸發48號中斷->kern/trap.c/trap(tf)->kern/trap.c/trap_dispatch(tf)的T_SYSCALL分支->
kern/syscall.c/syscall(eax,edx,ecx,ebx,edi,esi)的SYS_cputs分支->kern/syscall.c/sys_cputs
((char)(a2),a3)*;
一個使用者程式從lib/entry.S的頂端開始執行,在做完一些設定之後,會呼叫lib/libmain.c中的libmain函數,修改libmain()來初始化全域性指針thisenv,使其指向當前進程的env結構體,提示: 在inc/env.h中查詢並使用sys_getenvid,然後libmain()呼叫umain,在hello程式的情況下,它位於user/hello.c中。注意,在列印「hello,world」之後,它會嘗試存取thisenv->env_id。這就是出現頁面故障的原因。如果正確地初始化了這個環境,那麼應該不會出錯。
閱讀inc/env.h的原始碼和註釋:
可以知道envid分爲三部分,最高位爲0,低十位爲其在envs陣列中的下標,中間21位用來唯一標誌當前進
程,宏函數ENVX通過保留envid的低十位來獲得其在envs陣列中的下標。那麼要初始化全域性指針thisenv。
新增如下程式碼即可:
測試通過:
記憶體保護是操作系統的一個重要功能,它能保證發生BUG的程式不會影響到其他程式和操作系統。操作系統通常依賴硬體的支援來實現記憶體保護,操作系統使硬體知道哪些虛擬地址有效,哪些無效。 當程式嘗試存取無效地址或沒有許可權的地址時,處理器會在導致錯誤的指令處停止程式,然後使用有關資訊陷入內核。如果該錯誤可修復,則內核會對其進行修復,並讓程式繼續執行。 如果故障無法修復,則程式將無法執行下去。可修復故障的範例典型是自動擴充套件堆疊。 在許多系統中,內核最初分配一個堆疊頁面,如果程式在存取堆疊中更遠的頁面時出錯,則內核將自動分配這些頁面並讓程式繼續。 這樣,內核僅分配程式所需的堆疊記憶體,但從程式角度看,它擁有任意大的堆疊。
系統呼叫引出了一個有趣的記憶體保護問題。 大多數系統呼叫介面都允許使用者程式將指針傳遞給內核
,這些指針指向要讀取或寫入的使用者緩衝區。 然後,內核在執行系統呼叫時解除參照這些指針。 這有兩個問題:
1.內核中的頁面錯誤可能比使用者程式中的頁面錯誤嚴重得多。如果內核在處理自己的數據結構時出現頁面錯誤,那就是內核錯誤,錯誤處理程式會使內核(以及整個系統)宕機。因此,當內核解除參照使用者程式傳遞過來的指針時,它需要記住這個錯誤屬於使用者程式。是來自使用者進程。
2.內核通常比使用者程式具有更多的記憶體許可權。 使用者程式可能會傳遞一個指向系統呼叫的指針,該指針指向內核,可以讀取或寫入使用者程式無法讀取的記憶體。 內核需要小心不被欺騙去解除參照這樣的指針,因爲這可能會泄露私有資訊或破壞內核的完整性。
由於這些原因,內核在處理使用者程式提供的指針時必須格外小心。
現在,我們將通過一種機制 機製來仔細檢查這兩個問題,該機制 機製將仔細檢查從使用者空間傳遞到內核的所有指針。 當程式將指針傳遞給內核時,內核將檢查該地址是否位於地址空間的使用者部分中,以及頁表是否允許進行記憶體操作。因此,內核將永遠不會由於解除參照使用者程式提供的指針而導致頁面錯誤,如果內核出現頁面錯誤,則它應該崩潰並終止。
練習9:
修改trap.c,如果是在內核態下發生頁面錯誤,應該呼叫panic,閱讀kern/pmap.c中的user_mem_assert並在該檔案中實現user_mem_check函數,修改一下 kern/syscall.c 去檢查系統呼叫的輸入參數。啓動內核後,執行 user/buggyhello 程式,使用者環境應該被銷燬,內核不會panic,應該會列印出如下資訊:
user_mem_check函數(根據註釋中的提示實現即可):
修改syscall.c的sys_cputs函數:
執行buggyhello結果:
最後,修改kern/kdebug.c檔案中的debuginfo_eip函數呼叫user_mem_check檢查usd,stabs,stabstr,修
改後執行user/breakpoint,應該能夠從kernel monitor執行backtrace,並在內核因頁面錯誤而宕機之前看到回溯遍歷到lib/libmain.c,爲什麼發生了頁面錯誤呢?
答:在kern/monitor.c的mon_backtrace函數中呼叫了debuginfo_eip函數,在執行user_mem_check發生了頁面錯誤,由於backtrace本身位於內核中,故在內核態下發生頁面錯誤導致了內核宕機。
練習10: 啓動你的內核,執行user/evihello.這個環境應該會被銷燬,而內核不會panic。
你會見到:
user/evihello執行結果: