作 者:道哥,10+年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux。
關注下方公眾號,回覆【書籍】,獲取 Linux、嵌入式領域經典書籍;回覆【PDF】,獲取所有原創文章( PDF 格式)。
【IOT物聯網小鎮】
目錄
x86
處理器中,提供了4
個特權級別:0,1,2,3。數字越小,特權級別越高!
一般來說,作業系統是的重要性、可靠性是最高的,需要執行在 0 特權級
;
應用程式工作在最上層,來源廣泛、可靠性最低,工作在 3 特權級別
。
中間的1 和 2
兩個特權級別,一般很少使用。
理論上來講,可以把那些可靠性介於作業系統和應用程式之間的程式安排在這兩個特權級上。
在處理器中,有3
個相關的術語與特權級密切相關:
CPL: Current Privilege Level 當前特權級;
DPL: Descriptor Privilege Level 描述符特權級;
RPL: Requestor Privilege Level 請求特權級;
理解了這3
個特權級的保護規則,就理解了作業系統保護系統的終極密碼!
CPL:當前特權級
當前特權級,是指當前正在執行的程式碼的特權級別,它由當前正在執行的程式碼段暫存器cs
中的bit[1 ~ 0]
來決定:
cs
暫存器中的最低兩位怎麼寫的是 RPL
?
原因是這樣的:我們在執行一段程式碼之前,這段程式碼位於記憶體中的一段空間中,而它的程式碼段描述符位於LDT
區域性描述符表中,如下圖所示:
假設現在想進入這個程式碼段中執行,那麼我們就需要給程式碼段暫存器 cs
賦值為:0x0007 (0000_0000_0000_0111)
。
此時cs
暫存器中當前內容的低兩位就稱作:當前優先順序,而準備賦值給cs
的值是 0x0007
,這個0x0007
就稱作選擇子。
按照選擇子的結構來解析:
RPL: bit[1 ~ 0] = 11B,十進位制就是 3,就表示這個選擇子的請求特權級別是 3;
TI: bit[2] = 1B,表示到 LDT 中查詢段描述符;
索引號:bit[15 ~ 3] 的索引值為 0,表示到 LDT 中偏移量為 0 (0 = 0 * 4, 每個描述符佔據 4 個位元組) 的位置獲取段描述符;
當處理器進行一系列許可權檢查之後,允許進入這段程式碼中去執行,那麼就設定 cs = 0x0007
。
此時cs
暫存器中的最低2
位就等於選擇子中的 RPL
,也就是 3
。
在一般情況下,CPL
都是等於RPL
的。
DPL:描述符特權級
DPL
指的是一個段描述符中,用來指定這個描述符所代表的段,具有什麼樣的特權級別。
關於描述符的結構,如下圖所示:
bit[14 ~ 13]
就表示這個段描述符的特權級別。
當請求訪問一個段時(不論是資料段,還是程式碼段),處理器在GDT
或者LDT
中找到段描述符之後,就會把CPL、RPL
與描述符中的 DPL
進行比較。只有滿足一定的規則,才允許訪問這個描述符所指向的那個段。
具體的比較規則,下文有描述。
RPL:請求者特權級
剛才的CPL
內容中,已經描述了RPL
是什麼東西,它倆是密切相關的。
但是,有時候 CPL 與 RPL 並不相同。
比如:
一個使用者程式,想通過作業系統提供的系統函式,去訪問記憶體中的一塊使用者程式自己的記憶體空間(資料段)。
使用者程式需要告訴作業系統:訪問哪一個資料段,偏移量是多少。
這些資訊需要把一個選擇子通過作業系統來賦值給資料段暫存器 ds。
假設選擇子是 0x000F(二進位制:0000_0000_0000_1111)
:
索引號:1;
TI: 使用 LDT;
RPL: 3;
也就是說:當作業系統接受使用者程式的請求之後,開始執行系統函式時,此時的CPL
是作業系統的特權級別 0
。
此時作業系統需要把一個選擇子賦值給資料段暫存器 ds,而這個選擇子是由使用者程式作為引數傳遞給作業系統的。
在這個場景中:CPL = 0, RPL = 3,它倆就不相等。
作業系統用這個選擇子0x000F
到使用者程式的LDT
中,根據索引號1
找到資料段描述符之後,把CPL(0)、RPL(3)
與描述符中的 DPL
進行比較,來判斷是否有許可權訪問這個資料段。
使用者程式的資料段 DPL 一定是 3,這是由作業系統在載入程式之初就決定好的;
根據下文的特權級檢查規則,這樣的訪問是允許的;
其實這裡有一個隱患:
假如使用者程式是一個惡意程式,它想破壞作業系統的資料,於是就傳入一個指向作業系統數段的選擇子:0x0010
(二進位制:0000_0000_0001_0000
):
索引號:2(假設通過其它途徑,知道作業系統的某個資料段位於 GDT 的第 2 個表項);
TI: 使用 GDT;
RPL: 0;
此時,如果作業系統很無腦的就原樣接收了使用者程式的呼叫請求,就會通過GDT
找到屬於作業系統的資料段進行破壞性操作。
作業系統不會這麼傻的,它在接收使用者程式請求的時候,會嚴格檢查使用者程式傳入的引數。
如果它發現執行在 3 特權級
的使用者程式,傳入一個 0 特權級
的 RPL,就會警覺:請求特權級竟然比你自己的執行特權級還高,你想幹什麼?
於是,作業系統就會把選擇子中的RPL
修改為使用者程式的當前特權級 CPL
。
就好比:一個村長去找市長辦事,訴求是:想在自己村的集體土地上蓋一座廠房。市長認為:這是你們村自己的土地,你可以隨便折騰,准許。
但是,如果村長的訴求是:想在市民廣場的旁邊蓋一座廠房。此時市長就會呵斥:這個地方不是你們村的一畝三分地,想幹啥就幹啥,滾開!
特權級檢查規則
程式碼段的特權級檢查
一般情況下,只允許兩個特權級相同的程式碼段進行轉移。
例如:
從使用者程式的一個程式碼段(CPL = 3),跳轉到另一個 DPL = 3 的程式碼段;
從作業系統的一個程式碼段(CPL = 0),跳轉到另一個 DPL = 0 的程式碼段;
但是處理器也提供了一些特殊途徑,讓低特權級的程式碼可以轉移到高特權級的程式碼中去執行:
如果在高特權級程式碼段描述中的 TYPE 欄位中,C = 1,就允許低特權級的程式碼轉移進來;
通過呼叫門,低特權級程式碼也可以轉移到高特權級的程式碼段;
這裡主要描述第一種情況,也就是當目的碼段描述符的TYPE
欄位中 C = 1
,也就是所謂的依從程式碼,或者一致性程式碼。
也即是說:如果 TYPE.C = 1
,那麼處理器就允許:比這個描述符的 DPL 更低特權級的程式碼,轉移到這個程式碼中來執行。
在數值上就是:(特權級越低,數值越大)
CPL >= DPL
RPL >= DPL
例如:作業系統中有2
個程式碼段,它們的描述符中的C
標誌位不同:
此刻正在執行一個使用者程式: CPL = 3
。
那麼使用者程式就可以轉移到程式碼段 2中去執行,不可以轉移到程式碼 1中。
並且,轉移到作業系統的程式碼段2 之後,當前特權級CPL
保持不變,仍然為 3
。
有兩個類比:
1. 類似於 Linux 中的 sudo 指令
如果一條指令需要root
許可權,我們可以使用su -
指令,把身份轉換到 root
,然後再去執行。
此時所有的身份、環境變數等資訊,都是root
使用者的。
我們還可以直接使用sudo
指令,這時就相當於是臨時提升了使用者的許可權,但是那些環境變數等資訊,依然是當前使用者的,而不是 root 使用者的。
2. 村長找市長辦貸款
村長去市裡的銀行申請貸款,但是自己的權力不夠,銀行不鳥他,於是村長就去找市長幫忙。
於是,市長就給村長一個親筆介紹信,村長帶著這封信到銀行之後,銀行一看:有市長大人的背書,於是就給村長辦理貸款手續了。
但是,在辦理手續的過程中,所有需要簽字的地方,只能寫村長自己(特權級不變),而不能寫市長的名字。
另外,對於上圖中的程式碼段1,由於其C
標誌位是 0
,只能允許相同特權級的程式轉移進來,從數值上表示就是:
CPL == DPL
RPL == DPL
最後還有一點需要記住:高特權級的程式碼,永遠都不能轉移到低特權級的程式碼。就好比:市長永遠都不會以村長的身份去辦事。
資料段的特權級檢查
資料段的特權級檢查規則比較簡單:高特權級的程式,可以訪問低特權級的資料,反之不可以。
從數值上表示就是:
CPL <= DPL
RPL <= DPL
棧段的特權級檢查
棧段的特權級檢查規則,也比較簡單,x86
處理器要求當前特權級 CPL 必須與目標棧段的 DPL 相同。
從數值上表示就是:
CPL == DPL
RPL == DPL
為了滿足這個要求,當從使用者程式(CPL = 3
)轉移到作業系統(DPL = 0
)時,如果是通過依存(一致性)程式碼段轉移進去,當前特權級是不變的,此時使用的棧仍然是使用者程式的棧空間。
如果是通過其他途徑轉移進去(eg: 呼叫門),當前特權級發生了變化(CPL = 0),此時使用的棧就必須是 0 特權級下的棧空間了。
因此,作業系統在載入這個使用者程式的時候,就需要提前申請一塊棧空間,以準備在以上這樣的場景中使用。
在Linux
系統中,只用了0 和 3
這兩個特權級,因此每一個使用者程式只需要提前準備好0
特權級下使用的棧就可以了。
如果一個作業系統使用了0 ~ 3
所有的四個特權級,那麼作業系統就必須為:執行在3
特權級下的使用者程式準備3
個棧空間,用於該使用者程式轉移到特權級 0、1、2
下作為棧空間來使用。
------ End ------
這篇文章主要從特權級的角度,來理解作業系統對系統的保護。
在這樣的機制下,作業系統很好的保護了系統不被惡意程式破壞,同時也為使用者程式的執行提供了一些通道,來呼叫更底層的功能。
推薦閱讀
【1】C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹
【2】一步步分析-如何用C實現面向物件程式設計
【3】原來gdb的底層除錯原理這麼簡單
【4】內聯彙編很可怕嗎?看完這篇文章,終結它!
其他系列專輯:精選文章、C語言、Linux作業系統、應用程式設計、物聯網
星標公眾號,能更快找到我!