《一個操作系統的實現》筆記(2)--保護模式


保護模式

什麼實模式和保護模式

這是CPU的兩種工作模式,解析指令的方式不同。

在實模式下,16位寄存器需要通過段:偏移的方法才能達到1MB的尋址能力。
物理地址 = 段值 x 16 + 偏移
此時段值還可以看成地址的一部分,段值爲XXXXh表示以XXXX0h開始的一段內存。

在保護模式下,CPU有着巨大的尋址能力,併爲操作系統提供了虛擬內存和內存保護。
雖然物理地址的仍然用上面的公式表示,但此時“段”的概念發生了變化,它變成了一個索引,指向一個數據結構的一個表項,表項中詳細定義了段的起始地址、界限、屬性等內容。這個數據結構就是GDT(全局符號表), 其中的表項叫做描述符(Descriptor),另外有一個指向描述符的指針叫做選擇子(Selector)。

內存還是一整塊,並沒有分段,段的劃分來自於CPU,就好比劃分行政單位,是另一種維度上的劃分。


《PC 彙編語言》
在實模式下,一個段地址的值是物理內存裏的一節的首地址,在保護模式下,一個段地址的值是一個指向描述符表的指針。
保護模式使用了一種叫做虛擬內存的技術。虛擬內存的基本思想是僅僅保存程序現在正在使用的代碼和數據到內存中。其它數據和代碼暫時儲存在硬盤中直到它們再次需要時。當一段從硬盤重新回到內存中,它很有可能放在不同於它移動到硬盤之前時的位置的內存中。所有這些都由操作系統透明地執行。程序並不需要因爲要讓虛擬內存工作而使用不同的書寫方法。
在保護模式下,每一段都分配了一條描述符表裏的條目。這個條目擁有系統想知道的關於這段的所有信息。這些信息包括:現在是否在內存中; 如果在內存中,在哪;訪問權限(例如: 只讀)。段的條目的指針是儲存在段寄存器裏的段地址值。

也是在調api,只不過這個api是由處理器CPU來指定的,調用的形式類似於給寄存器、某塊指定位置的內存填二進制數據,然後調用CPU提供的指令或者中斷等觸發一下就可以了。

; 宏 -----------------------------------------------------------------------------
;
; 描述符
; 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 字節

[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]

段式存儲的機制是通過段寄存器和GDT中的描述符共同提供的。
Descriptor這個宏的作用是自動化地把段基址、段界限和段屬性安排在一個描述符中合適的位置(由於歷史原因,它們都被拆開存放了)。
代碼段和數據段描述符:
代碼段和數據段描述符
用c語言可以這樣表示:

/* 存儲段描述符/系統段描述符 */
typedef struct s_descriptor     /* 共 8 個字節 */
{
    u16 limit_low;      /* Limit */
    u16 base_low;       /* Base */
    u8  base_mid;       /* Base */
    u8  attr1;          /* P(1) DPL(2) DT(1) TYPE(4) */
    u8  limit_high_attr2;   /* G(1) D(1) 0(1) AVL(1) LimitHigh(4) */
    u8  base_high;      /* Base */
}DESCRIPTOR;

保護機制就體現在描述符的屬性段中,對特定類型段的操作能夠受到CPU的限制。
選擇子(Selector)的結構:
選擇子(Selector)的結構
因爲每個描述符佔8字節的數據大小,選擇子只需要前面15bits就能定位到一個描述符了,最小的3bits表示選擇子的屬性,有其它用途。

段式尋址:
段式尋址

如何實現由實模式到保護模式的轉換

1、準備GDT

初始化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 ; 注意物理地址的表示=段值*16+偏移,而下面的LABEL_SEG_CODE32就是代碼段的偏移
    add eax, LABEL_SEG_CODE32
    mov word [LABEL_DESC_CODE32 + 2], ax ;LABEL_DESC_CODE32就是保護模式下32位代碼段的開始處
    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 ; LABEL_GDT是在數據段上定義的,也要偏移
    add eax, LABEL_GDT      ; eax <- gdt 基地址
    mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址

2、用lgdt加載gdtr

關鍵是把GDT的物理地址填充到GdtPtr這個6字節的數據結構中,然後執行如下的指令,把這個6字節的數據加載到寄存器gdtr中。這樣CPU就能知道GDT這張表了,之後的內存尋址就會根據這張表的指示去找了。

    ; 加載 GDTR
    lgdt    [GdtPtr]

3、實模式下的善後工作和保護模式下的初始化工作。

4、跳轉,進入保護模式

;...
; 真正進入保護模式
jmp dword SelectorCode32:0  ; 執行這一句會把 SelectorCode32 裝入 cs,
                            ; 並跳轉到 LABEL_SEG_CODE32處
; END of [SECTION .s16]
;...
[SECTION .s32]; 32 位代碼段. 由實模式跳入.
LABEL_SEG_CODE32:
    mov ax, SelectorVideo
    ;...

