關於ucore lab1 challenge1 的思考
這個擴充套件練習是要求設定兩個中斷處理程式。一個是可以實現從核心態轉換為使用者態的程式。另一個是實現從使用者態轉換到核心態的程式。其實,這個練習,我們只要弄明白int指令和iret指令到底做了什麼,以及cpu是如何表示特權級狀態的就可以做了。
int 指令進行下面一些步驟:(這些步驟來自xv6中文文件)
1. 從 IDT 中獲得第 n 個描述符,n 就是 int 的引數。
2.檢查 %cs 的域 CPL <= DPL,DPL 是描述符中記錄的特權級。
3. 如果目標段選擇符的 PL < CPL,就在 CPU 內部的暫存器中儲存 %esp 和 %ss 的值。(這裡的目標段選擇符我不太清楚是什麼,我覺得可能是相應的GDT表項裡特區級或者就是這個中斷描述符裡的CS所記錄的特權級)
4.從一個任務段描述符中載入 %ss 和 %esp。(這裡的任務段就是TSS,一般設定為每個CPU一個,且在固定的段表項)
5.將 %ss 壓棧。
6.將 %esp 壓棧。
7.將 %eflags 壓棧。
8.將 %cs 壓棧。
9.將 %eip 壓棧。
10.清除 %eflags 的一些位。
11.設定 %cs 和 %eip 為描述符中的值。
從上面的內容可以看出,int這短短的一條指令其實做了非常多的事(這可能表明實現這條指令的電路非常複雜吧)。不過,如果步驟3不為真的話,3,4,5,6這幾個步驟都是不會執行的。因此,對於沒有發生特權級轉換的中斷,其實是沒有棧切換的。一直都是用那個棧在處理中斷。
至於iret指令的動作也是類似的,因為int指令和iret指令是一對的,就像call指令與ret指令。其指令的步驟如下:
1.將 %eip 彈棧。
2.將 %cs 彈棧。
3.將 %eflags 彈棧。4.將 %esp 彈棧。
5.將 %ss 彈棧。
從上面的內容可以看出,iret其實就是int的逆過程。相應的,如果彈棧過程中,發現棧中%cs和當前%cs的特權級是一樣的,那麼步驟4,5是不會發生的。因為cpu會認為int指令並沒有把這兩個內容壓棧。
至於cpu如何表示特權級:就是通過%cs暫存器的最低兩位來表示,表示為數字即0~3,其中數字越小特權級越高。一般實現只用兩個狀態,核心態為0,使用者態為3。
好了,有了這些知識儲備,這個練習就可以做了。
因為,核心初始化完之後,是處於核心態的。要想通過中斷來實現轉換到使用者態,就必須在一箇中斷裡把當前中斷的trapframe裡的各種段暫存器強行改為使用者態的內容,然後通過iret指令彈棧就實現了特權級的轉換了。不過,根據上面的知識,這裡有點坑人的地方就是:因為這個中斷是在核心態陷入的,所以int指令並沒有將%ss和%esp壓棧!!!但是,我們在這個中斷例程裡卻要修改它的值(%ss),所以,如果我們在int指令呼叫前,不做點處理的話,就是寫入到錯誤的位置,從而破壞了棧結構。因此,解決辦法就是:int指令沒有為我們提供足夠的位置,那我們就自己提供。於是,在int指令前,我們將%esp的值減8就為%esp和%ss預留了空間了。這裡還有一個小細節就是,使用者態返回後要進行cprintf呼叫,這個函式使用了in和out指令。但是如果不設定好eflags的值的話,在使用者態執行這兩條指令是會產生13號中斷錯誤的。於是,要在系統呼叫裡修改以下trapframe的eflags的值。最後,中斷返回後,通過彙編指令,把ebp的值傳給esp就使棧頂位置正確歸位,於是就完成了。
至於從使用者態轉為核心態也是大同小異。坑人的地方就是,這次int指令的確把%ss和%esp壓棧了,但是這回是iret指令沒有把他們彈出,因為我們強行改為核心態,會讓cpu認為沒有發生特權級轉換。於是,%esp的值就不對了(%ss的值是對的)。但是,解決辦法很簡單,因為%ebp的值是正確的,而執行int指令前,%ebp和%esp的值是一樣的。因為,只要把%ebp的值傳給%esp就會使棧頂位置正確歸位,於是就順利完成了呼叫。