1. 程式人生 > >有特權級變換的轉移——堆疊的切換

有特權級變換的轉移——堆疊的切換

在沒有特權級變換的情況下,程式的轉移中的一些引數和返回地址都是push進同一個堆疊,這種情況比較簡單。而如果轉移伴隨著特權級變換,那麼我們就會涉及到兩個堆疊,外層堆疊(呼叫者堆疊)和內層堆疊(被呼叫者堆疊)。(特權級變化的時候,堆疊也要發生變化,這個是處理器的機制,其作用是為了避免高特權級的過程由於棧空間不足而崩潰。)

    既然涉及到兩個堆疊,那麼我們從哪裡取得其餘堆疊的ss和esp呢?那就得使用到TSS(Task-State Stack)。TSS裡面包含多個欄位,現在只關心偏移4到偏移27的3個ss和3個esp。我們有四個特權級ring0,ring1,ring2 & ring3,為什麼只有ring0,ring1,ring2 的ss 和 esp 呢?原來只有當轉移時從外層到內層時(低特權級到高特權級

),新的堆疊ss & esp才會從TSS中取得,而特權級從高到低轉移時,新堆疊的ss & esp 則通過其他方式獲取。

    好,新堆疊問題解決後,我們看下轉移過程。下面是CPU在整個過程中所做的工作:

    1. 根據目的碼段的DPL(新的CPL)從TSS中選擇切換到的對應的ss和esp。

    2. 從TSS中讀取新的ss和esp。在這個過程中如果發現ss、esp或者TSS界限錯誤都會導致無效TSS異常(#TS)。

    3. 對ss描述符進行檢驗,如果發生錯誤,同樣產生錯誤,同樣產生#TS異常。

    4. 暫時性地儲存當前ss和esp的值。

    5. 載入新的ss和esp。

    6. 將剛剛儲存起來的ss和esp的值壓入新棧。

    7. 從呼叫者堆疊中將引數複製到被呼叫者堆疊(新堆疊)中,複製引數的數目有呼叫門中Param Count一項來決定。如果Param Count是零的話,將不會複製引數。

    8. 將當前的cs和eip壓棧。

    9. 載入呼叫門中指定的新的cs和eip,開始執行被呼叫者過程。

 

    那麼,正如call指令對應ret,呼叫門也面臨返回的問題。ret基本上就是call的反過程,只是帶引數的ret指令會同時釋放事先被壓棧的引數。由被呼叫者到呼叫者的返回過程中,處理器的工作包括一下步驟:

    1. 檢查儲存的cs中的RPL以判斷返回時是否要變換特權級。

    2. 載入被呼叫者堆疊上的cs和eip(此時會進行程式碼段描述符和選擇子型別和特權級檢查)。

    3. 如果ret指令含有引數,則增加esp的值以跳過引數,然後esp指向被儲存過的呼叫者ss和esp。注意,ret的引數必須對應呼叫門中的Param Count的值。

    4. 載入ss和esp,切換到呼叫者堆疊,被呼叫者的ss和esp被丟棄。在這裡將會進行ss描述符、esp以及ss段描述符的檢查。

    5. 如果ret指令含有引數,增加esp的值以跳過引數(此時已經在呼叫者堆疊中)。

    6. 檢查ds、es、fs、gs的值,如果其中哪一個暫存器指向的段的DPL小於CPL(此規則不適用於一致程式碼段),那麼一個空描述符會被載入到該暫存器。