LDT(Local Descriptor Table)

LDT跟GDT類似,都是描述表,區別僅僅在於局部(Local)和(Global)的不同。
指向LDT中描述符的選擇子的T1 位必須置1,在運用它時,需要先用lldt指令加載ldtr,其操作數是GDT中用來描述LDT的描述符。LDT相當於是GDT的二級目錄,增加了一個層次。
我們可以把一個單獨的任務所用到的所有東西封裝在一個LDT中,這種思想是多任務處理的雛形。
多任務所用的段類型如下圖,使用LDT來隔離每個應用程序任務的方法,正是關鍵保護需求之一:
多任務所用的段類型

一個LDT的例子:


[SECTION .gdt]
; GDT
;                                         段基址,       段界限     , 屬性
LABEL_DESC_CODE32: Descriptor       0,  SegCode32Len - 1, DA_C + DA_32  ; 非一致代碼段, 32
LABEL_DESC_LDT:    Descriptor       0,        LDTLen - 1, DA_LDT    ; LDT
;...
; GDT 結束

; GDT 選擇子
;...
SelectorCode32      equ LABEL_DESC_CODE32   - LABEL_GDT
SelectorLDT     equ LABEL_DESC_LDT      - LABEL_GDT
; END of [SECTION .gdt]

LABEL_BEGIN:
    ; 初始化 LDT 在 GDT 中的描述符
    ;...
    add eax, LABEL_LDT
    mov word [LABEL_DESC_LDT + 2], ax
    ;...

[SECTION .s32]; 32 位代碼段. 由實模式跳入.
LABEL_SEG_CODE32:
    ;...
    ; Load LDT
    mov ax, SelectorLDT
    lldt    ax

    jmp SelectorLDTCodeA:0  ; 跳入局部任務
;...    
; END of [SECTION .s32]

; LDT
[SECTION .ldt]
LABEL_LDT:
;                            段基址       段界限      屬性
LABEL_LDT_DESC_CODEA: Descriptor 0, CodeALen - 1, DA_C + DA_32 ; Code, 32 位

LDTLen      equ $ - LABEL_LDT

; LDT 選擇子
SelectorLDTCodeA    equ LABEL_LDT_DESC_CODEA    - LABEL_LDT + SA_TIL ; 選擇子是通過地址相減得到的,所以末尾3位都是0,可以利用起來
; END of [SECTION .ldt]

; CodeA (LDT, 32 位代碼段)
[SECTION .la]
LABEL_CODE_A:
    mov ax, SelectorVideo
    mov gs, ax          ; 視頻段選擇子(目的)
    mov edi, (80 * 12 + 0) * 2  ; 屏幕第 10 行, 第 0 列。
    mov ah, 0Ch         ; 0000: 黑底    1100: 紅字
    mov al, 'L'
    mov [gs:edi], ax
CodeALen    equ $ - LABEL_CODE_A
; END of [SECTION .la]

特權級概述–保護的意義

具體可以看《linux內核完全剖析》的4.2.3、4.5保護一節。

特權級示意圖
特權級示意圖

處理器利用特權級來防止運行在較低特權級的程序或者任務訪問具有較高特權級的一個段,除非是在受控的條件下。

CPL、DPL、RPL

處理器會對調用方的CPL、RPL跟被調用方的DPL做特權級檢查。

1、CPL(Current Privilege Level)

CPL是當前執行的程序或任務的特權級。
存儲在cs和ss第0位和第1位上。
通常情況下,CPL等於代碼所在的段的特權級。當程序轉移到不同特權級的代碼段時,處理器將改變CPL。當處理器訪問一個與CPL特權級不同的一致代碼段時,CPL將保持之前的不變。

2、DPL(Descriptor Privilege Level)

DPL表示段或者門的特權級。
存儲在段/門描述符的DPL字段中。

3、RPL(Requested Privilege Level)

存儲在選擇子的RPL字段中。
操作系統過程往往用RPL來避免低特權級應用程序訪問高特權級內的數據。

訪問數據段時的特權級檢查:
訪問數據段時的特權級檢查


特權級轉移

1、通過jmp或call進行直接轉移

通過jmp或call所能進行的代碼段間轉移是非常有限的,對於非一致代碼段,只能在相同特權級代碼段之間轉移。遇到一致代碼段也最多能從低到高,而且CPL不會改變。
如果想自由地進行不同特權級之間的轉移,顯然需要其他幾種方式,即運用門描述符或者TSS。

2、調用門

