作 者:道哥,10+年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux。
關注下方公眾號,回覆【書籍】,獲取 Linux、嵌入式領域經典書籍;回覆【PDF】,獲取所有原創文章( PDF 格式)。
【IOT物聯網小鎮】
目錄
在之前的文章中Linux從頭學10:三級跳過程詳解-從 bootloader 到 作業系統,再到應用程式,由於當時沒有引入特權級的概念,使用者程式和作業系統都工作在相同的特權級,因此可以直接通過[段選擇子:偏移量] 的方式,來呼叫屬於作業系統程式碼段中的函式,如下所示:
使用者程式header
中橙色部分的資訊,表示作業系統提供的2
個系統函式,位於作業系統的哪個段描述符中,偏移地址是多少。
一旦引入了特權級別,上面這樣的呼叫方式就行不通了。
因為使用者程式的特權級一定比作業系統的特權級別低,所以即使使用者程式能夠知道函式的段選擇子和偏移地址,作業系統也會禁止使用者程式跳轉進去。
例如:應用程式的 CPL 和 RPL 都為 3,而作業系統中的函式所在的段 DPL = 0,不能通過特權級的檢查。
看過上一篇文章的小夥伴一定知道,如果把目的碼段的描述符中,TYPE.C
標誌設定為1
,也就意味著這是一個依從(或者叫一致性)程式碼段,就允許低特權級的使用者程式呼叫了。
除了這個方法之外,處理器還提供了另外一種更“正規”的方式,來實現低特權級的程式碼轉移到高特權級的程式碼,這就是:呼叫門。
這篇文章,我們就一起來學習呼叫門的機制,順帶著把所有的門描述符也一起介紹下。
門描述符
所謂的門,就是一個通道。通過這個通道,可以進入另一個程式碼段中進行執行。
在x86
中,有下面這些門:
呼叫門:用於低特權級程式碼轉移到高特權級程式碼;
任務門:用於不同任務之間的排程;
中斷門:用於非同步執行中斷處理程式;
陷阱門:也用於執行中斷處理程式,不過這裡的中斷是處理器內部產生的;
門描述符與之前介紹的段描述符本質是一樣的,都是用來描述一個程式碼段的資訊,只不過門描述符增加了一層間接性。
下面是4
個門描述符的結構(32
位系統):
從以上這4
個門描述符的結構中可以看出: 它們並沒有直接記錄目的碼段的開始地址和界限,而是記錄了目的碼段的選擇子。
也就是說:先通過門描述符找到程式碼段選擇子,然後再用這個選擇子到 GDT 中去查詢真正的目的碼段描述符,最終找到目的碼段的開始地址和界限、屬性等資訊,也就是下面這個結構:
所以說,這些門就是增加了一層間接性。
這層間接性,為作業系統提供了諸多好處。
首先,對於中斷處理來說,把所有的中斷描述符放在一個表中,可以對中斷處理程式的地址進行解耦。
其次,對於執行程式碼段的轉移來說,可以利用門來提供更靈活的特權級別控制,實現更加複雜的操作。
關於任務門中的TSS
選擇子:
所謂的任務門可以簡單理解為用於任務切換。
因為一個 TSS 段中,儲存的就是一個任務的上下文資訊快照。
只要處理器發現選擇子指向的描述符是一個任務門(通過 TYPE 欄位),它就執行任務切換:
a. 儲存當前 CPU 中的上下文到當前任務的 TSS 段中;
b. 再把 TSS 選擇子中所指向的那個 TSS 段中的上下文內容,載入到 CPU 暫存器中,這樣就實現了任務切換。
呼叫門特權級檢查規則
從呼叫門的名字就可以看出,它是為系統呼叫服務的。
再來看一下它的描述符結構:
引數個數:呼叫者傳遞多少個引數給目的碼(是通過棧空間來傳參的);
DPL:表示這個呼叫門本身的特權級;
目的碼段選擇子:最終呼叫的目的碼段的選擇子,需要用這個選擇子到 GDT 中尋找目的碼段的基地址;
偏移量:呼叫的程式碼距離目的碼段開始地址的偏移位元組數;
從以上這些欄位來看,這簡直就是為:從低特權級的使用者程式碼,呼叫高特權級的作業系統程式碼,量身定做的,只要處理器在特權級上放過使用者程式一馬就可以了。
事實上也正是如此:當用戶請求呼叫門時,作業系統會進行如下特權級檢查:
- 當前特權級 CPL (使用者程式)和請求特權級 RPL,必須 [高於或等於] 呼叫門中的 DPL;
即在數值上:CPL <= DPL,RPL <= DPL。(注意:這是呼叫門描述符裡的 DPL)
- 當前特權級 CPL(使用者程式),必須 [低於或等於] 目的碼段中的 DPL;
即在數值上:CPL >= 目的碼段描述符中的 DPL。
從以上規則可以再次看出:即使通過呼叫門,目的碼段只允許相同或者更低的特權級程式碼進入,也驗證了之前所說的:高特權級程式碼不會主動轉移到低特權級的程式碼中。
如果特權級檢查被通過,進入目的碼段之後,當前特權級CPL
是否會改變呢?
這就依賴於目的碼段描述符中的TYPE
欄位中的 C
標誌位的值:
TYPE.C = 1:CPL 保持不變,仍然為使用者程式中的特權級 3;
TYPE.C = 0: CPL 改變,變成目的碼段的特權級;
呼叫門的使用過程
安裝呼叫門
所謂的安裝,就是在GDT
中構造一個呼叫門描述符,讓它的目的碼段選擇子指向真正的程式碼段。
假設:下面這張圖是安裝呼叫門之前的狀態:
作業系統提供2
個系統函式給使用者程式呼叫,它們的程式碼位於獨立的一個程式碼段中(在GDT
中有一個程式碼段描述符)。
然後在GDT
中,新增一個門描述符(index = 8
),描述符中的“目的碼段選擇子”中的索引號,就等於 8
:
注意:根據前文提到到特權級檢查規則,為了讓使用者程式能正確進入呼叫門,需要把呼叫門描述符的DPL
設定為 3
才可以(與使用者程式的CPL
相同)。
把呼叫門的選擇子告訴使用者程式
按照之前的慣例,作業系統可以在使用者程式的頭部header
中的約定位置處,填寫呼叫們的選擇子以及函式偏移地址:
選擇子的數值為:0x0043(二進位制:0000_0000_0100_0011)
:
RPL = 3;
到 GDT 中去查詢;
索引號 index = 8;
使用者程式通過呼叫門進入系統函式
當用戶程式請求呼叫系統函式時,處理器就開始對這 3 方
的特權級展開檢查:
使用者程式的 CPL = 3, RPL = 3;
呼叫門自身的 DPL = 3;
呼叫門中的目的碼段選擇子所指向的描述符(index = 7)中 DPL = 0;
以上這些特權級的數值滿足呼叫門的特權級規則要求,於是就進入系統函式所在的程式碼中執行了。
棧的切換
x86
處理器要求:當前特權級 CPL 必須與目標棧段的 DPL 相同。
因此,使用者程式在進入作業系統中的系統函式之後:
1. 如果特權級 CPL 沒有變化
那麼在系統函式執行的時候,使用的棧仍然是使用者程式之前所使用的那個棧空間。
如果使用者程式通過棧傳遞了引數,系統函式可以直接在同一個棧空間中獲取到這些引數。
2. 如果特權級 CPL 發生了變化
那麼在系統函式執行的時候,就需要切換到使用者程式在 0 特權級
下的棧空間(作業系統在載入使用者程式的時候,就提前準備好了)。
同時,處理器會把使用者程式在 3 特權級
下使用的棧空間中的引數,全部複製到 0 特權級
下的棧空間中,這樣的話,系統函式就可以正確獲取到這些引數了。
------ End ------
打完收功!
推薦閱讀
【1】C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹
【2】一步步分析-如何用C實現面向物件程式設計
【3】原來gdb的底層除錯原理這麼簡單
【4】內聯彙編很可怕嗎?看完這篇文章,終結它!
其他系列專輯:精選文章、C語言、Linux作業系統、應用程式設計、物聯網
星標公眾號,能更快找到我!
本文正在參與 “走過Linux 三十年”話題徵文活動。