《操作系統真象還原》讀書筆記 第4章

0x1 保護模式基本概念

物理內存地址不能直接被程序訪問,程序內部的地址(虛擬地址)需要被轉換成物理地址後再去訪問,程序對此一無所知。地址轉換是由處理器和操作系統共同協作完成的,處理器在硬件上提供地址轉換部件,操作系統提供轉換過程中所需的頁表。

0x1.1 實模式不是32位CPU,變成了16位

32位CPU具有保護模式和實模式兩種運行模式,可以兼容實模式下的程序。兼容實模式,是指能夠正確處理好實模式下的程序,並不是說在實模式下運行時就完全變成了純16位的CPU。就像高中生做小學生的題,就意味着做題人從高中生變爲了小學生。
實模式的運行環境時16位,保護模式運行環境32位。
當它以16位的實模式運行時,不是說CPU變成了純粹的16位CPU(硬件本身不會變),32位CPU在16位的實模式下運行,雖說相當於更爲強大的16位的CPU,但其本質可是32位的。當我們開機時,CPU一開始處於實模式下,它相當於邏輯上的純粹CPU,它不知道自己可以使用32位尋址,只有在指定的標誌位上設置爲1時才,CPU纔會“明白”自己原來是可以使用32位尋址的。

0x2 初見保護模式

0x2.1 保護模式之寄存器拓展

到了保護模式下CPU變成了32根地址總線,32根地址總線足夠訪問4GB的空間,所以沒有必要按照老方式尋址了,爲了滿足4GB空間尋址,寄存器寬度也增加了一倍,從原來的2字節變爲4字節32位。除了段寄存器仍然使用16位,其餘通用寄存器都提升到32位。
寄存器要保持向下兼容,不會重新構造原來的基礎設備而是在原有的寄存器基礎上進行了拓展。經過拓展後的寄存器在原有名字上加了個e。如圖所示。
在這裏插入圖片描述
高16位沒辦法單獨使用,只能在用32位寄存器時纔有可能使用到它們。
保護模式下還有一個大的變化就是段寄存器,段寄存器中要記錄32位地址的數據段基址,16位肯定是裝不下的,所以段基址都存儲在一個數據結構中——全局描述符表。既然叫表,說明裏面肯定存在表項,其中每個表項稱爲段描述符,其大小爲64字節,用來描述各個內存段的起始地址、大小、權限等信息。該全局描述符表很大,所以放在內存中,由GDTR寄存器指向它就行。
這樣,段寄存器中保存的就再也不是段基址了,裏面保存的內容叫“選擇子”,selector,說白了就是段描述符表的表項,就是個索引用的數,把全局描述符表當作數組來看的話,它就是數組的下標。
注意:
1)段描述符在內存中,訪問內存對CPU來說比較慢,效率不高
2)段描述的格式很奇怪,一個數據要分三個地方存,所以CPU要把這些七零八落的數拼合成一個完整的數據也是要花時間的。
爲了提高獲取段信息的效率,對段寄存器採用了緩存技術,將段信息用一個寄存器來緩存,這就是段描述符緩衝寄存器(Descriptor Cache Registers)。對程序員而言它是不可見的。CPU每次將獲取到的內存段信息,整理成“完整的、通順、不蹩腳”的形式後,存入段描述符緩寄存衝器,以後每次訪問相同的段時,就直接讀取該段寄存器對應的段描述符緩衝寄存器。
另外,雖然段描述符緩衝寄存器時保護模式下的產物,但它也可以用在實模式下。也是爲了避免重複計算浪費相應的時間。
既然是緩存,就一定要有個失效時間。段描述符緩衝寄存器的失效時間是多少?
其實這個時間並不固定,原則上,只要往段寄存器中賦值,CPU就會更新段描述符緩衝寄存器。例如,在保護模式下加載選擇子(即使新選擇子的值和之間段寄存器中老選擇子相同),CPU就會重新訪問全局描述符表,再將獲取的段信息重新放回描述符緩衝寄存器,或在實模式下爲段寄存器賦予段基址,無論是否與之前段基址相同,段基址左移4位後的結果就被送入段描述符緩衝寄存器。
下面列出三種段描述符緩衝寄存器結構。
在這裏插入圖片描述
保護模式下基址寄存器拓展到了所有通用寄存器,而不再是只限於bx,bp,變址寄存器也不再是si、di這兩個。具體算法拓展圖如下,作者給的很清楚。
在這裏插入圖片描述

0x2.2 保護模式之運行模式反轉

