JOS學習筆記(九)

LAB3代碼已經上傳。

最近忙於打WOWTCG,早就做完了一直沒騰出時間寫博客。

LAB3第二部分主要是處理系統調用。

第一部分我們已經讓第一個env運行了起來,接着這個env執行一個cprintf,這個cprintf是一個系統調用,因爲os暫時沒有實現系統調用,所以系統崩潰。

在lab2我們就要完成各種系統調用以及exception和trap等的實現。

handout地址:http://pdos.csail.mit.edu/6.828/2011/lec/x86_idt.pdf

博文中的很多說明都引用了handout裏面的圖。

一、原理

系統調用、中斷以及異常os的處理方式都一樣,他們的區別只在於陷入內核的方法不同。

interrupt和exception略有差異,但總體可以認爲是某些硬件機制導致我們陷入內核,而系統調用是我們在用戶態程序中執行int48進行調用,用戶態的調用工作在lib下的syscall.c中。之所以是int 48,是因爲T_SYSCALL常量定義爲48,理論上定義32--255之間的任何值都可以,0--31規系統使用。詳細信息可見handout。

下面詳細分析系統調用陷入內核的過程。

首先系統調用的入口在lib下的syscall.c,可以看到,函數syscall使用了嵌入式彙編,先將各個參數分別賦值給eax,ebx,ecx,edx,edi,esi,然後約定將返回值放入eax中(把返回值放入eax的過程是我們需要在內核中實現的),接着使用int 48陷入內核。

剩下的部分中斷、異常、陷阱、系統調用都一樣(雖然我也分不太清這幾個概念),因此下文中除非特殊說明,“中斷”一詞代表着中斷、異常、陷阱、系統調用這4個概念,但唯獨中斷號和系統調用號進行區分,中斷號是指idt表中的索引,系統調用號是指不同的系統調用函數的標識符。

int指令是一個較爲複雜的指令,其做了很多事情,按順序包括以下幾步:

1、查找idtr裏面的idt地址,根據這個地址找到idt,然後根據idt找到中斷向量的表項。

2、檢查cpl和表項的dpl,如果cpl>dpl產生保護異常,否則繼續

3、根據tssr寄存器裏的tss地址找到tss在內存中的位置,讀取其中的ss和esp並裝載(tss結構是x86定義好的,其內存中存放的位置需要os去決定,並對其中內容的賦值也要os實現,這部分內容在trap.c的trapinitpercpu函數中,同時還包括着加載idt的邏輯)。

4、如果是一個用戶態到內核態的陷入操作,則像堆棧中壓入ss和esp,注意這個ss和esp是之前用戶態的數據,而不是新裝載的數據

5、壓入esp,eflags,eip

6、修改eflags中的某些位(比如關中斷)

7、如果有必要,壓入errorcode,某些中斷需要errorcode以及不同中斷的errorcode含義可查看handout。

8、根據idt表項設置cs和eip,也就是跳轉到處理函數執行。idt內容相關可查看handout。

壓入後的堆棧就應該是這個樣子的,跳轉到相應中斷處理函數的時候我們面對的就是這樣一個堆棧。接着中斷處理函數處理相應操作,然後根據目前堆棧裏有的內容和當前寄存器的內容恢復現場,繼續程序執行。

 

 

二、實驗

大概弄懂了原理,接下來解析具體的實驗。

1、完成trapentry.S和trap.c的部分內容,使之能正確的調用trap函數,並將一個正確的trapframe結構指針當做函數的參數

在這個實驗時,還需要完成很多操作才能進行系統調用。因爲加載idt的工作JOS已經幫我們做了,我們需要做的就是初始化idt,給不同的中斷分配不同的處理函數。

簡單分析JOS的邏輯可知,JOS是先將所有中斷都跳轉到trap函數,再在trap函數裏調用trap_dispatch來進行分發,再在trap_dispatch中調用具體的處理函數,雖然這個過程複雜繁瑣且個人認爲完全沒有必要,但至少這意味着idt裏指向的函數只需要調用trap函數就可以了。

