有4個層次的特權級,從高到低依次是:0級、1級、2級、3級。切換特權級是指從0級轉移到1級、或從1級轉移到3級。總之,是指從一個特權級轉移到了另外一個不同的特權級。
學習特權級切換,關鍵知識點是:
- 兩個指令
call
和iret
。 - 一個CPU特性:特權級變換時會將一個特權級的堆棧復制到另外一個特權級堆棧。
從低到高
只有使用調用門才能從低特權級轉移到高特權級,更具體地說,是使用語句call 門選擇子
。
門描述符
門選擇子的結構和段選擇子一致,只不過它指向的是門描述符而不是段描述符。
門描述符和段描述符占用的內存空間相同,都是8個字節,64個bit。可后者包含的元素是:
- 目標代碼段選擇子。
- 在目標代碼段中的偏移量。
- ParamCount。這是什么?后面再解釋。
- 門描述符的屬性。
代碼
使用調用門從低特權級轉移到高特權級的代碼是:
push ax
push bx
;SelectorGate 指向一個高特權級的目標代碼段
call SelectorGate:0
ParamCount
入棧了2個元素,2
就是上文提到的ParamCount
。可以把它理解成函數的參數。在門描述符中,ParamCount
占用5個bit位,能表示的最大值是2的5次方-1,即31。這意味着,使用一個調用門,最多能入棧31個元素。
堆棧復制
假定,上面的代碼的能成功從低特權級代碼段L轉移到高特權級代碼段H。L和H是不同的代碼段,各自的堆棧也不同。這不是必須的。L和H共用一個堆棧,也不是不行。這樣的話,在H中操作堆棧可能會破壞L中的堆棧(總之,存在這種可能)。所以,代碼段擁有獨立的堆棧更好。
那么,問題來了。入棧操作發生在L中,被入棧元素存在於L的堆棧LS,在H中怎么從LS中獲取數據呢?也許存在方法,但一定很繁瑣。現實中,CPU會自動把LS中的元素復制到H的堆棧中。並非復制LS中的全部元素,而是從LS的棧頂開始,復制ParamCount
個元素。這個復制操作發生在call
執行的時候。
示意圖
短調用是段內部的調用,長調用是段之間的調用。長調用才能從低特權級轉移到高特權級。
短調用和長調用示意圖之間的差別,僅僅在於后者把cs
入棧了。cs
是調用者的代碼段選擇子。
示意圖中的堆棧,是H的堆棧HS。
短調用
call執行前----> | 高地址 | |
---|---|---|
ss | ||
esp | <-----------堆棧 | |
參數二(ax) | ||
參數一(bx) | ||
call執行后----> | eip | |
長調用
call執行前----> | 高地址 | |
---|---|---|
ss | ||
esp | <-----------堆棧 | |
參數二(ax) | ||
參數一(bx) | ||
cs | ||
call執行后----> | eip | |
CPU工作流程--調用門
調用門的運行過程,涉及LS和HS兩個堆棧。上面的示意圖只畫出了HS的狀態,不足以闡述調用門的整個流程,本小節再用文字詳細說明調用門的運行過程。
TSS
先介紹一個新東西,TSS,任務狀態寄存器,一個寄存器。CPU在切換任務的時候,能用TSS給切換下來的任務建立一個快照,這個快照包含一個任務的所有寄存器數據。不過,大部分操作系統嫌棄這種切換方式太消耗時間,並沒有完全按照CPU廠商的意圖使用TSS。Linux系統也是如此。
在TSS中,包含ss0、esp0
、ss1、esp1
、ss2、esp2
三組數據,正好對應0特權級、1特權級、2特權級三個層次的特權級。
特權級不是有四個層次嗎?為什么沒有對應3特權級的那一組數據呢?TSS的作用是為低特權級向高特權級轉移時提供高特權級的堆棧。3特權級是最低特權級,沒有更低的特權級向它轉移。
假如,從3特權級向0特權級轉移,CPU會從TSS中選擇ss0、esp0
作為0特權級代碼段的堆棧。
流程
使用call SelectorGate:0
實現低特權級向高特權級轉移的流程如下(不敘述CPL等特權級檢查流程,假設滿足這些條件):
- 執行
call
語句時。 - 把當前代碼段的堆棧LS的
ss_old、esp_old
臨時保存起來。 - 從門選擇子指向的目標代碼段中獲取DPL,根據DPL的值,在TSS中選擇
ss、esp
,將堆棧指向的新堆棧。例如,DPL的值是0,選擇ss0、esp0
。 - 把
ss_old、esp_old
中的入棧到新堆棧中。 - 把LS中的棧元素復制到新堆棧中。復制規則是:從LS的棧頂開始,復制
ParamCount
個元素。 - 依次入棧
cs、eip
。
小結
從低特權級轉移到高特權級的方法是,使用調用門,具體語句是call SelectorGate:0
。
不能使用jmp
。jmp
只能在實現短調用,在同一個特權級轉移。因為jmp
不會將下一條指令的地址存儲到新特權級的堆棧中,這意味着,jmp
是一個有去無回的指令。從低特權級轉移到高特權級的場景,一般是用戶進程求助操作系統完成某種功能,需要再次返回用戶進程。
從高到低
電腦開機后,CPU的特權級是0,這是從BIOS那里寄存下來的。這種知識似乎無用,懶得多說。
前文講了從低特權級切換到高特權級的方法,可CPU從開始工作的那一刻起,一直都是在0特權級。這樣說來,如果要動手實現從低特權級轉移到高特權級,應該先實現從高特權級轉移到高特權級。
怎么實現?
前文已經埋下了伏筆,call
指令會將調用者(低特權級)的cs(選擇子)
和eip(偏移量)
入棧到被調用者(高特權級)的堆棧中。從堆棧中獲取調用者(低特權級)的cs(選擇子)
和eip(偏移量)
,就能從高特權級轉移到低特權級。完成這項工作,只需一個指令而已,iretf
。
代碼
;特權級是3
push ax
push bx
call SelectorGate:0
mov ax, 5
call SelectorGate:0
調用下面的代碼。
;特權級是0
;調用門選擇子指向的目標代碼段,高特權級代碼段
;一些操作,示范,不必理會具體功能
mov al, 'A'
mov ah, 0Fh
mov [gs:(80*20+20)*2], ax
iretf
iretf
執行后,CPU會繼續執行mov ax, 5
。mov ax, 5
就是被調用者堆棧中的cs:eip
指向的指令。
示意圖
還是畫兩個和上面call
指令類似的堆棧圖吧。
短調用返回
ret執行后 | 高地址 |
---|---|
調用者ss | |
調用者esp | |
參數一 | |
參數二 | |
ret執行前 | 調用者eip |
長調用返回
retf執行后 | 高地址 |
---|---|
調用者ss | |
調用者esp | |
參數一 | |
參數二 | |
調用者cs | |
retf執行前 | 調用者eip |
工作流程--iretf
長調用返回使用iretf
指令將上面的堆棧S中的元素出棧。具體流程如下:
- 從S中獲取調用者cs、調用eip,並加載到當前cs、eip中。
- 若
iretf
含有參數,esp增加參數個數跳過這些參數。 - 繼續出棧,把調用者esp、調用者ss加載到當前
esp
、ss
中。此時會切換到調用者堆棧。 - 若
iretf
含有參數,增加esp的值以跳過參數(在call
前,參數也壓入了調用者堆棧中)。 - 最后執行
cs:eip
代碼。
iretf
- RET:可能是近返回,也可能是遠返回。
- RETN:近返回指令。
- RETF:遠返回指令。
- RET6:子程序返回后,(SP)←(SP) + 6。
沒有找到iretf
的權威資料,上面的資料也沒有驗證。先擱置吧。
疑問
不使用調用門能不能轉移特權級?
使用jmp
只能在同特權級跳轉。
一致代碼段,call
只能轉移到比當前特權級高或相等的特權級。
非一致代碼段,call
只能在相同特權級跳轉。
什么時候檢查特權級
沒有弄明白。好像不重要。