我們的32位CPU既支持實模式,又支持保護模式。那麼同一個彙編指令我們怎麼確定它是在保護模式下執行的還是實模式下運行的?
解決這個問題前先回憶下指令格式
在這裏插入圖片描述
在這個格式種第3個字段用於指令尋址方式和操作數類型,在指令格式不變的情況下,爲了兼容保護模式,一種方案是重新定義各種尋址方式、寄存器的編碼。由於保護模式中的尋址方式和操作數類型同時模式下完全不同,故相應的編碼也不同。比如在實模式下,用二進制010表示dx寄存器,在保護模式下的010就表示edx寄存器,對於硬件來說是完全不一樣的,所以編譯器必須明確操作對象是哪個。
在實模式下,指令和操作數都是16位,它可以使用32位的資源。同樣在保護模式下,指令和操作數都是32位的,它也可以使用16位的資源。也就是說,在某模式下,可以使用另一模式的資源。
爲了區分指令運行是在哪種模式下運行的,編譯器提供了僞指令bits,用它來向編譯器傳達:我下面的指令都要編譯成xx位的,因爲我們編程者知道也希望自己的程序運行在xx模式下。比如在實模式下,運行的指令都是16位的,所以編譯器要將代碼編譯成16位的指令。在實模式下準備好了保護模式所需的環境後,進入保護模式後的代碼就應該是32位指令。也就是,同一段程序要經歷兩種模式,所以同一段程序中有兩種模式的機器碼。

bits 的指令格式是 [bits 16][bits 32]
[bits 16]是告訴編譯器,下面的代碼幫我編譯成16位的機器碼
[bits 32]是告訴編譯器,下面的代碼幫我編譯成32位的機器碼
範圍是從當前[bits xx]標籤一直到下一個[bits xx]標籤

進入保護模式的代碼並不統一。比如進入保護模式需要三個步驟。
1)打開 A20
2)加載gdt
3)將cr0的pe位置1
這三個步驟可以不順序,也可以不連續,並且每個步驟又是由多個小步驟完成的,每個小步驟形式又是不固定的,組合多種多樣。爲了明確確定運行模式,使用bits僞指令是最簡單的辦法。
什麼是反轉前綴
在指令格式種有一個前綴字段,裏面存放的是指令選項,比如指令重複前綴rep、段跨越前綴“段寄存器:”,還有操作數反轉前綴0x66和尋址方式反轉前綴0x67。
在不同模式下的操作,操作數和尋址方式都各有不同。
我們知道模式之間可以相互使用對方環境下的資源。比如,16位實模式下可以使用32位保護模式下的寄存器。如果要用另一模式下的操作數大小,需要在指令前添加指令前綴0x66,將當前指令模式臨時轉變爲另一種模式。這就是反轉的意義,不管當前模式是什麼,總是轉變成相反的運行模式。
比如,在指令中添加了0x66反轉前綴後
假如當前運行模式是16位實模式,操作數大小變爲32位。
假設當前運行模式是32位保護模式,操作數大小變爲16位。
這個轉換是臨時的,只有在當前指令纔有效。
在這裏插入圖片描述
尋址方式反轉前綴0x67
操作數可以在模式間相互轉換,那麼尋址方式一樣可以,只需要在它的指令前加上0x67反轉前綴即可。
在這裏插入圖片描述

0x3 全局描述符表

到了保護模式下,內存段(如數據段、代碼段等)不再是簡單地用段寄存器加載下基址就能使用了,增加了很多信息,需要提前把段定義好才能使用。
全局描述符表(Global Descriptor Table,GDT)是保護模式下內存段的登記表,這是不同於實模式的特徵。

0x3.1 段描述符