在trapentry.S中,根據定義的兩個宏,參考handout裏面errorcode所對應的中斷號,可以爲不同的中斷號定義處理函數,名字隨便取,然後再在trap.c中聲明這些函數,並獲取函數地址填充進idt中(使用setgate宏,使用GD_KT段,也就是OS的代碼段),DPL我參考了linux一些信息,將breakpoint,overflow,以及system call設置爲3,其餘都是0。

此時idt初始化代碼就完成了,發現所有中斷處理函數跳轉到_alltrap處,在trapentry.S中定義_alltrap符號,接着往堆棧裏壓入某些值,使堆棧看起來像是一個trapframe,此時堆棧棧頂的指針就是指向這個trapframe的指針(結構體內存從低向高增長),將這個指針也就是esp壓入堆棧,調用trap函數,進入函數時,會取棧頂元素當做參數,正好就是這個trapframe,這樣就完成了我們想要的功能。

接下來考慮如何壓棧才能讓堆棧看起來像trapframe。

tramframe看起來應該是這個樣子的:

在每一箇中斷處理函數裏,已經壓入了trapno,所以在_alltrap處,還需要壓入trapno以下的所有內容。接着根據要求加載內核數據段,最後再壓入esp,執行call trap即可轉入c執行。至此exercise 4完成。

 

2、完成部分trap_dispatch邏輯

這部分沒啥可說的,根據中斷號把page fault分發到trap.c裏的trap_fault_handler函數,然後發現函數裏把這個唯一的env給毀掉了,也就是說用戶程序出了page fault直接銷燬,大概是這麼個邏輯。這樣exercise 5就完成了。

 

3、完成breakpoint的中斷處理

自己建個函數,然後在trap_dispatch里根據中斷號跳轉到此函數,在此函數中調用monitor(tf)即可,如果想讓到達斷點的程序繼續執行,可在monitor中多加個命令,退出monitor中的那個死循環,然後trap函數就會根據trapframe裏的內容恢復現場繼續執行下去。

編程5年總算是知道這個斷點是怎麼實現的了,囧。

 

4、完成內核區的系統調用

根據trapframe內的系統調用號(放在eax裏)完成相應的系統調用,返回結果要放在eax裏,這樣經過iret返回時才能得到正確的結果。

 

5、完成libmain

在libmain用剛纔寫好的系統調用取得該env的envid,然後根據envid得到Env結構(使用ENVX宏獲取UENVS數組的索引),簡單的很,也沒啥可說的。

 

6、如果在內核態發生page fault,則panic

在page_fault_handler中判斷是否在內核態產生page_fault,判斷的方法是查看傳入trapframe的cs中的DPL,如果是0,即爲內核態。

 

7、系統調用中的參數檢查

在系統調用中有一個是向控制檯輸出信息的函數sys_cputs,在此函數中要去檢查用戶傳入的字符串所處的內存是否有映射以及是否有read權限,方法也很簡單,遍歷page table,查看其頁表項的PTE_P和PTE_U位即可,如果通不過檢測,就把這個envpanic掉。

 

好了,到此爲止應該能通過絕大多數testcase,只有一個testbss除外。

bss裏存放着未初始化的變量,和data段不同的是,data段存放變量及其值,而bss段只存放變量及其所佔空間大小的信息,所以bss段在elf中所佔用的空間要小的多。

現在的問題是,在load_icode時,我們並沒有加載bss段。通過objdump來看,按JOS的邏輯,讓我們在program header表中只加載ELF_PROG_LOAD類型的段是不會加載bss段的,bss段存放於elf的section表中,且其類型的id是8,因此我們必須寫點代碼把這個bss段加入內存纔行。

但問題是實驗中並沒有給出這些內容相關的提示,或許是我閱讀的材料不夠(我沒閱讀所有相關材料),也許是實驗的設計者壓根就忘了,反正加載bss段之後就能通過測試了。

代碼以上傳,因版面問題,就不貼在這裏了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章