Orange's一個操作系統的實現:保護模式

概述

在計算機加電之後,bios檢查硬件,並且把第一個扇區中的bootloader代碼加載到0000: 07c00h處,開始執行bootloader代碼.bootloader主要做兩件事情:
- 從實模式進入保護模式
- 從硬盤(或者其他)中讀取OS kernel到內存的固定位置處,然後跳轉到OS中執行.

這裏先討論如何從實模式進入保護模式
參考鏈接:
- 學堂在線 - 清華大學OS課程

先讓代碼跑起來

  1. 先下在freedos,並將其中的a.img複製到代碼在的工作目錄,改名爲freedos.img.下載鏈接:freedos
  2. 用bximg生成軟盤映像,pm.img
  3. 修改bochsrc,添加下面三行
floppya: 1_44=freedos.img, status=inserted
floppyb: 1_44=pm.img, status=inserted
boot: a
#  相當於有兩個軟盤,軟盤a作爲bootloader,引導操作系統啓動,軟盤b裏面放置我們自己
# 的對操作系統的操作(相當於操作系統內核)
  1. 啓動Bochs,待FreeDos其中之後,使用format b:命令格式化B:盤
  2. 把代碼編譯成.com文件,nasm pmtest1.asm -o pmtest1.com
  3. 把pmtest1.com複製到虛擬軟盤pm.img上:一種拷貝方法,值得學習
sudo mkdir /mnt/floppy
sudo mount -o loop pm.img /mnt/floppy
sudo cp pmtest1.com /mnt/floppy
sudo umount /mnt/floppy
  1. 現在我們的.com文件已經拷貝到軟盤pm.img中了,在freedos中使用B:\pmtest1.com即可執行

GDT(Global Descriptor Table)

參考:GDT,LDT,GDTR,LDTR 詳解,包你理解透徹

GDT就是一個裝有段描述符的大數組.在GDT中每一個段描述符佔用8字節,段描述符最主要的包含兩個信息:

  • 段的起始地址(段基址)
  • 段的大小(段界限)
    代碼段和數據段描述符如下所示:
    在這裏插入圖片描述
    GDT表的基地址存放在GDTR寄存器中.GDTR是一個48位寄存器,其低16位保存GDT表的長度,高32位保存表的基址.所以GDT表的最大長度是1M,因爲每一個描述符是8字節,所以一共可以存放213個段描述符,但是注意:第一個描述符爲空,不使用

實模式下的尋址方式爲:
在這裏插入圖片描述
保護模式下,段寄存器實際存放的是選擇子,選擇子實際上就是GDT表的index.所以在保護模式下,段寄存器就相當於一個指針.在這裏插入圖片描述
在下圖中,邏輯地址由一個16位的段寄存器和32位EIP寄存器指出.根據段寄存器中的值(選擇子,index),在GDT中找到段描述符(描述了段的基址和界限),線性地址=基址+EIP.需要注意的是:在沒有頁機制的情況下,線性地址和物理地址是等價的
在這裏插入圖片描述
GDT基本工作原理

選擇子(Selector)

選擇子的結構如圖:
在這裏插入圖片描述
包括3個部分:

  • 描述符索引(index):表示所需要的段的描述符在描述符表的位置,由這個位置再根據在GDTR中存儲的描述符表基址就可以找到相應的描述符。
  • TI:段選擇子中的TI值只有一位0或1,0代表選擇子是在GDT選擇,1代表選擇子是在LDT(Local Descriptor Table)選擇。
  • 請求特權級(RPL):代表選擇子的特權級,共有4個特權級(0級、1級、2級、3級).0代表最高級(操作系統在0級),3代表最低級(應用程序在3級).任務中的每一個段都有一個特定的級別。每當一個程序試圖訪問某一個段時,就將該程序所擁有的特權級與要訪問的特權級進行比較,以決定能否訪問該段。系統約定,CPU只能訪問同一特權級或級別較低特權級的段。