內存段是一片內存區域,訪問內存就要提供段基址,所以要有段基址屬性。
爲了限制程序訪問內存的範圍,還要對段大小進行約束,所以要有段界屬性。同時還增加了一些約束條件。
這些用來描述內存段的屬性,被放到一個稱爲段描述符的結構體中,這個結果專門用來描述一個內存段,該結構是8字節大小,這8個字節是連續不可拆分的。如下圖
在這裏插入圖片描述
保護模式下地址總線是32位,段基址需要用32位來表示。
段界限表示段邊界的擴展最值,即最大擴展到多少或最小擴展到多少。擴展方向只有上下兩種。對於數據段和代碼段,拓展方向是向上拓展的,即地址越來越高,此時的段界限用來表示段內偏移的最大值。對於棧段,段的拓展方向是向下拓展,即地址越來越低,此時段界限用來表示段內偏移的最小值。無論是向上還是向下拓展,段界限作用只是表示段邊界、大小、範圍。段界限用20個二級制位來表示。只不過此段界限只是個單位量,它的單位要麼是字節,要麼是4KB,這是由段描述符G位來表示的。最終段的邊界是此段界值 * 單位,故段的大小要麼是2的20次方等於1MB,要麼是2的32次方等於4GB。
上面所說的1MB和4GB是範圍,並不是具體的邊界值。由於段界限只是個偏移量,是從0算起的,所以實際的段界限邊界值 = (描述符中段界限+1)X (段界限粒度大小:4KB或者1)-1。
如果G位爲0,表示段界限粒度大小爲1字節,根據上面的公式,實際段界限=(描述符段界限+1)X1-1=描述符中段界限,段界限實際大小就等於描述符中段界限值。如果G位爲1同理,只是將粒度大小換成4KB。
11 ~ 8位是TYPE字段,用來指定描述符的類型。在CPU眼裏一個段描述符分爲兩大類,要麼描述的是系統段,要麼描述的是數據段。這是由段描述符S位決定的,用它指示是否是系統段。在CPU眼裏,凡是硬件運行所需要的的東西都可以稱之爲系統,凡是軟件(操作系統也屬於軟件,CPU眼中,它與用戶程序無異)需要的東西都稱之爲數據。無論是代碼,還是數據,甚至包括棧,他們都作爲硬件的輸入,都是給一硬件的數據而已,所以代碼段在段描述符也屬於數據端(非系統段)。S爲0時表示系統段,S爲1時表示數據段。type字段時要和S字段配合在一起才能確定段描述符的確切類型,只有S字段的值確定後,type字段纔有意義。
什麼是系統段
各種稱爲"門"的結構便是系統段,也就是硬件系統需要的結構,非軟件使用的,如調用門、任務門。簡而言之,門的意思就是入口,它通往一段程序。
type字段一共4爲,用於表示內存段或門的子類型。說明見下圖。
在這裏插入圖片描述
表中Accessed位,由CPU來設置,每次倍CPU訪問後,CPU就將此位置置爲1。創建一個新描述符是,應該將此位置置0。在調試時可以判斷該描述符是否可用。
C表示一致性代碼段,也稱爲依從代碼段,Conforming。一致性代碼段是指如果自己是轉移的目標段,並且自己是一致性代碼段,自己的特權級一定要高於當前特權級,轉移後的特權級不與自己的DPL爲主,而時與轉移前的低特權級一致,也就是聽從、依從賺一千的低特權級。C爲1時則表示該段是一致性代碼段,C爲0時表示該段爲非一致性代碼段。
R表示可讀,R爲1表示可讀,R爲0表示不可讀。這個屬性一般用來限制代碼段的訪問。這個屬性一般用來限制代碼段的訪問。如果指令執行過程中,CPU發現某些指令對R爲0的段進行訪問,如果使用段前綴CS來訪問代碼段,CPU將拋出異常。不可讀代碼段只是來限制代碼指令的,並不是連CPU也不能看。CPU可以看任何位置的數據。
X表示該段是否可執行,EXecutable。我們所說的指令和數據,在CPU眼裏是沒有任何區別的,都是類似010101的二級制數據。所以要用type中的X位來標識出是否是可執行的代碼。代碼段是可執行的,即爲1。而數據端是不可執行的,即X爲0。
E是用來標識段的拓展方向的,Extend。E爲0表示向上拓展,即地址越來越高。E爲1表示向下拓展,地址越來越低。
W是指段是否可寫,Writable。W爲1表示可寫,通常用於數據段。W爲0表示不可寫入,通常用於代碼段。
描述符第12位是S字段,用來指出當前描述符是否是系統段。S爲0表示系統段,S爲1表示非系統段。
段描述符的第13 ~ 14位是DPL字段,Descriptor Privilege Level,即描述符特權級這個是保護模式提供的安全解決方案,將計算機世界按權力劃分成不同等級,每種等級稱爲一種特權級。
由於段描述符用來描述一個內存段或一段代碼的情況(若描述符類型爲"門"),所以段描述符中的DPL是指所代表的內存段的特權級。
這兩位能表示4種特權級,分別是0、1、2、3級特權,數字越小特權越大。特權級是保護模式下才有的東西(也是CPU運行在保護模式下的必須物,不是操作系統的必須物,操作系統只是利用了這種機制進行了一定的拓展,比如Windows操作系統只有0、3特權級,並沒有使用所有的特權級)因爲保護模式下的代碼已經是操作系統的一部分,所以操作系統應該處於最高特權級0。用戶程序通常處於3特權級,權限最小。某些指令只能在0特權級下運行,從而保證了安全。
段描述符第15位是P字段,Present,即段是否存在。如果段存在內存中,P爲1,否則P爲0。P字段是由CPU來檢查的,如果爲0,CPU將拋出異常,轉到相應的異常處理程序,此異常處理程序是由我們自己來實現的,在異常處理完成後要將P置爲1。也就是說,對於P字段,CPU只負責檢查,我們程序員進行賦值。不過通常情況下段,段都是在內存中。當初CPU的設計是當內存不足時,可以將段描述符對應的內存段換出,也就是把不用的段直接換出到硬盤,待使用時再加載進來。現在即使內存不足時,也沒有把整個段換出,現在都是平坦模式,一般情況下,段都要4GB大小,而且這些平坦的段都是公用的,換出去佔空間也容易出問題。所以這是未開啓分頁時的解決方案,保護模式下有分頁功能,可以按頁(4KB)的單位來將內存換入換出。
段描述符的第20位爲AVL字段,從名字上看它是AVaiLable,可用的。不過這個“可用的”是對用戶來說的,也就是操作系統可以隨意用此位。對硬件來說,它沒有專門的用途,就當作是硬件給軟件的饋贈吧。
段描述符的第21位爲L字段,用來設置是否是64位代碼段。L爲1代表64位代碼段,否則表示32位代碼段。這目前屬於保留位,在我們32位CPU下編程將其設置爲0即可。
段描述符的第22位是D/B字段,用來指示有效地址(段內偏移地址)及操作數的大小,我們可能有這樣的疑問,實模式已經是32位了的地址線和操作數了,難道操作數不是32位嗎?其實這是爲了兼容286的保護模式,286的保護模式下的操作數是16位。既然是指定“操作數”的大小,也就是對指令來說的,與指令相關的內存段是代碼段和棧段,所以此字段是D或B。
對於代碼段來說,此位是D位,若D位爲0,表示指令中有效地址和操作數是16位,指令有效地址用IP寄存器。若D爲1,表示指令中的有效地址及操作數是32位,指令有效地址用EIP寄存器。
對於棧段來說,此位是B位,用來指定操作數大小,此操作數涉及到的指針寄存器的選擇及棧的地址上線。若B位0,使用的是sp寄存器,也就是棧的起始地址是16位寄存器的最大尋址範圍,0xFFFF。若B爲1,使用的是esp寄存器,也就是棧的起始地址是32爲寄存器的最大尋址範圍,0xFFFFFFFF。
段描述符的第23位是G字段,Granularity,粒度,用來指定段界限的單位大小。所以此位是用來配合段界限的,它與段界限一起來決定段的大小。若G爲0,表示段界限的單位是1字節,這樣段最大是2的20次方X1字節,即1MB。若G爲1,表示段界限的單位是4KB,這樣段的最大時2的20次方X4KB字節,即4GB。

