本講咱們不繼續寫任何代碼,而是專門拿出一講來講說特權級的事,爲後續的工做作一個知識儲備。這段內容太難啃了,也可能我剛好對這塊不太感冒,反正我是噁心了很久才啃下來。html
爲了讓你們清楚目前的程序進度,畫了到目前爲止的程序流程圖,以下linux
爲何要進行特權級檢查,我就不說太多了,簡單理解,操做系統不但願用戶進程訪問內核數據,因此須要給指令呀還有數據呀都附上一個特權級的屬性,讓程序受限制。git
特權級分爲 0 1 2 3 四種,咱們常說的 用戶態 就是最低等級的 3 特權級,內核態 就是最高等級的 0 特權級。安全
處理器在 訪問數據 或 跳轉到代碼 時,須要進行特權級檢查,特權級檢查的具體細則在下面有具體描述。這裏我先把大體的思想方向總結出來:學習
檢查時機:特權級檢查會發生在往 數據段寄存器 中加載 段選擇子 的時候,數據段寄存器包括 DS 和附加段寄存器 ES、FS、GS,如操作系統
mov ds,ax指針
檢查條件:CPL <= 目標數據段DPL && RPL <= 目標數據段DPL (只能高特權級的指令訪問地特權級的數據)htm
檢查時機:特權級檢查會發生在可以 改變 代碼 段寄存器 CS 和 指令指針寄存器 EIP 的指令中,即這些指令要麼改變 EIP,要麼改變 CS 和 EIP。例如 call、jmp、int、ret、sysexit 等能改變程序執行流的指令,如blog
call 內核選擇子索引
總結成最精煉的一句話就是:數據只能高的訪問低的,代碼只能從低的跳到高的(門或一致),從高到低只有返回指令能夠完成
門描述符一共有四種,分別是
這些描述符也是記錄在 描述符表 中的,與以前說的 段描述符 同樣。因此這裏把以前的段描述符,以及今天要說的四種門描述符,都畫在下面的圖中
門 | type值 | 存在位置 | 用法 |
---|---|---|---|
任務門 | 0101 | GDT、LDT、IDT | 與TSS配合實現任務切換,不過大多數操做系統都不這麼玩 |
中斷門 | 1110 | IDT | 進入中斷後屏蔽中斷(eflags的IF位置0),linux利用此實現系統調用,int 0x80 |
陷阱門 | 1111 | IDT | 進入中斷後不屏蔽中斷 |
調用門 | 1100 | GDT、LDT | 用戶用call或jmp指令從用戶進程進入0特權級 |
門描述符的訪問流程是相似的,這裏咱們用 調用門 來舉例。
沒有門描述符的時候,咱們用 jmp 指令指向一個普通的段描述符,通過一次拼接(段基址 + 偏移地址)就獲得了邏輯地址。
調用門是用 jmp 或者 call 指令跳轉過去的,當指向一個調用門時,無非就是多一次拼接而已,最開始的 選擇子:偏移地址 中的 選擇子 用來定位一個門描述符,偏移地址 則被忽略了,以下圖。
直觀地說就是:當前特權級必須比門特權級高,又必須比最終要跳到的代碼段的特權級低
下面用一個調用門的具體例子梳理一下整個過程,因爲書中的描述太精彩了,我看完以後對整個流程的理解又有了一大飛躍,因此我原封不動粘貼過來:
假設當前處理器正在 DPL 爲 3 的代碼段上運行,即正在運行用戶程序,故處理器當前特權級 CPL 爲 3。此時用戶進程想獲取安裝的物理內存大小,該數據存儲在操做系統的數據段中,該段 DPL 爲 0。因爲當前運行的是用戶程序,CPL 爲 3,因此沒法訪問 DPL 爲 0 的數據段。因而它使用調用門向系統救助。調用門是操做系統安裝在全局描述符表 GDT 中的,爲了讓用戶進程可使用此調用門,操做系統將該調用門描述符的 DPL 設爲 3。該調用門只須要一個參數,就是用戶程序用於存儲系統內存容量的緩衝區所在數據段的選擇子和偏移地址。調用門描述符中記錄的就是內核服務程序所在代碼段的選擇子及在代碼段內的偏移量。用戶進程用「call 調用門選擇子」的方式使用調用門,此調用門選擇子是由操做系統提供的,該選擇子的 RPL 爲 3,此時若是用戶僞造一個調用門選擇子也沒用,由於此選擇子是用來索引門描述符的,並不用來指向緩衝區的選擇子,調用門選擇子中的高 13 位索引值必需要指向門描述符在 GDT 中的位置,選擇子中低 2 位的 RPL 僞造也沒意義,由於此時 CPL 爲 3,是短板,以它爲主。此時處理器便進行特權級檢查,CPL 爲 3,RPL 爲 3,門描述符 DPL 爲 3,即數值上(CPL≤DPL && RPL≤DPL)成立,初步檢查經過。接下來還要再將 CPL 與門描述符中選擇子所對應的代碼段描述符 DPL 比較,這是調用門對應的內核服務程序的 DPL,爲敘述方便將其記做 DPL_CODE。因爲 DPL_CODE 是內核程序的特權級,因此DPL_CODE 爲 0,CPL 爲 3,即數值上知足 CPL≥DPL_CODE,CPL 比目標特權級低,檢查經過,該用戶程序能夠用調用門,因而處理器的當前特權級 CPL 的值用 DPL_CODE 代替,記錄在 CS.RPL 中,此時CPL 變爲 0。接下來,處理器便以 0 特權級的身份開始執行該內核服務程序,因爲該服務程序的參數是用戶提交的緩衝區所在的數據段的選擇子及偏移量,爲避免用戶將緩衝區指向了內核的數據區,安全起見,在該內核服務程序中,操做系統將這個用戶所提交的選擇子的 RPL 變動爲用戶進程的 CPL,也就是指向緩衝區所在段的選擇子的 RPL 變成了 3。前面說過,參數都是內核在 0 級棧中得到的,雖然用戶進程將緩衝區的選擇子及偏移量壓在了 3 特權級棧中,但因爲調用門的特權級變換,參數已經由處理器在固件一級上自動複製到 0 特權級棧中了。用戶的代碼段寄存器 CS 也在特權級發生變化時,由處理器自動壓入到 0 特權級棧中,因此操做系統須要的參數均可以在本身的 0 特權級棧中找到。用戶緩衝區的選擇子修改事後,接下來內核服務程序將用戶所須要的內存容量大小寫到這個選擇子和用戶提交的偏移量對應的緩衝區。若是用戶程序想搞破壞,所提交的這個緩衝區選擇子指向的目標段不是用戶進程本身的數據段,而是內核數據段或內核代碼段,因爲目標段的 DPL 爲 0,雖然此時已在內核中執行,CPL 爲 0,但選擇子 RPL 已經被改成 3,數值上不知足 CPL≤DPL && RPL≤DPL,往緩衝區中的寫入被拒絕,處理器引起異常。若是用戶程序提交的緩衝區選擇子確實指向用戶程序本身的數據段,DPL 則爲 3,數值上知足 CPL≤DPL && RPL≤DPL,往緩衝區中的寫入則會成功。若是中斷服務程序內部再有訪問內核本身內存段的操做,還會按照數值上(CPL≤DPL && RPL≤DPL)的策略進行新一輪的特權檢測。一般,若是不是用戶程序向內核提交緩衝區地址來接收數據的話,內核不會主動訪問用戶的內存段,可能是訪問本身的數據段或代碼段,內核服務程序中若訪問內核本身的內存段,因爲內存段的 DPL 爲 0,因此段選擇子的 RPL 也必須爲 0
特權級檢查又是操做系統與處理器打配合的經典案例,處理器會在硬件層面作特權級檢查的工做,而操做系統負責在軟件層面定義特權級須要的相關數據(如選擇子和門描述符)
正常狀況下代碼只能平級跳轉,除非是用門結構實現低跳高,或者返回指令實現高跳低。而數據只能是高特權級指令訪問低特權級的數據。
在內存中的數據和指令本沒有特權級的概念,自己也沒有訪問者或受訪者的概念。特權級被賦予在選擇子的 RPL 位,或者描述符的 DPL 位,配合着這兩個東西,指令和數據纔有特權級的屬性,單獨的代碼和數據討論特權級是沒有意義的。這也順利成章地證實了處理器不會每執行一條指令就去檢查特權級,只是某些條件下才進行一次特權級檢查。
若是你對自制一個操做系統感興趣,不妨跟隨這個系列課程看下去,甚至加入咱們,一塊兒來開發。
《操做系統真相還原》這本書真的贊!強烈推薦
當你看到該文章時,代碼可能已經比文章中的又多寫了一些部分了。你能夠經過提交記錄歷史來查看歷史的代碼,我會慢慢梳理提交歷史以及項目說明文檔,爭取給每一課都準備一個可執行的代碼。固然文章中的代碼也是全的,採用複製粘貼的方式也是徹底能夠的。
若是你有興趣加入這個自制操做系統的大軍,也能夠在留言區留下您的聯繫方式,或者在 gitee 私信我您的聯繫方式。
本課程打算出系列課程,我寫到哪以爲能夠寫成一篇文章了就寫出來分享給你們,最終會完成一個功能全面的操做系統,我以爲這是最好的學習操做系統的方式了。因此中間遇到的各類坎也會寫進去,若是你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即便沒有,交個朋友也是好的哈哈。
目前的系列包括