一個栗子
例如給出邏輯地址:21h:12345678h轉換爲線性地址

  • 選擇子SEL=21h=0000000000100 0 01b 他代表的意思是:選擇子的index=4即100b選擇GDT中的第4個描述符;TI=0代表選擇子是在GDT選擇;左後的01b代表特權級RPL=1
  • OFFSET=12345678h若此時GDT第四個描述符中描述的段基址(Base)爲11111111h,則線性地址=11111111h+12345678h=23456789h

段描述符屬性

在這裏插入圖片描述

  • P位:存在位,P=1表示段在內存中存在;P=0表示段在內存中不存在.
  • DPL描述特權級.特權級可以是0,1,2,3.數字越小特權級越大
  • S位指明描述符是數據段/代碼段描述符(S=1)還是系統段/門描述符
  • TYPE
    在這裏插入圖片描述
  • G位段界限粒度位.G=0時段界限粒度爲字節;G=1時段界限粒度爲4KB
  • D/B位
    在這裏插入圖片描述
  • AVL位保留位,可以被系統軟件使用

代碼理解

  1. 問題1:爲什麼還要初始化32位代碼段描述符??????
    ; 初始化 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
  1. 問題2:計算描述符的宏爲什麼這樣寫?
; 宏 ------------------------------------------------------------------------------------------------------ 
; 
; 描述符 
; 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 字節 
  1. 問題3:爲什麼界限=長度-1??
GdtLen		equ	$ - LABEL_GDT	; GDT長度
GdtPtr		dw	GdtLen - 1	; GDT界限

解釋:段界限表達的是段內的最大偏移,而不是段的最大長度。 這個偏移從0開始計數,有點類似於C數組中的index.一個簡單的例子,假如一個段有如下內存:
var1 db 0x01 ; 偏移0
var2 db 0x02 ; 偏移1
那麼段界限應該是1還是2呢?答案是1,最大偏移是1。
訪問段中數據使用:段基址 + 偏移(所以段界限說明的是這個最大偏移)
參考:段界限爲什麼要減1

保護模式進階

這一章主要研究如何從保護模式跳回實模式
問題1:爲啥段描述符的基址都是0????

; GDT
;                            段基址,        段界限 , 屬性
LABEL_GDT:         Descriptor    0,              0, 0         ; 空描述符
LABEL_DESC_NORMAL: Descriptor    0,         0ffffh, DA_DRW    ; Normal 描述符
LABEL_DESC_CODE32: Descriptor    0, SegCode32Len-1, DA_C+DA_32; 非一致代碼段, 32
LABEL_DESC_CODE16: Descriptor    0,         0ffffh, DA_C      ; 非一致代碼段, 16
LABEL_DESC_DATA:   Descriptor    0,      DataLen-1, DA_DRW    ; Data
LABEL_DESC_STACK:  Descriptor    0,     TopOfStack, DA_DRWA+DA_32; Stack, 32 位
LABEL_DESC_TEST:   Descriptor 0500000h,     0ffffh, DA_DRW
LABEL_DESC_VIDEO:  Descriptor  0B8000h,     0ffffh, DA_DRW    ; 顯存首地址
; GDT 結束

下面的代碼展示瞭如何從保護模式進入實模式
我們可以看到,前面那個jmp並沒有直接跳轉到實模式,而是選擇跳轉到一個保護模式下的16位代碼段,在16位保護模式代碼段下的jmp才真正跳入到實模式。

那麼,你也許會問,爲什麼要這麼麻煩,我們在實模式下不是直接從16位代碼段跳到32位代碼段的保護模式下了麼,那爲什麼從保護模式就不能直接跳回去呢?

弄明白這個問題很重要,下面我們慢慢詳解。

我們還是先來搞清楚一個概念:段描述符高速緩衝寄存器

在實模式下,段寄存器含有段值,爲訪問存儲器形成物理地址時,處理器引用相應的某個段寄存器並將其值乘以16,形成20位的段基地址。在保護模式下,段寄存器含有段選擇子,如上所述,爲了訪問存儲器形成線性地址時,處理器要使用選擇子所指定的描述符中的基地址等信息。爲了避免在每次存儲器訪問時,都要訪問描述符表而獲得對應的段描述符,從80286開始每個段寄存器都配有一個高速緩衝寄存器,稱之爲段描述符高速緩衝寄存器或描述符投影寄存器,對程序員而言它是不可見的。每當把一個選擇子裝入到某個段寄存器時,處理器自動從描述符表中取出相應的描述符,把描述符中的信息保存到對應的高速緩衝寄存器中。此後對該段訪問時,處理器都使用對應高速緩衝寄存器中的描述符信息,而不用再從描述符表中取描述符。