0x3.2 全局描述符表GDT、局部描述符表LDT及選擇子

一個段描述符只用來定義(描述)一個內存段。代碼段要佔用一個段描述符、數據端和棧段等,多個內存段也要各自佔用一個描述符,這些段描述符放在全局段描述符表,就是GDT(Global Desriptor Table)。全局描述符表GDT相當於是描述符數組,數組中每個元素都是8字節的描述符。可以用選擇子中提供的下標在GDT中索引描述符。
將該表稱爲全局描述符表,其全局性體現在多個程序都可以在裏面定義自己的段描述符,是公用的,全局描述符表在內存裏,需要用專門的寄存器指定它,CPU才知道他在哪裏。這個專門的寄存器就是GDTR,即GDT Register。專門用來存儲GDT的內存地址及大小。GDTR是個48位的寄存器,如下圖。
在這裏插入圖片描述
對此寄存器的訪問不能用mov gdtr,有專門的指令爲它進行初始化,這就是lgdt指令。雖然我們是爲了進入保護模式纔用到該指令對GDTR寄存器初始化,但實際上,此指令在保護模式下也能執行。進入保護模式需要有GDT,但進入保護模式後,還可以重新更換GDT加載。在保護模式下重新更換GDT的原因是實模式下只能訪問1MB空間,所以GDT只能位於1MB之間。根據操作系統的真實情況,有可能把GDT放在其他內存位置,所以在進入保護模式後,訪問內存空間突破了1MB,可以將GDT放在合適的位置重新加載進來。
lgdt的格式是:lgdt 48位內存數據
這48位內存劃分爲兩部分,其中前16位是GDT以字節爲單位的界限值。後32位是GDT的起始地址。由於GDT界限大小是16位2進制,其表示範圍是2的16次方等於65536字節。每個描述符大小是8字節,故GDT中最多可容納的描述符是65536/8=8192個,即GDT中最多容納8192個段或門。
段描述符與內存之間的關係如下圖。
在這裏插入圖片描述
有了段描述符和段描述符表,接下來該引出段選擇子的概念。
段寄存器CS、DS、ES、FS、GS、SS,在實模式下,段中存儲的是段基址,即內存段的起始地址。而在保護模式下時,由於段基址已經存入在段描述符中,所以段寄存器中再存放段基址是沒有意義的,在段寄存器中存儲的是一個叫選擇子的東西——selector。選擇子“基本上”是個索引值,這裏說的是基本上,其中還有其他屬性。用此索引值在段描述符表中索引相應的段描述符,這樣,便在段描述符中得到了內存段的起始地址和段界限值等相關信息。
在這裏插入圖片描述
由於段寄存器是16位,所以選擇子也是16位,在其低2位即第0 ~ 1位,用來存儲RPL,即請求特權級,可以表示0、1、2、3四種特權級。在選擇子的第2位是TI位,即Table Indicator,用來表示訓責子實在GDT中,還是LDT中索引描述符。TI位0表示在GDT中,TI爲1表示在LDT中索引描述符。
由於選擇子的索引值部分是13位,即2的13次方是8192,故最多可以索引8192個段,這和GDT中最多定義8192個描述符是吻合的。
選擇子的作用主要是確定段描述符,確定描述符的目的,一是爲了特權級、界限等安全考慮,最主要還是爲了確定段基址。
注意GDT的第0個段描述符是不可用的,原因是定義在GDT中的段描述符是要用選擇子來訪問的,如果使用的選擇子忘記初始化,選擇子的值便會是0,這邊會訪問到第0個段描述符。爲了避免出現這種因忘記初始化選擇子而選擇到第0個描述符,處理器將會發出異常。
局部段描述符表,叫做LDT,Local Descriptor Table,它是CPU廠商爲在硬件一級原生支持多任務而創造的表,按照CPU的設想,一個任務對應一個LDT,其實在操作系統中很少用LDT的,作者書中的LDT也沒有用到。
CPU廠商建議每個任務的私有內存段都存到自己的段描述符表中,該表就是LDT即每個任務都有自己的LDT,隨着任務的切換,也要切換任務的LDT。LDT位於內存中,其他地址需要先被加載到某個寄存器後,CPU才能使用LDT,該寄存器是LDTR,即LDT Register。同樣也有專門的指令用於載入LDT,即lldt。以後每切換任務時,都要用lldt指令重新加載任務的私有內存段。
回顧之前的type字段,LDT段類型屬於系統段。LDT雖然是個表,但也是一片內存區域,所以也需要描述符在GDT中先註冊。段描述符是需要選擇子去訪問的。
lldt指令格式lldt 16位寄存器/16位內存
無論是寄存器,還是內存,其內容一定是一個選擇子,該選擇子用來在GDT中索引LDT的段描述符。
在LDT被加載到ldtr寄存器後,之後再訪問某個段時,選擇子中的TI位若爲1,就會用該選擇子中的高13位在ldtr寄存器所指向的LDT中去索引相應段描述符。
LDT中的段描述符和GDT中的一樣,與GDT中不同的是LDT中的第0個段描述符是可以使用的。因爲提交的選擇子中的TI位,TI位用於指定是GDT,還是LDT,TI爲1則表示在LDT中索引段描述符,即TI爲1必然是經過顯式初始化的結果,完全排除了忘記初始化的可能。

