Linux從頭學13:想徹底搞懂“系統調用”的底層原理?建議您別錯過這篇【調用門】


作 者:道哥,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選擇子:

  1. 所謂的任務門可以簡單理解為用於任務切換。

  2. 因為一個 TSS 段中,保存的就是一個任務的上下文信息快照。

  3. 只要處理器發現選擇子指向的描述符是一個任務門(通過 TYPE 字段),它就執行任務切換:

a. 保存當前 CPU 中的上下文到當前任務的 TSS 段中;

b. 再把 TSS 選擇子中所指向的那個 TSS 段中的上下文內容,加載到 CPU 寄存器中,這樣就實現了任務切換。

調用門特權級檢查規則

從調用門的名字就可以看出,它是為系統調用服務的。

再來看一下它的描述符結構:

參數個數:調用者傳遞多少個參數給目標代碼(是通過棧空間來傳參的);

DPL:表示這個調用門本身的特權級;

目標代碼段選擇子:最終調用的目標代碼段的選擇子,需要用這個選擇子到 GDT 中尋找目標代碼段的基地址;

偏移量:調用的代碼距離目標代碼段開始地址的偏移字節數;

從以上這些字段來看,這簡直就是為:從低特權級的用戶代碼,調用高特權級的操作系統代碼,量身定做的,只要處理器在特權級上放過用戶程序一馬就可以了。

事實上也正是如此:當用戶請求調用門時,操作系統會進行如下特權級檢查

  1. 當前特權級 CPL (用戶程序)和請求特權級 RPL,必須 [高於或等於] 調用門中的 DPL;

即在數值上:CPL <= DPL,RPL <= DPL。(注意:這是調用門描述符里的 DPL)

  1. 當前特權級 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 方的特權級展開檢查:

  1. 用戶程序的 CPL = 3, RPL = 3;

  2. 調用門自身的 DPL = 3;

  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 三十年”話題征文活動。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM