歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

一個操作系統的實現(2)-認識保護模式

今天開始學習intel處理器的保護模式。書的第二章

這節講述的是如何從實模式進入保護模式。用的例子是在保護模式下向屏幕上輸出字符P

如何進入保護模式呢?主要步驟如下:

0. 進入保護模式的步驟

  1. 准備GDT
  2. 用lgdt加載gdtr
  3. 打開A20
  4. 置r0的PE位位1
  5. 跳轉,進入保護模式

    下面是書的例子:

    1. 進入保護模式實例

    ; ==========================================
    ; pmtest1.asm
    ; 編譯方法:nasm pmtest1.asm -o pmtest1.bin
    ; ==========================================
    
    %include        "pm.inc"        ; 常量, 宏, 以及一些說明
    
    org     0100h
            jmp     LABEL_BEGIN
    
    [SECTION .gdt]
    ; GDT
    ;                              段基址,       段界限     , 屬性
    LABEL_GDT:         Descriptor       0,                0, 0           ; 空描述符
    LABEL_DESC_CODE32: Descriptor       0, SegCode32Len - 1, DA_C + DA_32; 非一致代碼段
    LABEL_DESC_VIDEO:  Descriptor 0B8000h,           0ffffh, DA_DRW      ; 顯存首地址
    ; GDT 結束
    
    GdtLen          equ     $ - LABEL_GDT   ; GDT長度
    GdtPtr          dw      GdtLen - 1      ; GDT界限
                    dd      0               ; GDT基地址
    
    ; GDT 選擇子
    SelectorCode32          equ     LABEL_DESC_CODE32       - LABEL_GDT
    SelectorVideo           equ     LABEL_DESC_VIDEO        - LABEL_GDT
    ; END of [SECTION .gdt]
    
    [SECTION .s16]
    [BITS   16] 
    LABEL_BEGIN:
            mov     ax, cs
            mov     ds, ax
            mov     es, ax
            mov     ss, ax
            mov     sp, 0100h
    
            ; 初始化 32 位代碼段描述符
            xor     eax, eax 
            mov     ax, cs
            shl     eax, 4
            add     eax, LABEL_SEG_CODE32
            mov     word [LABEL_DESC_CODE32 + 2], ax
            shr     eax, 16
            mov     byte [LABEL_DESC_CODE32 + 4], al
            mov     byte [LABEL_DESC_CODE32 + 7], ah
    
            ; 為加載 GDTR 作准備
            xor     eax, eax
            mov     ax, ds
            shl     eax, 4
            add     eax, LABEL_GDT          ; eax <- gdt 基地址
            mov     dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
    
            ; 加載 GDTR
            lgdt    [GdtPtr]
    
            ; 關中斷
            cli
    
            ; 打開地址線A20
            in      al, 92h
            or      al, 00000010b
            out     92h, al
    
            ; 准備切換到保護模式
            mov     eax, cr0
            or      eax, 1
            mov     cr0, eax
    
            ; 真正進入保護模式
            jmp     dword SelectorCode32:0  ; 執行這一句會把 SelectorCode32 裝入 cs,
                                            ; 並跳轉到 Code32Selector:0  處
    ; END of [SECTION .s16]
    
    
    [SECTION .s32]; 32 位代碼段. 由實模式跳入.
    [BITS   32]
    
    LABEL_SEG_CODE32:
            mov     ax, SelectorVideo
            mov     gs, ax                  ; 視頻段選擇子(目的)
    
            mov     edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
            mov     ah, 0Ch                 ; 0000: 黑底    1100: 紅字
            mov     al, 'P'
            mov     [gs:edi], ax
    
            ; 到此停止
            jmp     $
    
    SegCode32Len    equ     $ - LABEL_SEG_CODE32
    ; END of [SECTION .s32]

    用到的Descriptor在pm.inc中定義,關於Descriptor定義的內容如下:

    ; 描述符
    ; usage: Descriptor Base, Limit, Attr
    ;        Base:  dd
    ;        Limit: dd (low 20 bits available)
    ;        Attr:  dw (lower 4 bits of higher byte are always 0)
    %macro Descriptor 3
            dw      %2 & 0FFFFh                             ; 段界限1
            dw      %1 & 0FFFFh                             ; 段基址1
            db      (%1 >> 16) & 0FFh                       ; 段基址2
            dw      ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)     ; 屬性1 + 段界限2 + 屬性2
            db      (%1 >> 24) & 0FFh                       ; 段基址3
    %endmacro ; 共 8 字節
    

    剛開始看到上面的代碼,我有點束手無策。因為也是最近才開始學習匯編,上面的程序我連指令都認不全。所以下面這一節對上面程序中語法的部分做一些講解

    2. 關於實例中匯編語法的講解

    %include "pm.inc":包含文件。類似c語言中的包含.h文件。

    org 07c00h:org是origin的縮寫。告訴編譯器下一條匯編語句的偏移地址是07c00h。

    [SECTION .gdt]:AT&T匯編語言格式,用於定義一個節。這裡是定義一個結構體數組,數組名稱是GDT,數組內部是三個Descriptor結構。

    LABEL_GDT: Descriptor 0, 0, 0:Descriptor是在pm.inc中定義的宏,8個字節。上面有列出來內部的定義。個人猜測定義中的%1、%2、%3是這裡傳進去的參數,按照位置分別是1、2、3。猜測跟shell中的位置參數類似。(現在先猜測一下,到影響繼續學習的時候再深究)。上面定義的Descriptor這個宏能夠用比較自動化的方法把段基址、段界限和段屬性安排在一個描述符中合適的位置。這兒也不是很了解,不知道自動化是如何實現的

    GdtLen equ $ - LABEL_GDT:equ是偽指令。這句話的意思是用GdtLen來代替$ - LABEL_GDT。從這兒看類似於c語言中的define。

    GdtPtr          dw      GdtLen - 1      ; GDT界限
                    dd      0               ; GDT基地址

    這裡定義一個結構體數組GdtPtr,共有6個字節。前2字節(處於低位)是GDT的界限;後4字節(處於高位)是GDT的基地址。

    SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT:這句話定義GDT的選擇子(sector)SelectorCode32。在下面講解GDT裡面會有詳細介紹。

    繼續向下看

    [BITS 16]:用來指明此節一個16位代碼段。

    lgdt [GdtPtr]:lgdt指令用來將GdtPtr這個結構體裝入寄存器GDTR中

    cli:關中斷,對應的開中斷指令是sti

    這裡面還出現了新的寄存器eax,下面的圖說明了eax,ax,ah,al的關系:

    00000000 00000000 00000000 00000000
    |===============EAX===============|--32個0,4個字節,2個字,1個雙字
                      |======AX=======|--16個0,2個字節,1個字
                      |==AH===|        --8個0,1個字節
                              |===AL==|--8個0,1個字節

    eax是32位的寄存器,但它實際上只是在原有的8086CPU的寄存器ax上增加了一倍的數據位數而已。所以eax和ax二者並不是獨立的,而是整體與部分的關系。舉例來說,對eax直接賦值,若更改了低16位自然會改變了ax值,同樣ax又會影響eax整體。而ah,al寄存器和ax之間的關系也是如此。同樣還有ebx,ecx,edx。(上面摘抄互聯網並作了一些補充)

    IA32還加了兩個段寄存器fs與gs。用來減緩es的壓力。用法與es相同。

    上面這些指令比較生疏。其他的指令在學習8086匯編的時候都是學過,比較熟悉。

    那指令都認識了,但是對於上面代碼的運行還是一頭霧水,接下來對代碼的含義進行分析。

    3. 關於實例如何實現實模式到保護模式的切換

    看上面的代碼,

    程序首先被加載到內存07c00h處,然後直接跳轉到LABEL_BEGIN處。

    在LABEL_BEGIN處,程序首先使ds和es寄存器指向與cs相同的段,上節裡面說了,這是為了以後進行數據操作的時候能定位到正確的位置。然後初始化棧。

    接下來初始化32位代碼段描述符(32位代碼段就是指程序最下面的[SECTION .s32])。這段初始化代碼就是將下面那個32位代碼段基址寫到GDT中對應的描述符結構中。你看GDT結構體中LABEL_DESC_CODE32那一項的段界限與屬性都定義好了,只有段基址沒有定義,上面關於初始化32位代碼描述符的作用就是初始化描述符中的基址。

    要說上面的這步是干什麼的,那麼首先需要了解IA32的尋址過程了,下面有詳細的介紹。不了解的需要先跳到下面關於GDT的講解,再回來繼續看。

    到這裡,GDT已經初始化好了,接下來的lgdt [GdtPtr]是把GDT的基地址和段界限加載到GDTR這個寄存器中。看看上面的GdtPtr結構體,它可不是隨意定義的。它的結構與gdtr寄存器的結構是相同的,看看下面gdtr的結構,在對比上面介紹的GdtPtr,你就知道了。

    
    
    
    	
    	32位基址
    	16位界限
    	
    
    
    H-------------------------------------------------------------------------L
    

    再下面是關中斷,因為進入保護膜是之後中斷處理機制與現在是不同的,所以在進入之前需要關中斷。如果不關中斷將會出現錯誤。

    關中斷之後的代碼就是純粹為了進入保護模式做准備的了。這裡主要有兩個步驟:

    首先打開地址線A20。關於A20,書上是這樣說的:

    那麼什麼是A20呢?這又是一個歷史問題。8086中,“段:偏移”這樣的模式能表示的最大內存是FFFF:FFFF,即10FFEFh。可是8086只有20位的地址總線,只能尋址到1MB,那麼如果試圖訪問超過1MB的地址時會怎樣呢?實際上系統並不會發生異常,而是回卷(wrap)回去,重新從地址零開始尋址。可是,到了80286時,真的可以訪問到1MB以上的內存了,如果遇到同樣的情況,系統不會再回卷尋址,這就造成了向上不兼容,為了保證百分之百兼容,IBM想出一個辦法,使用8042鍵盤控制器來控制第20個(從零開始數)地址位,這就是A20地址線,如果不被打開,第20個地址位將會總是零。顯然,為了訪問所有的內存,我們需要把A20打開,開機時它默認是關閉的。這裡打開A20的方式是讓92h這個端口的第1位(從低位0開始)的值置為1

    接下來將cr0這個寄存器的第0位置為1。為什麼要這麼做呢?這是因為當該位為0時,CPU運行於實模式,為1時,運行於保護模式。所以當將cr0的第0位置1之後,我們就相當欲閉合了進入保護模式的開關。

    也就是說,“mov cr0, eax”這一句之後,系統就運行於保護模式之下了。但是,此時cs的值仍然是實模式下的值,我們需要把代碼段的選擇子裝入cs。所以,我們需要第71行的jmp指令:

    jmp dword SelectorCode32:0
    根據尋址機制我們知道,這個跳轉的目標將是描述符DESC_CODE32對應的段的首地址,即標號LABEL_SEG_CODE32處。

     

    到這裡,執行jmp指令後,就真正進入了保護模式。

    進入保護模式後,就開始運行[SECTION .s32]段的代碼。這段代碼比較簡單:就是在屏幕的第12行80列輸出一個紅色的P,然後進入無線循環。

    至此,整個程序運行完畢。

    上面的介紹中只是粗略的講了一下GDT,下面對IA32為什麼引入GDT進行詳細介紹。

    4. GDT(Global Descriptor Table)

    如果你熟悉Intel 8086匯編,那麼你一定知道Intel 8086是16位的CPU,它有著16位的寄存器(Register)、16位的數據總線(Data Bus)以及20位的地址總線(Address Bus)和1MB的尋址能力。一個地址是由段和偏移兩部分組成的,物理地址遵循這樣的計算公式:

    物理地址(Physical Address)=段值(Segment)×16+偏移(Offset)

    其中,段值和偏移都是16位的。

    從80386開始,Intel家族的CPU進入32位時代。80386有32位地址線,所以尋址空間可以達到4GB。所以,單從尋址這方面說,使用16位寄存器的方法已經不夠用了。這時候,我們需要新的方法來提供更大的尋址能力。

    在實模式下,16位的寄存器需要用“段:偏移”這種方法才能達到1MB的尋址能力,如今我們有了32位寄存器,一個寄存器就可以尋址4GB的空間,是不是從此段值就被拋棄了呢?實際上並沒有,新政策下的地址仍然用“段:偏移”這樣的形式來表示,只不過保護模式下“段”的概念發生了根本性的變化。實模式下,段值還是可以看做是地址的一部分的,段值為XXXXh表示以XXXX0h開始的一段內存。而保護模式下,雖然段值仍然由原來16位的cs、ds等寄存器表示,但此時它僅僅變成了一個索引,這個索引指向一個數據結構的一個表項,表項中詳細定義了段的起始地址、界限、屬性等內容。這個數據結構,就是GDT(還可能是LDT)。GDT中的表項也有一個專門的名字,叫做描述符(Descriptor)。

    也就是說,GDT的作用是用來提供段式存儲機制,這種機制是通過段寄存器和GDT中的描述符共同提供的。其中描述符有多種:代碼段欲數據段描述符、系統段描述、門描述符。上面的程序用到了代碼段的描述符,它的結構如下:

    \

    上面除了BYTE5和BTYE6中的一堆屬性看上去有點復雜以外,其他三個部分倒還容易理解,它們分別定義了一個段的基址和界限。不過,由於歷史問題,它們都被拆開存放。至於那些屬性,我們暫時先不管它。

    好了,我們回頭再來看看代碼,Descriptor這個宏用比較自動化的方法把段基址、段界限和段屬性安排在一個描述符中合適的位置,有興趣的讀者可以研究這個宏的具體內容。本例的GDT中共有3個描述符,為方便起見,在這裡我們分別稱它們為DESC_DUMMY、DESC_CODE32和DESC_VIDEO。其中DESC_VIDEO的段基址是0B8000h,顧名思義,這個描述符指向的正是顯存。

    現在我們已經知道,GDT中的每一個描述符定義一個段,那麼cs、ds等段寄存器是如何和這些段對應起來的呢?你可能注意到了,在[SECTION.s32]這個段中有兩句代碼是這樣的:

    mov ax, SelectorVideo
    mov gs, ax

    看上去,段寄存器gs的值變成了SelectorVideo,我們在上文中可以看到,SelectorVideo是這樣定義的:SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT。直觀地看,它好像是DESC_VIDEO這個描述符相對於GDT基址的偏移。實際上,它有一個專門的名稱,叫做選擇子(Selector),它也不是一個偏移,而是稍稍復雜一些,它的結構如圖3.5所示。

    
    
    
    	
    	15
    	14
    	13
    	12
    	11
    	10
    	9
    	8
    	7
    	6
    	5
    	4
    	3
    	2
    	1
    	0
    	
    	
    	描述符索引
    	TI
    	RPL
    	
    
    
    

    不難理解,當TI和RPL都為零時,選擇子就變成了對應描述符相對於GDT基址的偏移,就好像我們程序中那樣。這點還是不太了解

    看到這裡,你肯定已經明白了mov [gs:edi], ax的意思,gs值為SelectorVideo,它指示對應顯存的描述符DESC_VIDEO,這條指令將把ax的值寫入顯存中偏移位edi的位置。

    總之,整個尋址方式如下圖所示:
    \

    上面關於GDT的內容引用自書本。

    到這裡,整個程序講解完畢。

    書上還有關於描述符屬性的詳細解釋和突破軟盤引導512字節的限制。

Copyright © Linux教程網 All Rights Reserved