0x3.2 打開 A20 地址線

爲了讓“段基址:段內偏移”策略繼續可用,CPU採取的做法是將超過1MB的部分自動繞回到0地址,繼續從0地址開始映射。相當於把地址對1MB求模。超過1MB多餘出來的內存被稱爲高端內存區HMA。
這種地址繞回的做法需要通過兩種情況分別討論:
對於只有20位地址線的CPU,不需要任何操作便能自動實現地址繞回。
地址(Address)線從0開始編號,在8086/8088中,只有20位地址線,即A0~A19。20位地址線表示的內存是2的20次方,最大爲1MB,即0x0 ~ 0xFFFFF。若內存超過1MB,是需要第21根地址線支持的。所以,若地址位進到1MB以上,如0x100000,由於沒有第21根地址線,相當於丟掉了進位1,變成了0x00000。如下圖
在這裏插入圖片描述
當其他有更多地址總線的時候,因爲CPU可以訪問更多的內存,所以不會產生地址回滾。這種情況下的解決方案就是對第21根地址線進行操作。
如果開啓了第21根線A20,則可以正常訪問超過1MB的數據。
如果關閉了A20,則物理上就相當於沒有21根線往後的所有總線,因爲爲了迎合CPU,地址線都是串口順序輸入輸出。所以A20線斷了後面的線也就訪問不到了。打開A20Gate的方式很簡單,將端口0x92的第1位置置1就可以了,作者給出了以下三個步驟

