記憶體保護模式
★PART1:進入保護模式
1. 全域性描述符表(Global Descriptor Table,GDT)
32位保護模式下,如果要使用一個段,必須先登記,登記的資訊包括段的起始地址,段的界限和各種訪問屬性,如果偏移地址超過了段的界限,就會引發異常中斷。和一個段有關的資訊需要8個位元組來描述,這被稱為段的描述符(Segement Descriptor),每個段都需要一個描述符,為了存放描述符,需要在記憶體中開闢一段空間。這些描述符集中存放,構成了一個描述符表。
為了跟蹤全域性描述符表,處理器內部有一個48位的暫存器,稱為全域性描述符暫存器(GDTR)。這個暫存器分為兩個部分,前16位為全域性描述表的邊界,後32位為全域性描述表的線性基地址。GDT的界限是16位的,所以GDT的最大是216
因為進入保護模式之前,是真實模式,真實模式只能訪問最多1MB的記憶體,所以GDT通常都是定義在1MB以下的記憶體範圍中,允許進入保護模式之後換個地方定義GDT。
因為在進入保護模式之前處理器初始化為真實模式,同樣的,主載入程式是在0x0000:0x7c00這個地方載入的,而主載入程式最大也就512位元組,所以我們把GDT放在主載入程式之後,也就是0x7e00後面,GDT的界限可以到0x17DFF(64KB最大)。
棧段暫存器SS被初始化為0x0000,棧是向下拓展的,我們可以把SP段定義到0x7c00處,然後向下拓展(要注意大小不能太大,因為BIOS資料區和中斷向量表都在下面)。
描述符不是由使用者程式自己建立的,都是由作業系統建立的,如果使用者程式對段的訪問超過了作業系統所規定的範圍,或者訪問了一個不屬於它的段,那麼這些操作都會被處理器阻止。
2. 32位保護模式下GDT每個位置的定義
每個描述符在GDT佔8個位元組,每個位置及其定義見下圖:
在32位保護模式下,如果未開始頁功能,那麼段地址就是實體地址,注意描述符段儲存器的段界限和基地址是不連續的(其實是為了相容80286)。另外需要注意的是,段基地址最好是16位元組對齊,雖然80386原則上是可以選取任何位置作為段基地址的,但是如果不進行16位元組對齊,那麼將會對效能產生很大的影響。因為32位CPU的匯流排是32位的,所以CPU一次會從記憶體中讀取32個位元組的資料(而且是從地址4倍數的位元組開始讀,比如0x00,0x04,0x08….等4倍數地址),然後進行剔除資料不要的部分,然後進行拼合,位移到暫存器裡,這個過程是很花費時間的,但這是為了保證傳輸到暫存器裡的內容總是正確的。讀取一個字的時候總是可以正確讀到,但是讀取兩個字的時候就可能跨越4倍數的界限,然後CPU必須讀取48位數(花費兩個時鐘週期),才能正確讀取資料。(注意CPU的16位元組對齊和程式設計時候的align16是不一樣的,程式設計時的align16是直接在記憶體上組織資料,和CPU讀取資料無關。)
GDT每個位置的意義如下:
G位(Granularity,粒度):
這個位用於解釋界限的含義,當G=0,段界限是以位元組為單位的,這個時候,段的拓展範圍是1B-1MB(段描述符的段界限是20位的);如果G=1,則段界限是以4KB為單位的,範圍4KB-4GB。
S位(Descriptor Type,描述符型別):
當S=0,說明這是一個系統段,當S=1,說明這是一個程式碼段或者是資料段(棧段是特殊的資料段)。
DPL位(Descriptor Privilege Level,DPL特權級)
32位處理器的DPL位有四種,分別是0,1,2,3(就是特權級0123),不同特權級的程式是相互隔離的,其訪問是嚴格限制的,而且有些處理器指令(特權指令)只能由0特權級的程式來執行。在這裡,DPL直的是訪問該段所必須擁有的最低特權級,如果這裡的數值是2,那麼特權級0,1,2可以訪問這個段,特權級3訪問這個段會被處理器阻止。
P位(Segement Present,存在位)
P位用於描述描述符對應的段是否存在,一般來說,描述符所指示的段都是存在於記憶體中的,但是,當記憶體空間緊張的時候,有可能只是建立了描述符,對應的記憶體空間並不存在,這個時候就應該把P位清零。表示段並不 存在,另外,同樣是在內從空間緊張的情況下,會把很少用到的段換出到硬碟中,騰出空間給當前急需記憶體的程式使用(當前正在執行的),這時,同樣要把段的描述符P位清零,當再次輪到它執行時,P=1.
P位是處理器負責檢查的,每當通過描述符訪問記憶體的段中時,如果P位是0,則處理器會產生一個異常中斷,通常,這個中斷處理過程是作業系統提供的。該處理過程的任務是負責將該段從硬碟中換回記憶體,並將P=1。在多使用者,多工的系統中,這是一種常用的虛擬記憶體排程策略。
D/B位(Default Operation Size,預設的運算元大小)
對於程式碼段,當D=0表示指令的偏移地址或者運算元是16位的;D=1表示偏移地址或者運算元是32位的.
對於棧段,當D=0,表示使用SP暫存器,棧段上界是0xFFFF;當D=1,表示使用ESP暫存器,棧段上界是0xFFFFFFFF。
L位(64-bit Code Segement)
這個位用於保留給64位處理器使用,當L=0,表示是32位處理器,當L=1,表示是64位處理器。
TYPE位(描述符類別)
X表示是否可執行(eXecutable)。資料段總是不可執行的,X=0,程式碼段可執行,X=1。
E是對資料段而言,指示段的擴充套件方向,E=0表示向上拓展,E=1表示向下拓展。
W段指示讀寫屬性,W=0指示不可寫入,否則會引發處理器異常中斷,W=1表示允許寫入。
對程式碼段而言,C表示是否特權級依從(Conforming),C=0表示非依從的程式碼段,這樣的程式碼段可以和他特權級相同的程式碼段呼叫。或者通過們呼叫,C=1表示允許從低特權級的程式轉移到該段執行。程式碼段總是可以 執行,但是總是不允許被修改(如果要修改程式碼段,可以指定一個可以讀寫的資料段指向這個程式碼段)。,至於能不能讀出,由R位決定,R=0表示不能讀出,R=1表示可以讀出。(相當於一個ROM)(R位不是指示處理器能 否讀取指令的,而是限制程式和指令的行為,比如使用超越字首CS:訪問程式碼段的內容)。
A位是已訪問位(Assessed),指示這個段最近有沒有被訪問過,在描述符建立後,這個位置應該被置零。之後,每當這個段被訪問的時候,處理器都會將這個位變成1,對這個位清零是作業系統做的,通過定期監視該段 的位置,可以統計出該段的使用頻率,當記憶體空間緊張的時候,可以不經常用的段退到硬碟上,從而實現虛擬記憶體管理。
AVL位(Available)
通常由作業系統用,處理器並不會使用它。
3. 安裝儲存器的段描述符並載入到GDTR中
處理器規定,全域性描述表的第一個表必須是0,相當於是NULL
在這裡,lgdt就是load GDT的簡稱,意圖也很明顯,就是載入GDT的意思,這個指令的運算元是一個48位的記憶體單位,低16位是GDT的界限,高32位是GDT的線性基地址,這個指令的運算元在16位模式下是16位的,在32位保護模式下是32位的,這個指令在保護模式和16位模式都可以執行。(注意段界限一定是大小-1,不要錯了)。
在初始化狀態下,GDTR的基地址被初始化為0x00000000,界限被初始化為0xFFFF,這個指令不會影響任何標誌位。
注意:在進入主載入程式的時候,段暫存器和GDTR的內容和處理器剛加電的時候不再相同。因為BIOS加電自檢程式在執行的時候要進入保護模式,進行相應的測試,這會改變相關段的內容。
4. 關於A20(第21個地址線)開啟問題
8086只有20根地址線,只能訪問1MB的記憶體,到了80386有32根地址線,這裡就會出現一個問題,在8086時代,很多程式都會利用20位地址迴繞特性(當實體地址超過0xFFFFF就會迴繞到0x00000),而到了80286以後,由於地址線加多了,這個進位不會被丟棄,所以就會引發很多問題。
Intel想了一個方法,他們在80286和80386在A20處使用一個與門控制,並且把這個與門的控制閥門放在鍵盤上,埠號是0x60,向這個埠寫入資料的時候,如果這個第一位是1,那麼鍵盤控制器通向與門的輸出就是1,與門的輸出決定於A20是0還是1(在真實模式下,只要強制與門的輸出為0,那麼真實模式的迴繞特性將會被保留)。
我們先來看這種老式方法的操作,程式碼From: http://hengch.blog.163.com/blog/static/107800672009013104623747/
這種方法非常麻煩,後來到了80486,這個問題被得到簡化。在80486以後,處理器本身就有了A20M#引腳(A20 Mask,A20遮蔽),這個引腳低電平有效。在ICH上,有一個用於相容老式裝置的埠0x92,第7-2位保留,第0位叫做INIT_NOW,用於初始化處理器,當它從0到1過渡的,ICH會使INIT#引腳電平變為低電平有效,並保持至少16個PCI時鐘週期,也就是說,如果向0x92寫入1,那麼就會讓處理器復位,導致計算機強制重啟。
當INIT_NOW從0到1,ALT_A20_GATE將會被置為1,這就是說,計算機啟動的時候,第21個根引線是自動啟用的(但是A20#M是僅用於單處理器系統,多核系統一般是不用的)。現在基本都是USB裝置了。
快速開啟A20的方法,非常簡單。
5. 32位保護模式下的記憶體訪問
要開啟保護模式,除了載入GDT,開啟A20還不夠,我們必須還要對CR0開關進行操作,CR0也是一個處理器內部的控制暫存器(Control Register,RD)。這樣的控制器還有CR1,CR2,CR3等。CR0是一個32位的暫存器,他的第一位(0位)是保護模式允許位(Protection Enable,PE),如果把這個位置為1,那麼處理器將會進入保護模式,按保護模式的規則開始執行。在保護模式下,真實模式下的中斷向量表不再適用,且我們不能再使用BIOS中斷,這就是為什麼我們之前要把中斷關掉的原因。
在8086下,執行到第三行時,處理器先將ds的內容左4位,然後加上偏移地址0xc0,然後再把al的內容寫入,實際寫入的內容的位置是0x200c0。
在32位處理器下的真實模式下,首先如果處理器要引用一個段(也就是執行將段地址傳到段暫存器的指令),處理器會自動將段地址左移4位,然後傳到描述符快取記憶體器,這以後,就一直使用描述符快取記憶體器的內容作為段地址。只要不改變段暫存器DS的內容,以後每次訪問記憶體都直接使用DS描述符快取記憶體器中的內容,在真實模式下段暫存器只能傳送16位的邏輯地址。(這個時候處理器不會把他看成是段的選擇子),處理器也只能訪問1MB的記憶體。
PS:這裡書上有句話是錯的,作者說快取記憶體器是32位的,顯然是錯的,用bochs一看就知道是64位的,而且即使在真實模式下,描述符快取記憶體器的各個位置的定義都是一樣的,並不存在像書上說的那樣會把高位填充為0,看圖。)
(dl和dh分別是描述符快取記憶體器的低32位和高32位)
而在32位處理器下的保護模式下,傳入段暫存器的內容不再是邏輯地址,而是段的選擇子,所謂段的選擇子,其實就是段描述符在描述符表(GDT,LDT)的索引號。
第一部分(0~1)是RPL特權級,表示給出當前選擇該選擇子的那個程式的特權級,第二部分是TI(2)(Table Indicator),當TI=0,表示描述表在GDT中;當TI=1,表示描述符在LDT中。第三部分(3~15)是描述符索引號,這個部分是隻有13位的,正好和213=8192個描述符對應。
比如,我們要載入第一個在GDT中的段,可以這麼寫:
這表示我們想載入在GDT的第一個段,特權級是0
GDT的線性基地址在GDTR中,每個描述符佔用8個位元組,黨處理器在執行改變段選擇器的指令的時候,就將指令中的索引號乘以8得到偏移地址,和GDTR中的線性地址相加,以此訪問GDT,處理器會根據GDT的界限以及特權級檢查,如果沒有問題,那麼處理器就會將在對應描述符的內容的一部分(線性基地址,段界限和段的訪問屬性)載入到快取記憶體中。此後,每當有訪問記憶體的指令,就不會再訪問GDT的描述符,而是直接用當前段暫存器的快取記憶體的內容提供線性基地址。,訪問程式碼段遺失一樣如此訪問的(EIP+快取記憶體中的線性基地址)。
6. 清空流水線並且序列化處理器
在進入保護模式之前的最後一個步驟,就是要清空流水線,因為在真實模式下,快取記憶體器也被用來直接訪問記憶體,但是這些內容在保護模式下是無效的;並且,在進入保護模式之前,已經有很多指令進入流水線了,在真實模式下他們都是按照16位運算元或者16位地址長度編譯的,即使用bits32編譯的指令,進入保護模式之後,因為CS的描述符快取記憶體中還有真實模式殘留的內容,可能會導致指令執行結果不正確,並且亂序執行得到的中間結果也是無效的,所以我們必須在進入保護模式之前把CS,SS,DS,ES,FS和GS的內容,包括段選擇器和描述符快取記憶體器的內容清除。
建議的做法就是在設定了CR0的PE位後,立馬使用直接遠轉移指令jmp,當處理器遇到jmp時,一般會清空流水線,並且序列化執行。不僅如此,CS還會被重新載入,描述符快取記憶體器的內容會被重新整理。
當然也可以使用dword來描述偏移地址,這樣的話flush對應標號有所不同(因為偏移地址和段的選擇子的長度變了,變成32位,不加dword這兩個長度都是16位),但是不影響執行。
需要注意的是,在保護模式下,不允許直接用mov指令改變段暫存器CS的內容,企圖這樣操作會引發無效操作碼的異常中斷。
注意:在跳轉指令之前,處理器雖然進入了保護模式,但是,這個時候描述符快取記憶體器的內容沒有被重新整理,但是處理器任然是可以繼續執行下去的,因為檢查描述符是否有效,通常是在載入段暫存器(選擇器),並重新整理描述符快取記憶體器的時候進行的,比如jmp 0x0008:flush這條指令,而對於資料段來說,是載入段選擇子的時候,比如mov ds,cx,但是現在因為是剛進入保護模式,描述符的很多位,是在真實模式下都是無效的。
如圖,在執行跳轉指令之前,CS是個資料段,這顯然是錯的,描述符裡面的資料只是真實模式遺留下來的而已。只有跳轉指令執行後,CS的描述符快取記憶體器的內容才會被重新整理。
★PART2:進入保護模式例程
;---------------------保護模式主引導扇區程式--------------------- mov ax,0x00 mov ss,ax mov sp,0x7c00 mov ax,[cs:gdt_base+0x7c00] mov dx,[cs:gdt_base+0x7c00+0x02] mov bx,0x10 div bx mov ds,ax ;得到base基地址,讓ds指向這個地址 mov bx,dx ;得到偏移地址 ;---------------------安裝描述符--------------------- ;描述符0 mov dword [ebx+0x00],0x00 ;第一個描述符必須是0 mov dword [ebx+0x04],0x00 ;描述符1 mov dword [ebx+0x08],0x7c0001FF mov dword [ebx+0x0c],0x00409800 ;基地址0x00007c00,段界限0x001FF,粒度是位元組, ;長度是512位元組,在記憶體中的32位段,特權級為0,只能執行的程式碼段 ;描述符2 mov dword [ebx+0x10],0x8000FFFF mov dword [ebx+0x14],0x0040920B ;基地址0x000B8000,段界限0x0FFFF,粒度是位元組, ;長度是64KB,在記憶體中的32位段,特權級為0,可以讀寫的向上拓展的資料段 ;描述符3 mov dword [ebx+0x18],0x00007A00 mov dword [ebx+0x2c],0x00409600 ;基地址0x00000000,段界限0x07A00,粒度是位元組, ;在記憶體中的32位段,特權級為0,可以讀寫的向下拓展的棧段 mov word [cs:gdt_size+0x7c00],31;寫入GDT段界限,4個描述符是32個位元組,所以界限就是31 lgdt [cs:gdt_size+0x7c00] ;load gdt mov dx,0x92 ;南橋ICH晶片內的埠0x92 in al,dx or al,0x02 out dx,al ;開啟A20 cli ;關閉中斷 mov eax,cr0 or eax,0x01 mov cr0,eax ;設定PE位,處理器進入保護模式 ;保護模式 jmp 0x0008:flush ;現在是在16位保護模式下,0x0008依然是段的選擇子,而flush則是偏移地址 [bits 32] flush: mov cx,0x0010 mov ds,cx ;以下在螢幕上顯示"Protect mode OK." mov byte [0x00],'P' mov byte [0x02],'r' mov byte [0x04],'o' mov byte [0x06],'t' mov byte [0x08],'e' mov byte [0x0a],'c' mov byte [0x0c],'t' mov byte [0x0e],' ' mov byte [0x10],'m' mov byte [0x12],'o' mov byte [0x14],'d' mov byte [0x16],'e' mov byte [0x18],' ' mov byte [0x1a],'O' mov byte [0x1c],'K'
hlt
;------------------------------------------------------------------------------- gdt_size dw 0 gdt_base dd 0x00007e00 ;GDT的實體地址,主引導扇區是512個位元組,這個地址剛好在主引導扇區之後 times 510-($-$$) db 0 db 0x55,0xaa