一个操作系统的设计与实现——第12章 任务(三):3特权级任务
特權級是保護模式的核心概念之一,但我們的操作系統一直沒有引入這個概念。這是因為,特權級只有在3特權級任務存在時才有意義。本章將要實現的是3特權級任務的加載與任務切換。
12.1 特權級
12.1.1 特權級的功能
特權級(Privilege Level),是保護模式中用于限制任務權限的機制。特權級有4級,分別是0~3特權級。0特權級權限最大,供操作系統使用;3特權級權限最小,供普通任務使用。中間的兩級在我們的操作系統中不使用。
特權級起到的作用如下:
- 在不使用特殊機制的前提下,禁止代碼段寄存器切換特權級。也就是說,0特權級任務只能執行0特權級的代碼,不能執行3特權級的代碼;反之,3特權級任務只能執行3特權級的代碼,不能執行0特權級的代碼。這樣,操作系統內部的代碼就能得到保護,3特權級任務不能隨意使用
- 在任何情況下,使用數據段寄存器只能訪問平級或更低級的數據。也就是說,0特權級任務能夠訪問0特權級的數據,也能訪問3特權級的數據;反之,3特權級任務只能訪問3特權級的數據,不能訪問0特權級的數據。這樣,操作系統內部的數據就能得到保護,3特權級任務不能隨意訪問
- EFLAGS的第12~13位是IOPL(IO特權級)位,初值為0。只有特權級大于等于這兩位,即數值上小于等于這兩位的代碼段才能執行
in和out指令。在我們的操作系統中不修改這兩位,因此,只有0特權級任務才有權限執行in和out指令。這樣,IO端口就能得到保護,3特權級任務不能隨意訪問 - 有少量非常關鍵的指令屬于特權指令,這些指令只有0特權級任務有權限執行。在我們的操作系統中,特權指令包括以下幾種:
-
hlt指令。顯然,低特權級任務不能隨意將CPU掛起 -
sti、cli指令,以及其他試圖修改IF位的指令,如popf。低特權級任務不能關中斷,否則搶占式任務切換就失效了 - 試圖修改IOPL的指令,如
popf。低特權級任務不能修改IO特權級,否則對IO端口的保護機制就失效了 - 任何試圖讀取或修改控制寄存器、GDTR、LDTR(局部描述符表,在我們的操作系統中未使用)、IDTR、TR(見下文)的指令。如
mov cr0, eax、lgdt等。這些指令對CPU有重大影響,例如,修改CR0能開關保護模式和分頁模式,這顯然不能供低特權級任務隨意使用 -
invlpg指令。低特權級任務不能干預TLB
-
12.1.2 DPL,RPL與CPL
特權級由三部分組成:描述符特權級(Descriptor Privilege Level,DPL),請求特權級(Requested Privilege Level,RPL)和當前特權級(Current Privilege Level,CPL)。DPL位于描述符中,如段描述符,中斷門等;RPL是段選擇子的低2位;CPL恒等于CS的RPL。
CPL決定了當前任務的執行權限。上文中的"只有0特權級任務有權限執行"、"3特權級任務只能訪問3特權級的數據"等描述中的特權級,指的就是CPL。由于CPL恒等于CS的RPL,所以切換CS的過程,就是切換CPL的過程。
DPL決定了一個描述符的最低訪問權限。向段寄存器加載段選擇子時,CPU要求:在數值上,CPL <= DPL && RPL <= DPL。即,只有特權級平級,或更高級的指令才有權限訪問此描述符。
RPL看上去是個沒什么用的概念。如果沒有RPL,CPL可以恒等于代碼段的DPL;向段寄存器加載段選擇子時,只需要CPL <= DPL即可。然而,RPL解決的是一個比較邊緣的問題。設想:操作系統給3特權級任務提供了一個讀硬盤函數,并要求3特權級任務提供數據段選擇子以存放結果。此時,用戶可以想辦法猜到0特權級數據段選擇子,并將其提供給操作系統,這樣一來,3特權級任務就能讀取0特權級數據了。雖然操作系統可以通過軟件手段檢測任務提供的段選擇子是否可行,但這樣做很麻煩且效率不高。給段選擇子附加RPL后,即使3特權級任務故意把RPL寫成0,操作系統也能在拿到段選擇子后,強制將RPL改成3,再進行后續操作,從而將特權級檢查交給CPU進行,即提高了效率,又比較方便。
正因為如此,CPL才由CS的RPL決定,而不是由代碼段描述符的DPL決定。因為DPL只是一個最低標準,RPL才能描述真正的特權級。
12.1.3 特權級的提升
操作系統存在的意義是為3特權級任務提供服務,而不是死守自己的0特權級代碼,不讓任何人使用。上文提到,數據段在任何情況下都不能被低特權級任務訪問,但代碼段不同,操作系統可以將一部分函數開放給3特權級任務使用。
然而,操作系統的函數畢竟是0特權級的,3特權級任務想要使用這些函數,就需要有一套升降級機制,在調用0特權級函數之前先升級,調用完成后再降級。
對于這個需求,CPU提供的標準機制是調用門(Call gate),但這種機制要求為每個函數分別安裝一個調用門,這是非常麻煩且效率低下的,所以,在我們的操作系統(以及幾乎所有的現代操作系統)中都不使用這個機制。
事實上,中斷門也有提升特權級的能力。因此,中斷門可以用于向3特權級任務提供0特權級函數。具體步驟如下:
- 發起一個中斷
- 檢查
CPL <= DPL(中斷沒有RPL)。如果通過,則允許任務進入中斷門。只有由指令發起的中斷會進行這一步,外中斷和CPU自己發起的中斷無視中斷門的DPL - 檢查
CPL >= 中斷門中CS的RPL,如果通過,則允許CS升級并調用中斷門中的函數
綜上,想要使用中斷門,就必須滿足兩個條件:
- 有權限進入中斷門。這由中斷門的DPL決定。這樣做的目的是對3特權級任務能夠使用的中斷門進行限制
- 進入中斷門后,必須發生升級或平級,不允許降級
12.1.4 0特權級棧、TSS與TSS描述符
在特權級切換時,CPU還有一個特殊要求:SS的RPL必須時刻等于CPL。這意味著,每個3特權級任務必須有兩個棧,分別供0特權級與3特權級使用。當特權級發生切換時,棧也要跟著切換。那么,這兩個棧存放在哪呢?
CPU規定:0特權級棧需要放置在任務狀態段(Task State Segment,TSS)中。TSS是一個至少為104字節(見下文)的表,結構如下:
TSS需要以TSS描述符的形式安裝在GDT中。TSS描述符是系統段的一種,結構如下:
TSS描述符中的B位,即忙(Busy)位,由CPU在加載TSS時自動置1,構造TSS描述符時應將其置0。其他位的含義同段描述符。
與GDT、IDT類似,CPU也為TSS提供了一個專用寄存器。不過,這個寄存器不叫TSSR,而是叫任務寄存器(Task Register,TR)。在TSS描述符安裝到GDT中以后,需要使用ltr TSS描述符的選擇子指令將TSS加載到TR。TSS描述符的選擇子可以存放在16位寄存器或內存中。
TSS(以及任務門,Task gate)是CPU提供的用于任務切換的標準機制,然而還是老問題:TSS使用起來非常麻煩,需要給每個任務都安裝一個,且效率很低。所以,在我們的操作系統(以及幾乎所有的現代操作系統)中都不使用這個機制。但TSS還有另一個功能,那就是獲取任務的0特權級棧。具體來說,當中斷發生時,引入特權級概念后的過程如下:
- 檢查CPL是否有權限進入中斷門
- 檢查中斷門中的CS是否能使CPL升級或平級
- 如果中斷門中的CS與CPL平級,跳過此步驟;否則,暫存SS和ESP,然后將SS和ESP分別切換為TSS中的SS0和ESP0,再將暫存的SS通過高位補0的方式填充至32位后壓棧,接著將暫存的ESP壓棧
- 將EFLAGS壓棧,然后將EFLAGS的IF位清零
- 將CS通過高位補0的方式填充至32位后壓棧
- 將EIP壓棧
- 跳轉至中斷門中的中斷處理函數
也就是說,雖然不使用TSS進行任務切換,但仍然需要一個TSS,其存在的唯一目的就是提供0特權級棧。
上文提到,TSS"至少為104字節"。這是因為TSS還能提供一個被稱為IO位圖的功能,這個位圖延長在TSS后面,由TSS中的IO位圖基址控制。IO位圖用于越過IOPL,給特定的一些IO端口開白名單。在我們的操作系統中不使用IO位圖,但也不能將其置0。CPU要求:如果IO位圖基址的值大于等于TSS描述符中的TSS限長,則表示IO位圖不存在。在我們的操作系統中,可將其置103(或0xff等更大的值)。
12.1.5 特權級的降低
3特權級任務能夠通過中斷門提升特權級,但這畢竟是暫時的。在中斷處理函數調用完成后,就需要回到3特權級。
特權級的降低由iret指令實現。引入特權級概念后,該指令的執行過程如下:
- 從棧中依次彈出EIP、CS、EFLAGS。如果彈出的CS的RPL為0,
iret指令就此完成;否則,繼續執行以下步驟 - 依次檢查DS、ES、FS、GS的RPL是否為3。如果不是,則將其修改為0。這一步的目的是:避免因中斷返回使0特權級的段選擇子泄漏到3特權級。GDT的第一個描述符必須為空的目的就在于此
- 繼續從棧中彈出ESP和SS,將棧恢復到3特權級棧。因此,TSS中不需要存放3特權級棧,3特權級棧的SS和ESP位于0特權級棧中
12.2 3特權級任務的實現原理
12.2.1 3特權級代碼段與數據段
GDT中需要安裝一個3特權級代碼段描述符,和一個3特權級數據段描述符,以供3特權級任務使用。這兩個描述符除了DPL為3外,其他屬性與0特權級描述符相同。
12.2.2 TSS
GDT中需要安裝一個TSS描述符,并使用ltr指令加載這個TSS。
12.2.3 3特權級任務的切換
3特權級任務的切換也基于時鐘中斷。但上一章中的"6個段寄存器不會發生改變"這一結論現在已經不成立了。所以,在時鐘中斷處理函數中,不僅需要將8個通用寄存器壓棧,還需要將除了CS和SS以外的4個段寄存器壓棧。
此外,由于每個任務都有一個ESP0,所以在任務切換時,需要修改TSS中的ESP0。SS0對于每個任務來說都是一樣的,所以無需修改。
12.2.4 3特權級任務的創建
與0特權級任務類似,3特權級任務的創建也基于偽造棧技術。
現在,由于4個段寄存器也被壓棧,對于0特權級任務來說,需要在棧上偽造15個寄存器的值;對于3特權級任務來說,需要在棧上偽造17個寄存器的值。
3特權級任務不僅需要0特權級棧,還需要3特權級棧。在我們的操作系統中,3特權級棧為一頁,其虛擬地址固定為0xc0000000向下的0x1000字節。
12.2.5 3特權級任務的加載與重定位
3特權級任務往往不是操作系統的一部分,而是由用戶提供的,存放在硬盤上的一個程序。從硬盤上加載程序可由硬盤驅動完成,解析ELF也不是難事,剩下的問題是:這個程序該如何重定位呢?
平坦模型失去了重定位能力,重定位由分頁模式以一種完全不同的方式實現。具體來說,編譯器可以為程序提供一套虛擬地址,只要虛擬地址落在任務地址空間內即可。操作系統在加載任務時,不主動分配虛擬地址,而是使用ELF文件提供的虛擬地址,并為這些虛擬地址分配物理地址,并安裝PDE、PTE,然后,將ELF文件中的程序段展開到這些虛擬地址中。
12.3 3特權級任務的實現
12.3.1 添加3特權級描述符、TSS描述符
請看本章代碼12/Mbr.s。
第143~145行,定義了三個新的段描述符。分別是3特權級代碼段描述符,段選擇子是(3 << 3) | 0x3;3特權級數據段描述符,段選擇子是(4 << 3) | 0x3;TSS描述符,段選擇子是5 << 3。
TSS定義在內核中,在MBR中不知道其地址,所以,TSS描述符目前僅用于占位。
12.3.2 修改時鐘中斷處理函數
請看本章代碼12/Int.s。
第3行,聲明了外部鏈接的printStr函數。
第7行,聲明了外部鏈接的TSS。TSS定義在本章代碼12/Task.hpp中。
第108~111行, 將四個數據段寄存器壓棧。
第137~138行,將TSS中的ESP0修改為新任務的ESP0。
第141~144行,彈出新任務的四個數據段寄存器。
12.3.3 系統調用
上文提到,可以使用中斷將0特權級函數提供給3特權級任務使用。這個方案看似需要很多中斷號,但實際上有更好的設計:構造一個函數表,其中存放的是操作系統為3特權級任務提供的函數。然后,只使用一個中斷,在中斷處理函數中調用函數表中的函數,具體調用哪個函數由調用者通過一個索引值指定。上述操作被稱為系統調用,調用者提供的索引值被稱為系統調用號。
在我們的操作系統中,調用者應使用EAX存放系統調用號;EBX、ECX、EDX用于存放參數(如果有)。在系統調用的中斷處理函數中,不管有幾個參數,都將EBX、ECX、EDX壓棧,然后使用EAX找到一個函數并調用。
請看本章代碼12/Int.s。
intSyscall是系統調用的中斷處理函數。
第150~152行,不管實際需要幾個參數,都將EBX、ECX、EDX壓棧。
第153行,使用EAX中存放的系統調用號調用[syscallList + eax * 4]這個函數。
第154行,將棧恢復。
第156行,使用iret指令從中斷返回。
第210行,在intList的最后添加intSyscall函數。現在的IDT擴充至49個中斷門,系統調用的中斷號是0x30。
第212~213行,定義系統調用表。目前只支持一個系統調用:printStr函數,其系統調用號為0。
接下來,請看本章代碼12/Int.hpp。
第7行,將IDT的大小修改為49。
第25行,將intSyscall函數安裝到IDT中。注意:由于系統調用是給3特權級任務使用的,所以中斷門的DPL必須為3,這樣3特權級任務才有權限使用這個中斷門。
12.3.4 內存管理系統的修改
請看本章代碼12/Memory.h。
第9行,聲明了installTaskPage函數。
接下來,請看本章代碼12/Memory.hpp。
installTaskPage函數是本章新增的函數,其用于在指定的虛擬地址處分配物理地址,并將虛擬地址與物理地址建立聯系。所以,這個函數相當于__allocatePage函數的簡化版。
第85行,從TCB中取得當前任務的虛擬地址位圖,接下來需要手動設置這個位圖。
第87行,判斷虛擬地址是否超出了位圖范圍。進行這個判斷的原因是,這個函數不僅用于安裝ELF文件中的地址,這些地址都很接近0;還用于安裝3特權級棧,地址是0xc0000000 - 0x1000,這個地址遠遠超過了一頁位圖能表示的128M內存,所以,此時無需設置位圖。
第89~92行,根據虛擬地址設定位圖中已使用的位。
第95~100行,為虛擬地址的每一頁安裝物理地址。這段代碼的實現原理和第66~71行一致。
12.3.5 0特權級任務加載器的修改
請看本章代碼12/Task.hpp。
由于任務切換時添加了4個段寄存器的壓棧,所以偽造棧的過程也要隨之修改。
第96行,將原先的11 * 4修改為15 * 4。
第108~114行,重新調整棧上偽造的數據。
12.3.6 安裝并加載TSS
請看本章代碼12/Task.h。
第16行,聲明了外部鏈接的TSS。
接下來,請看本章代碼12/Task.hpp。
第10行,定義了TSS,但暫時沒有初始化。
__makeTSSDescriptor函數用于構造TSS描述符。
讀者要格外小心這個函數的實現,例如"TSS地址的最高8位",從TSS描述符的結構圖上看,其位于第56~63位,但如果實現為(tssBase & 0xff000000) << 56,那就完全錯了。tssBase & 0xff000000得到的這個數字相當于已經左移了24位,所以,只需要再左移32位即可。
__installTSS函數用于安裝TSS。
第23~24行,初始化TSS中的SS0與IO位圖基址。內核用不到ESP0,所以無需初始化。
第28行,使用sgdt指令獲取GDTR。
第30行,在GDT中安裝TSS描述符。
第32行,重新加載GDT。此時,TSS的段選擇子5 << 3可用。
第33行,使用ltr指令加載TSS。
第51行,在taskInit函數中添加對__installTSS函數的調用。
12.3.7 3特權級任務的加載
請看本章代碼12/Task.h。
第23行,聲明了loadTaskPL3函數。
接下來,請看本章代碼12/Task.hpp。
loadTaskPL3函數是本章新增的函數,其用于加載3特權級任務。
不同于loadTaskPL0函數,loadTaskPL3函數的參數是硬盤的起始扇區號與扇區數。所以,此函數可以一步到位的從硬盤上加載3特權級任務。
第124~136行,與loadTaskPL0函數的開頭部分一致。用于分配新任務的TCB,CR3,以及虛擬地址位圖,并設置好新的CR3。
第138行,將扇區數轉換為頁數。這里使用了以下公式:
\[\lceil \frac{a}{b} \rceil =\lfloor \frac{a\,\,+\,\,b\,\,-\,\,1}{b} \rfloor \]第139行,分配ELF緩沖區。
第141行,調用硬盤驅動中的讀硬盤函數,將任務從硬盤讀取到緩沖區。
第143~145行,讀取ELF文件頭中的3個信息:程序頭表地址,程序頭表中每個表項的大小,以及程序頭表中表項的數量。
第147~154行,保存當前的CR3,并將其暫時切換到新任務的CR3上。這里的內聯匯編使用了第6章討論的獨占約束"=&r"。這一步的目的是:任務加載到的虛擬地址屬于任務自己的地址空間,所以,應在任務的CR3中分配內存。
第156行,遍歷程序頭表中的每個表項。
第158行,判斷當前表項的類型,只關注類型為1的表項。
第160行,從表項中讀取源地址。
第161行,從表項中讀取目的地址。
第163行,將ELF要求的內存大小向上取整到頁數。這里使用了上文中的公式。
第165行,使用installTaskPage函數在ELF要求的加載地址處安裝頁。
第167~168行,加載程序段并構造BSS段。
第172行,從ELF文件頭中讀取任務的入口點。
至此,ELF已經加載完畢。
第174行,回收ELF緩沖區。這里雖然使用的是任務的CR3,但內核地址空間是共享的,只要有權限,任何CR3都能分配和回收內核頁。
第176~196行,偽造3特權級任務的0特權級棧,一共需要偽造17個寄存器的值。
第198行,為3特權級棧安裝物理頁。
第200~204行,將CR3換回。
第206~208行,初始化新任務的虛擬地址位圖,然后將其添加到任務隊列中。這兩行代碼與loadTaskPL0函數中的一致。
12.4 測試
請看本章代碼12/Test.c。
這個任務現在運行在3特權級下,所以其沒有權限使用操作系統中的所有函數,唯一能用的是0號系統調用。使用系統調用還有一個好處:由于系統調用是中斷,而中斷過程中不會發生任務切換,所以系統調用是自帶鎖的。
第5~9行,在循環中不斷發起0號系統調用,打印Task字符串。
由于我們的操作系統目前仍然不支持任務回收,所以任務不能退出。
接下來,請看本章代碼12/Makefile。
第6行,編譯Test.c。
第8行,鏈接Test.o。
對于ld命令來說,如果不設定-Ttext-segment參數,則任務的默認加載地址為0x8048000。我們不需要也不能使用這么大的加載地址,因為這個數字甚至超過了128M。因此,鏈接Test.o時需要加上-Ttext-segment 0參數。
第11行,將Test寫入虛擬硬盤。
接下來,請看本章代碼12/Kernel.c。
第15~16行,將測試任務加載兩次。
第22~26行,在循環中不斷發起0號系統調用,打印Kernel字符串。這樣做有兩個目的:
- 利用系統調用自帶鎖這一性質
- 觀察中斷門的平級調用
12.5 調試
本章的任務運行在3特權級下,因此,如果想要手動進行任務切換以調試程序,就需要將__installIDT函數中的0x8e00臨時修改為0xee00,使得3特權級任務也能進入0x20中斷門。
在bochs調試器中,TSS可以通過info tss命令查看。
總結
以上是生活随笔為你收集整理的一个操作系统的设计与实现——第12章 任务(三):3特权级任务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python标准库中隐藏的利器
- 下一篇: c# char unsigned_dll