in al,0x92
or al,0000_0010B
out 0x92,al

0x3.3 保護模式的開關,CR0 寄存器 PE 位

CRx系列寄存器屬於控制寄存器一類。控制寄存器是CPU的窗口,既可以用來展示CPU內部的狀態,也可以用於控制CPU的運行機制。我們需要用到的是CR0寄存器的第0位,即PE位,Protection Enable,此位用於啓動保護模式,是保護模式的開關。打開此位後,CPU才能真正的進入保護模式,這是進入保護模式三步中的最後一步。CR0寄存器如下圖。
在這裏插入圖片描述
作者爲了照顧我這種有強迫症的同學還特意列出所有字段含義,真的用心了。
在這裏插入圖片描述
PE爲0表示在實模式下運行,PE爲1表示在保護模式下運行。所以,我們的任務是將此位置設置爲1。

mov eax,cr0
or eax,0x00000001
mov cr0,eax

0x3.4 進入保護模式

保護模式是在我們之前寫的加載器中進入的也就是loader.bin中進入,除了源程序loader.S要更新,還需要更新2個相關文件。
第一個是mbr.S,由於loader.bin超過了512字節,所以我們要把mbr.S加載loader.bin的讀入扇區數目增大。
在這裏插入圖片描述
除了修改mbr.S的讀取扇區數外,還需要修改包含文件boot.inc中的配置信息,修改內容如下

;--------------------- loader 和 kernel---------------------

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;--------------------  gdt 描述符屬性  ----------------------
DESC_G_4K         equ 1_00000000000000000000000b
DESC_D_32         equ  1_0000000000000000000000b
DESC_L            equ   0_000000000000000000000b		;64位代碼標記,此處標記爲0便可
DESC_AVL          equ    0_00000000000000000000b
DESC_LIMIT_CODE2  equ     1111_0000000000000000b
DESC_LIMIT_DATA2  equ     DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ      0000_000000000000000b		;CPU不用此位,暫置爲0
DESC_P			  equ         1_000000000000000b
DESC_DPL_0        equ          00_0000000000000b
DESC_DPL_1		  equ          01_0000000000000b
DESC_DPL_2        equ		   10_0000000000000b
DESC_DPL_3        equ          11_0000000000000b
DESC_S_CODE		  equ            1_000000000000b
DESC_S_DATA       equ            DESC_S_CODE
DESC_S_sys        equ            0_000000000000b
DESC_TYPE_CODE    equ             1000_00000000b		;x=1,c=0,r=0,a=0 代碼段是可執行的,非依從的,不可讀的,已訪問位a清0.  
DESC_TYPE_DATA    equ             0010_00000000b		;x=0,e=0,w=1,a=0 數據段是不可執行的,向上擴展的,可寫的,已訪問位a清0.

DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b

;--------------   選擇子屬性  ---------------
RPL0  equ   00b
RPL1  equ   01b
RPL2  equ   10b
RPL3  equ   11b
TI_GDT	 equ   000b
TI_LDT	 equ   100b

修改完相關文件後接下來就是實際代碼進入保護模式

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start

;構建gdt以及內部的描述符
GDT_BASE: dd 0x00000000
		  dd 0x00000000
CODE_DESC: dd 0x0000FFFF
		   dd DESC_DATA_HITH4
DATA_STACK_DESC: dd 0x0000FFFF
				 dd DESC_DATA_HIGH4

VIDEO_DESC: dd 0x80000007;limit=(0xbffff-0xb8000)/4k=0x7
			dd DESC_VIDEO_HIGH4;此時dpl爲0
GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times dq 60		;此處預留60個描述符空位
SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0	;相當於(CODE_DESC-GDT_BASE)/8+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

;以下是gdt的指針,前2字節是gdt界限,後4字節是gdt起始地址
gdt_ptr dw GDT_LIMIT
		dd GDT_BASE
