ASM:《X86匯編語言-從實模式到保護模式》第11章:進入保護模式


★PART1:進入保護模式

1. 全局描述符表(Global Descriptor Table,GDT)

       32位保護模式下,如果要使用一個段,必須先登記,登記的信息包括段的起始地址,段的界限和各種訪問屬性,如果偏移地址超過了段的界限,就會引發異常中斷。和一個段有關的信息需要8個字節來描述,這被稱為段的描述符(Segement Descriptor),每個段都需要一個描述符,為了存放描述符,需要在內存中開辟一段空間。這些描述符集中存放,構成了一個描述符表。

       為了跟蹤全局描述符表,處理器內部有一個48位的寄存器,稱為全局描述符寄存器(GDTR)。這個寄存器分為兩個部分,前16位為全局描述表的邊界,后32位為全局描述表的線性基地址。GDT的界限是16位的,所以GDT的最大是216字節,又因為一個描述符是8個字節的,所以最多可以定義8192個描述符。

       因為進入保護模式之前,是實模式,實模式只能訪問最多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+0x1c],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

 

 

 


免責聲明!

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



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