爲了對具有不同特權級的代碼段提供受控的訪問,處理器提供了稱爲門描述符的特殊描述符集。
門描述符結構,同樣是8個字節,直觀來看,一個門描述了由一個選擇子和一個偏移所指定的線性地址,程序正是通過這個地址進行轉移的,這裏的S=1表示此描述符是門,而不是普通的數據/代碼段描述符。
門描述符
描述符分爲以下4種:
- 調用門,TYPE=12;
- 陷阱門,TYPE=15;
- 中斷門,TYPE=14;
- 任務門,TYPE=5;
其中,調用門用於在不同特權級之間實現受控的程序轉移。
Linux內核中並沒有用到調用門。
門調用過程:
門調用過程
通過調用門進行控制轉移的特權級檢查:
通過調用門進行控制轉移的特權級檢查
通過調用門和call指令,可以實現從低特權級到高特權級的轉移,無論目標代碼段是一致的還是非一致的。

3、關於堆棧

  • 短調用:在段內跳轉
  • 長調用:在段間跳轉
    call指令是會影響堆棧的,不同於jmp的是,call就像調用一個函數,也會返回的,長調用和短調用對堆棧的影響是不同的。

短調用時堆棧示意:
短調用時堆棧示意
短調用返回時堆棧示意:
短調用返回時堆棧示意

長調用時的堆棧示意:
長調用時的堆棧示意
長調用返回時的堆棧示意:
長調用返回時的堆棧示意

在調用門,堆棧發生了切換,call指令執行前後的堆棧已經不再是同一個了。Intel提供了一種機制,將堆棧A的諸多內容複製到堆棧B中,這裏參數的複製就是由Param Count一項來決定的,有特權級變換的轉移時堆棧如下圖,
有特權級變換的轉移時堆棧變換

4、進入ring3–由高特權級進入低特權級

我們手動將ring3的cs、eip等信息壓棧,然後執行ret指令就可以轉移到低特權級代碼中了

5、進入ring0–從低特權級進入高特權級

從低特權級到高特權級轉移時需要用到TSS,跟LDT的用法類似,在初始化TSS結構之後,調用ltr指令讓CPU加載它,之後再進入ring3。當在ring3中使用調用門進入ring0時,CPU就會去之前設置的TSS結構中找不同特權級的堆棧等信息了。


內存尋址

內存是指一組有序字節組成的數組,每個字節有唯一的內存地址。內存尋址是指對存儲在內存中的某個指定數據對象進行定位。

地址變換

地址變換能夠讓操作系統在給任務分配內存時具有靈活性,並且因爲我們可以讓某些物理地址不被任何邏輯地址所映射,所以在地址變換過程中同時提供了內存保護功能。
分段和分頁操作都使用駐留在內存中的表來指定它們各自的變換信息。這些表只能由操作系統訪問,以防止應用程序擅自修改。

虛擬地址(邏輯地址)到物理地址的變換過程如下圖,如果沒有啓用分頁機制,那麼分段機制產生的線性地址空間就直接映射到處理器的物理地址空間上。
圖4-4 虛擬地址(邏輯地址)到物理地址的變換過程

邏輯地址、線性地址和物理地址之間的變換如下圖。分段提供了一種機制,用於把處理器可尋址的線性地址空間劃分成一些較小的稱爲段的受保護地址空間區域。段可以用來存放程序的代碼、數據和堆棧,或者用來存放系統數據結構(例如TSS或LDT)。
圖4-5 邏輯地址、線性地址和物理地址之間的變換
當使用分頁時,每個段被劃分成頁面(通常每頁爲4KB大小),頁面會被存儲於物理內存中或硬盤上。操作系統通過維護一個頁目錄和一些頁表(存放在物理內存的某個位置)來留意這些頁面。當程序試圖訪問線性地址空間中的一個地址位置時,處理器就會使用頁目錄和頁表把線性地址轉換成一個物理地址,然後在該內存位置上執行所要求的讀寫操作。
如果當前被訪問的頁面不在物理內存中,處理器就會中斷程序的執行(通過產生一個頁錯誤異常)。然後操作系統就可以從硬盤上把該頁面讀入物理內存中,並繼續執行剛纔被中斷的程序。

不同進程可以有相同的邏輯地址,原理就是在任務切換時通過改變cr3的值來切換頁目錄,從而改變地址映射關係。

系統的內存分佈

可以利用中斷15h得到機器的內存信息,調用的結果是BIOS會填充es:di指向的一塊內存,此結構成爲ARDS。
地址範圍描述符結構(Address Range Descriptor Structure)
地址範圍描述符結構
ARDS之Type:
ARDS之Type
一個可能的內存分佈:
內存分佈
由於歷史原因,系統可用的內存分佈得並不是連續的。


中斷和異常機制

在實模式下能用的BIOS中斷在保護模式下已經不能用了,實模式下的中斷向量表被保護模式下的IDT所代替。
IDT的作用是將每一箇中斷向量和一個描述符對應起來。
聯繫調用門我們知道,其實中斷門或者任務門的作用機理幾乎是一樣的,只不過使用調用門時使用call指令,而這裏我們使用int指令。
中斷過程調用:
中斷過程調用

中斷門和陷阱門的結構:
中斷門和陷阱門的結構


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章