loadermsg db '2 loader in real.'
loader_start:
;---------------------------------------------------------
;INT 0x10	功能號:0x13	功能描述符:打印字符串
;---------------------------------------------------------
;輸入:
;AH 子功能號=13H
;BH = 頁碼
;BL = 屬性(若AL=00H或01H)
;CX = 字符串長度
;(DH,DL)=座標(行,列)
;ES:BP=字符串地址
;AL=顯示輸出方式
;0——字符串中只含顯示字符,其顯示屬性在BL中。顯示後,光標位置不變
;1——字符串中只含顯示字符,其顯示屬性在BL中。顯示後,光標位置改變
;2——字符串中只含顯示字符和顯示屬性。顯示後,光標位置不變。
;3——字符串中只含顯示字符和顯示屬性。顯示後,光標位置改變。
;無返回值
mov sp,LOADER_BASS_ADDR
mov bp,loadermsg		;ES:BP=字符串地址
mov cx,17				;CX=字符串長度
mov ax,0x1301			;AH=13,AL=01h
mov bx,0x001f			;頁號爲0(BH=0)藍底分紅子(BL=1fh)
mov dx,0x1800
int 0x10

;---------------------準備進入保護模式-------------------------
;1 打開A20
;2 加載gdt
;3 將cr0的pe位置1

;-----------------------打開A20--------------------------
in al,0x92
mov or al,0000_0010B
out 0x92,al
;-----------------------加載GDT--------------------------
lgdt [gdt_ptr]

;----------------------cr0 第 0 位置 1-------------------
mov eax,cr0
or eax,0x00000001
mov cr0,eax

jmp dword SELECTOR_CODE:p_mode_start		;刷新流水線

[bits 32]
p_mode_start:
	mov ax,SELECTOR_DATA
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov esp,LOADER_STACK_TOP
	mov ax,SELECTOR_VIDEO
	mov gs,ax
	mov byte [gs:160],'P'
	jmp $

將mbr.S和loader.S重新編譯成2進制文件,並且覆蓋原來的扇區位置。注意這裏loader.bin編譯後爲615個字節,需要2個扇區大小,寫入磁盤時要給count賦值爲2,不要忘了。
在這裏插入圖片描述
我們觀察下各個寄存器的值,首先看gdt。我們代碼中設置的段描述符的屬性都已經成功設置進入了段寄存器中,第0個段選擇子用來排錯所有位均爲0;第2個是代碼段,我們採用了平坦模式,將整個4GB空間都設置成代碼段,屬性可執行非依從;數據段也是平坦模式,屬性可讀可寫。注意,這裏棧段和數據段用的是一個段描述符,所以堆棧也是向上拓展的;最後一個是顯存段,因爲我們要在保護模式下顯示信息,但這裏沒有再採用平坦模式,因爲文本顯存的空間是0xb8000 ~ 0xbffff。直接通過段描述符將這段範圍規定好。
在這裏插入圖片描述
查看PE位,我們可以看到CR0的PE位被設置爲1,此後就是保護模式的天下了
在這裏插入圖片描述
還記得我們最後進入保護模式前還要刷新下流水線嗎?
流水線式CPU爲提高效率採取的一種工作方式,CPU當前指令及後面幾條指令同時放在流水線中重疊執行。由於在實模式下時,按照16位指令格式來譯碼,而進入保護模式的loader.bin是既包含16位指令,又有32位指令的,所以CPU按照32位譯碼16位指令就會出錯。解決這個問題的方法就是用無條件跳轉指令進行流水線刷新(《x86從實模式到保護模式》中提到過,CPU被設計成,每次執行跳轉指令就會清空當前執行機器碼的流水線進行重新加載,大家有興趣可以看下)。
刷新完流水線後就可以按照32位方式對各個寄存器重新初始化了,作者代碼中直接使用了之前構造好的段選擇子進行初始化。
萬事俱備,看下程序執行的效果。
在這裏插入圖片描述
在這裏插入圖片描述

0x3.5 使用遠跳指令清空流水線,更新段描述符緩衝寄存器

以下指令我們剛剛使用過,是一個標準的遠跳指令。

jmp dowrd SELECTOR_CODE:p_mode_start

首先我們要明確,段描述符緩衝寄存器是CPU爲了提升訪問內存速度要使用的緩衝寄存器,無論實模式還是保護模式都要使用它來提升地址訪問速度。不重新引用一個段時,它是不會主動更新的。無論實模式還是保護模式下CPU都以段描述符緩衝寄存器的內容爲主。
實模式進入到保護模式時,由於段描述符緩衝寄存器的內容僅僅是實模式下的20位的段基址,很多屬性位都是錯誤的值,這對於保護模式必定會造成錯誤,所以需要馬上更新下段描述符緩衝寄存器,也就是想辦法往相應段寄存器中加載段選擇子。
其次,流水線中指令譯碼錯誤。
在默認情況下,如果未使用bits僞指令來設置運行環境,編譯器就將代碼按照16位實模式編譯。進入保護模式前,SRAM中都是它預測的,將來要執行的16位指令,所以進入32位保護模式下需要使用jmp指令無條件刷新流水,重新加載32位譯碼出的指令。