這些高速緩衝寄存器在實方式下仍發揮作用,只是內容上與保護模式下有所不同。如上表所示,其中“Y”表示“是”; “N”表示“否”;“B”表示字節;“U”表示向上擴展,“W”表示以字方式操作堆棧。段基地址仍是 32位,其值是相應段寄存器值(段值)乘以16,在把段值裝載到段寄存器時刷新。由於其值是16位段值乘上16,所以在實模式下基地址實際上有效位只有 20位。每個段的32位段界限都固定爲0FFFFH,段屬性的許多位也是固定的。所謂固定是指在實方式下不可設置這些屬性值,只能繼續沿用保護方式下所設置的值。因此,在準備結束保護模式回到實模式之前,要通過加載一個合適的描述符選擇子到有關段寄存器,以使得對應段描述符高速緩衝寄存器中含有合適的段界限和屬性。GDT中的描述符Normal就是這樣一個描述符,在返回實模式之前把對應選擇子Normal加載到DS和ES就是此目的。

CS段的段描述符高速緩衝寄存器的D位屬性值爲1表示代碼當前運行在保護模式,D位屬性值爲0表示代碼當前運行在實模式。而這個D位屬性值在實模式下是固定的,也就是不容許改變的,只能在保護模式下改變。我們從上一篇日誌看到,進入保護模式的標誌是cr0的PE位置1,此時CPU就運行在了保護模式下,因此可以直接加載32位代碼段的描述符,同時改變CS段描述符高速緩衝寄存器的D位屬性值,這就是爲什麼可以直接從16位實模式代碼段跳轉到32位保護模式代碼段的原因,因爲跳轉後的保護模式可以改變D位屬性值。但是反過來就出問題了,由於實模式下D位屬性值不容許改變,只能在保護模式下的時候就提前將D位屬性值置0。那怎麼置呢?我們想到一個方法就是先跳轉到16位保護模式代碼段,這樣就可以置D位屬性值爲0,然後再由此跳入實模式。至此,這個問題我們算是弄明白了~

; 16 位代碼段. 由 32 位代碼段跳入, 跳出後到實模式
[SECTION .s16code]
ALIGN	32
[BITS	16]
LABEL_SEG_CODE16:
	; 跳回實模式:
	mov	ax, SelectorNormal
	mov	ds, ax
	mov	es, ax
	mov	fs, ax
	mov	gs, ax
	mov	ss, ax

;ES 附加段寄存器
;CS 代碼段寄存器
;SS 堆棧段寄存器
;DS 數據段寄存器
;FS 附加段寄存器
;GS 附加段寄存器

	mov	eax, cr0
	and	al, 11111110b
	mov	cr0, eax

LABEL_GO_BACK_TO_REAL:
	jmp	0:LABEL_REAL_ENTRY	; 段地址會在程序開始處被設置成正確的值

Code16Len	equ	$ - LABEL_SEG_CODE16
; END of [SECTION .s16code]

再來看看下面這一段代碼,這個jmp實現了從保護模式跳轉到實模式.首先,這個jmp是一個段間跳轉,佔用5字節(jmp1字節,段地址2字節(:前面的部分),偏移量2字節(:後面的部分)).指令圖示
在這裏插入圖片描述
編譯之後,指令的"Segment"部分就是0000,但是在運行過程中,存放此指令的內存單元被mov [LABEL_GO_BACK_TO_REAL+3], ax修改了,即此時"Segment"部分放置的是ax寄存器中的值.

LABEL_GO_BACK_TO_REAL:
	jmp	0:LABEL_REAL_ENTRY	; 段地址會在程序開始處被設置成正確的值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章