0x4 保護模式之內存段的保護

0x4.1 向段寄存器加載段選擇子時的保護

當引用一個內存段時,實際上就是往段寄存器中加載個段選擇子,爲了避免非法引用內存段的情況,在這時候,處理器會在以下方面做出檢查。
勾線根據選擇子的值驗證段描述符是否超越界限。
選擇子的高13位時段描述索引值,第0 ~ 1位是RPL,第2位是TI位。首先處理器得保證段選擇子是正確的,判斷標準是選擇子的索引值一定要小於等於段描述符表(GDT或LDT)中段描述符個數。像數組下標一樣,不能越界。也就是說,段描述符最後1字節一定要在段描述符表的界限地址之內。每個段描述符的大小是8字節,所以在往段寄存器中加載段選擇子時,處理器要求段選擇子的索引值要滿足以下表達式:

描述符表基地址+選擇子中的索引值*8+7<=描述符表基地址+描述符表界限值

段描述符中還有一個type字段,用來表示段的類型,也就是不同的段有不同的作用。在段選擇自檢查過後,就要檢查段的類型了。
檢查原則大致如下:
1)只有具備可執行屬性的段(代碼段)才能加載到CS寄存器裏
2)只具備執行屬性的段(代碼段)不允許被加載到除CS外的段寄存器中
3)只有具備可寫屬性的段(數據段)才能被加載到SS寄存器中
4)至少具備可讀屬性的段才能加載到DS、ES、FS、GS寄存器中
在這裏插入圖片描述
檢查完type後,還會再檢查描述符中的P位來確認內存段是否存在,如果P位存在,P=1,這時候就可以將選擇子載入寄存器了,同時段描述符緩衝寄存器也會更新爲選擇子對應的段描述符內容,隨後處理器將段描述符中的A位置設置爲1,表示已經訪問過了。如果P位爲0,表示該內存段不存在,不存在的原因可能是由於內存不足,操作系統將該段移出內存轉儲到硬盤,這時候處理器會拋出異常,自動轉去執行相應的異常處理程序,異常處理程序將段從硬盤加載到內存後並將P置爲1,隨後返回。CPU繼續執行剛纔的操作,判斷P位。
上述涉及到的P位,其值由軟件(通常是操作系統)來設置,由CPU來檢查。A位由CPU來設置。

0x4.2 代碼段和數據段的保護

對於代碼段和數據段來說,CPU每訪問一個地址,都要確認該地址不能超過其所在內存段的範圍。
實際段界限值爲:

(描述符中段界限+1*(段界限的粒度大小:4KB或者1-1

對於G位爲1的4k(0x1000)中以0爲起始的最後一字節。所以此充實的意義是以0爲起始的段偏移量,即段界限。
實際的段界大小,是段內最後一個可訪問的有效地址。由於有了段界的限制,我們給CPU提交的每一個內存地址,無論是指令地址還是數據的地址,CPU都要幫我們檢查地址的有效性。首先地址指向的數據是有寬度的,CPU要保證該數據一定要落在段內,不能騎在段邊界上。
訪問內存時只需要:

EIP中的偏移地址+指令長度-1<=實際段界限大小

如果未小於,則指令未完整的落在段內,CPU會拋出異常。
在這裏插入圖片描述

0x4.3 棧段保護

雖然段描述符中type的e位用來表示段的拓展方向,但它和別的描述符屬性一樣,僅僅時用來描述段的性質,即使e等於1向下拓展,依然可以引用不斷向上遞增的內存地址,即使e等於0向上拓展,也依然可以引用不斷向下遞減的內存地址。棧頂指針[e]wp的值逐漸降低,這是push指令的作用與描述符是否向下拓展無關,也就是說,數據段就可以用作棧。
CPU對數據段的檢查,其中一項就是看地址是否超越界限。如果向上拓展的數據段用作棧,拿CPU將按照上一節提到的的數據段方式檢查改段。如果向下拓展的段做棧就會出現以下檢測機制
1)對於向上拓展的段,實際段界限是段內可以訪問的最後一個字節。
2)對於向下拓展的段,實際段界限是段內不可以訪問的第一個字節。

實際段界限+1<=esp-操作數大小<=0xFFFFFFFF
發佈了16 篇原創文章 · 獲贊 1 · 訪問量 1103
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章