天天看點

作業系統開發:編寫開機引導

作業系統是用來管理與協調硬體工作的,開發一款作業系統有利于了解底層的運轉邏輯,本篇内容主要用來了解作業系統是如何啟動的,又是如何加載磁盤中的核心的,該系列文章參考各類底層書籍,通過自己的了解并加以叙述,讓内容變得更加簡單,一目了然,即可學到知識又能提高自己的表述能力。

作業系統是用來管理與協調硬體工作的,開發一款作業系統有利于了解底層的運轉邏輯,本篇内容主要用來了解作業系統是如何啟動的,又是如何加載磁盤中的核心的,該系列文章參考各類底層書籍,通過自己的了解并加以叙述,讓内容變得更加簡單,一目了然,即可學到知識又能提高自己的表述能力。

該系列文章是在學習《作業系統真相還原》時通過閱讀後簡化并适當描述整理的學習筆記,首先,緻敬作者鄭剛博士,在讀本書時能深刻的感覺到作者寫書時一絲不苟的态度,書很厚寫的,講解細緻幽默,很能讓人願意繼續讀下去,同時也不得不佩服作者計算機底層功力的深厚。

本文章隻是學習筆記,并非原創作品,你可以任意轉載,請保留原作者(鄭剛)版權資訊。

1. BOIS 是如何蘇醒的

BIOS 基本輸入輸出系統,BIOS代碼所做的工作是一成不變的,是以他是被固化到ROM中的一塊隻讀區域中,在開機時此ROM會被映射到低端1MB記憶體的頂部,原因是系統在開啟時預設是實位址模式(該模式最大尋址範圍0-fffff),是以其尋址範圍也就被限制在了

0xF0000-x0xFFFFF

區域中,這64KB的記憶體就是BIOS的執行代碼.

在開機的一瞬間,CPU的

CS:IP

寄存器會被強制初始化為

0xF000:0xFFF0

,在實位址模式下該位址需要乘以16也就是左移四位加上偏移位址得到,于是

0xF000:0xFFF0

就等效于

0xFFFF0

此處的位址距離

0xFFFFF

隻有16個位元組的空間,裡面存放着一條

jmp far f000:e05b = fe05b

的彙編指令,該指令将跳轉到真正的BIOS開始的位置.

接着BIOS将會通過自身的代碼對硬體進行自檢測,在初始化硬體後,則開始向記憶體

0x000-0x3ff

中初始化資料結構以及拷貝中斷向量表,緊接着BIOS将會通過調用

int 19h

中斷,此中斷用以檢測計算機中的硬碟,如果檢測到0盤0道1扇區末尾的兩個位元組是

0x55,0xaa

則認為此扇區确實存在,于是就會将此區域中的内容,加載到記憶體

0x7c00

的位置,并通過一條

jmp far 0:0x7c00h

的指令跳轉到該位置執行,這樣BIOS就将CPU控制權交給了MBR了,而BIOS将會再次睡去.

2. MBR 繼續執行引導

如上提到過的

0x7c00

就是MBR代碼的開始位置,之是以是

0x7C00

是因為,DOS中要求最小記憶體是

32KB

而MBR大小必須是512位元組,是以選擇

32kB

中的最後1KB的位置最為合适

(32KB(0x8000)-1KB(0x400)=>0x7c00)

,這就是

0x7C00

的由來,同時還需要保證第

510-511

位元組必須為

0x55,0xaa

才可以,這就需要在末尾部分自動補齊兩位元組的填充.

簡單的引導MBR的代碼如下,首先我們需要先初始化每個段寄存器

DS,ES,SS,FS,SP

然後通過調用兩次

int 0x10

中斷對指令行進行置空操作,最後通過

mov ax,01301h

也就是13号中斷,列印出字元串.

SECTION MBR vstart=0x7c00     ; 告訴編譯器加載到7c00記憶體處
        mov ax,cs
        mov ds,ax
        mov es,ax
        mov ss,ax
        mov fs,ax
        mov sp,0x7c00

        mov ax,0x600      ; 清屏範圍,也就是寬度
        mov bx,0x0
        mov cx,0x0        ; 清屏 左上角(0,0)
        mov dx,0x184f     ; 清屏 右下角(80=0x4f,25=0x18)

        int 0x10
        mov dh,0x0        ; 設定光标列号
        mov dl,0x0        ; 設定光标行号
        mov bh,0x0        ; 頁碼
        int 0x10

        mov ax,Message
        mov bp,ax         ; 儲存字元串位址
        mov cx,15         ; 儲存字元串長度
        mov ax,01301h     ; 子功能号13是顯示字元及屬性
        mov bx,000ch      ; 頁号位0,使用黑色為背景色,紅色為字型顔色
        mov dl,0
        int 10h           ; 調用10h号中斷,用來顯示字元
        hlt
        ret

Message: db "hello lyshark !"
times 510-($-$$) db 0  ; 填充剩餘的510位元組的空間為0
db 0x55,0xaa           ; mbr的結束标志
           

我們直接将其儲存為

mbr.asm

檔案,通過Nasm彙編器編譯為二進制檔案,然後再通過dd指令寫入到一個鏡像檔案中,具體編譯流程如下,這裡需要下載下傳好Windows版本的dd指令.

# 首先編譯為二進制檔案
nasm mbr.asm -o mbr.bin

# 将鏡像寫入到kernel.img鏡像中,寫入長度512位元組,循環1次
dd if=mbr.bin of=kernel.img bs=512 count=1 conv=notrunc

# 通過seek跳過第一個扇區,然後向後填充4096位元組的0
dd if=/dev/zero of=kernel.img seek=1 bs=512 count=4096
           

由于我們使用的模拟器是

Bochs x86

是以,在制作好鏡像後,需要在編寫一個虛拟機配置檔案,該配置檔案命名為

mbr.src

其内部需要定義好虛拟機的類型,啟動方式,鏡像位置等基本參數,一個簡易版定義語句如下.

megs:32
romimage:file=./BIOS-bochs-latest
vgaromimage:file=./VGABIOS-lgpl-latest
boot:disk
mouse:enabled=0
ata0-master: type=disk, path="kernel.img", mode=flat, status=inserted
keyboard: keymap=./x11-pc-de.map
           

啟動時可以直接調用

bochsdbg -q -f mbr.src

指令,使用調試模式運作,并通過語句

vb sp:0x7c00

在開頭下斷點,使用

c

指令可運作到MBR代碼處,單步n執行,即可輸出一段話,标志着MBR已經成功被加載.

作業系統開發:編寫開機引導

3. 讓MBR直接驅動顯示卡

如上代碼,我們通過調用BIOS提供的

int 0x10

中斷來實作列印字元操作,但我們在後期必須要借助顯示卡來輸出圖像,而顯示卡是外部裝置,必須通過總線來操作。

由于CPU使用的信号是TTL電平,而外部裝置都是機械裝置,故他們不會使用該電平驅動,這就導緻CPU與硬體裝置沒有辦法實作溝通,硬體工程師們提供的方法是,在這兩者之間架起一座橋,也就是在CPU和外設之間加上一層IO接口,該接口的作用就是實作CPU和外設之間互相做協調轉換。

其次外部裝置的種類也是多種多樣的,其輸出的信号可能是數字信号,也可能是模拟信号,而我們的CPU隻能處理數字信号,數字信号需要經過

數模轉換器<D/A>

成模拟量才能送到外設來驅動硬體工作,模拟量也同樣需要經過

模數轉換器<A/D>

轉換成數字量才能被CPU直接處理,是以接口電路中需要包括A/D轉換器和D/A轉換器。

轉換後的數字信号,會經過總線進行傳遞,總線的别名是BUS,之是以叫做BUS是因為其是公共線路,所有硬體裝置都會走此線路,但同一時刻,CPU隻能和一個IO接口(寄存器/端口)通信,當有多個IO接口同時想和CPU通信時,那麼IO仲裁子產品會對其進行競争與選優,仲裁子產品固化到,輸入輸出控制中心(ICH)也就是南橋晶片上的。

多數情況下,南橋和北橋是成對出現的,南橋主要負責連接配接

PCI,PCI-Express,AGP

等低速裝置,而北橋則用于連結高速裝置,如記憶體等。

IO接口都是串行口,其在設計之初就是負責與CPU進行通信的,我們想要與CPU通信,其實是向這些接口中寫入資料,同時為了差別CPU中的寄存器,是以把IO接口叫做端口,某些外設可以通過記憶體映射來通路,即把某些端口映射到指定記憶體中,通路某個記憶體區域就相當于通路了指定的端口。

由于顯示卡的起始位址為

0xb800

向該位址寫入資料即可回顯在顯示器上,如下代碼是一個簡單的填充過程。

SECTION MBR vstart=0x7c00     ; 告訴編譯器加載到7c00記憶體處
        mov ax,cs
        mov sp,0x7c00
        mov ax,0xb800
        mov gs,ax

        mov ax,0x600      ; 清屏範圍,也就是寬度
        mov bx,0x0
        mov cx,0x0        ; 清屏 左上角(0,0)
        mov dx,0x184f     ; 清屏 右下角(80=0x4f,25=0x18)
        int 0x10

        mov dh,0x0        ; 設定光标列号
        mov dl,0x0        ; 設定光标行号
        mov bh,0x0        ; 頁碼
        int 0x10

        mov byte [gs:0x00],'L'
        mov byte [gs:0x01],0xa4 ; 顯示A=綠色閃爍 4=紅色

        mov byte [gs:0x02],'y'
        mov byte [gs:0x03],0xa5

        mov byte [gs:0x04],'S'
        mov byte [gs:0x05],0xa6

        mov byte [gs:0x6],'h'
        mov byte [gs:0x7],0xa7

        mov byte[gs:0x8],'a'
        mov byte [gs:0x9],0xa6

        mov byte[gs:0xa],'r'
        mov byte [gs:0xb],0xa5

        mov byte[gs:0xc],'k'
        mov byte [gs:0xd],0xa4
        hlt
        ret

times 510-($-$$) db 0  ; 填充剩餘的510位元組的空間為0
db 0x55,0xaa           ; mbr的結束标志
           

編譯并運作這段代碼,由于使用的是顯示卡輸出,是以在輸出色彩上,我們的選擇餘地更多了。

作業系統開發:編寫開機引導

如上代碼中需要注意,偶數行

gs:0x04

代表的是輸出資料,奇數行

gs:0x05

則代表顔色背景色,如果需要實作循環輸出,那麼我們除需要考慮循環條件外,還應把基數偶數行也考慮進來。

SECTION MBR vstart=0x7c00     ; 告訴編譯器加載到7c00記憶體處
	; 清屏和設定光标位置
	mov ax,0x600      ; 清屏範圍,也就是寬度
	mov bx,0x0
	mov cx,0x0        ; 清屏 左上角(0,0)
	mov dx,0x184f     ; 清屏 右下角(80=0x4f,25=0x18)
	int 0x10

	mov dh,0x0        ; 設定光标列号 左上角(0,0)
	mov dl,0x0        ; 設定光标行号 右下角(0,0)
	mov bh,0x0        ; 頁碼
	int 0x10

	; 初始化,使SP寄存器指向段基址0X7C0處,GS指向顯存基位址
	mov ax,cs
	mov sp,0x7c00
	mov ax,0xb800
	mov gs,ax              ; 設定顯存位址

	; 設定字元串長度與字元串基位址
	mov cx, msglen        ; 擷取字元串長度
	mov si, message       ; 設定字元串基址
	xor di, di            ; 每次清空di寄存器

loop_str:
	mov al, [si]            ; 每次取出一個字元
	mov [gs:di], al         ; 将字元逐一指派到顯存中
	inc si
	inc di
	
	mov byte [gs:di], 000ch ; 設定字型顔色
	inc di
	loop loop_str

	hlt      ; 程式在此處終止

;message db "Loading MBR...",0ah,0dh
message db "Loading MBR..."
msglen  equ $ - message

times 510-($-$$) db 0  ; 填充剩餘的510位元組的空間為0
db 0x55,0xaa           ; mbr的結束标志
           
作業系統開發:編寫開機引導

4. 讓MBR直接驅動硬碟

既然顯示卡中存在端口可以被操作,那麼硬碟也同樣存在,硬碟控制器屬于IO接口,如果想讓硬碟工作,我們需要通過讀寫硬碟控制器上的端口,此處的端口指的就是硬碟控制器上的寄存器組。

  • 硬碟控制器中的端口可被分為兩種,最主要的是 Command Block registers 組中的寄存器
  • Command Block registers 用于向硬碟驅動器寫入指令字或者從硬碟控制器獲得硬碟狀态
  • Control Block registers 用于控制硬碟工作狀态

一般硬碟中的一個通道包括兩片硬碟,其中0為主盤,1為從盤,硬碟控制器中的主要寄存器如下,其中主盤所對應的通道為

Primary

,後面的那個

Secondary

則是從盤通道,主從盤調用中斷号完全不同:

作業系統開發:編寫開機引導
  • DATA 寄存器主要負責管理資料,相當于資料的門,作用是讀取或寫入資料
  • 讀硬碟時: 硬碟準備好資料後,硬碟控制器将其放在内部的緩沖區中,不斷讀此寄存器便是讀出緩沖區中的全部資料。
  • 寫硬碟時: 把資料源源不斷地輸送到此端口,資料便被存入緩沖區裡,硬碟控制器發現這個緩沖區中有資料了,便将此處的資料寫入相應的扇區中。
  • ERROR/FEATURES 由于環境不同用途不同,是以兩個寄存器名字指的是同一個
  • 讀硬碟時: 端口0x171或0x1F1的寄存器名字叫Error寄存器,若讀取失敗,裡面存儲的是失敗狀态資訊,并且0x1F2端口中存儲未讀的扇區數。
  • 寫硬碟時: 就變成feauture寄存器,用于寫指令的參數,有些指令需要指定額外參數,這些參數就寫在 Feature 寄存器中。
  • SectorCount 寄存器用來指定待讀取或待寫入的扇區數
  • 硬碟每完成1個扇區,就會将此寄存器的值減1,如果中間失敗了,此寄存器中的值便是尚未完成的扇區。
  • LBA 邏輯塊位址,解決了磁盤在柱面磁頭扇區上尋址的麻煩(CHS),尋址時不用再考慮扇區所在的實體結構,當今LBA有兩種,一種是LBA28最大支援128GB的尋址,另外一種是LBA48,最大支援128PB尋址.
  • LBA寄存器,有三種不同的形式: LBA low、LBA mid、LBA high
  • LBA low 寄存器用來存儲28位位址的第0-7位
  • LBA mid 寄存器用來存儲第8-15位
  • LBA high 寄存器存儲第16-23位
  • Device 寄存器是個雜項,寬度8位,此寄存器的低4位用來存儲LBA位址的第24-27位
  • 第4位用來指定通道上的主盤或從盤
  • 第6位用來設定是否啟用LBA方式
  • 第5位和第7位是固定為1的,稱為MBS位
  • Status 狀态寄存器,控制端口0x1F7或0x177,它是8位寬度寄存器,用來給出硬碟的狀态資訊
  • 第0位是ERR位,為1表示指令出錯,具體原因可見error寄存器。
  • 第3位是data request位,為1表示硬碟已經把資料準備好了機現在可以把資料讀出來。
  • 第6位是DRDY,表示硬碟就緒表示硬碟檢測正常,可以繼續執行一些其他指令。
  • 第7位是BSY位,表示硬碟是否繁忙,為1表示硬碟正忙。
  • 注釋: 狀态位與Error寄存器一樣,在寫硬碟時寄存器就變成Command,此寄存器用來存儲讓硬碟執行的指令,隻要把指令寫進此寄存器,硬碟就開始工作。

由于MBR受制于隻能容納512位元組大小的資料,沒法為核心準備好環境,更沒法将核心成功加載到記憶體并運作,此時我們需要讓MBR實作從硬碟加載Loader程式到記憶體,加載完成後再将接力棒交給Loader繼續運作,MBR加載磁盤代碼如下。

%include "boot.inc"
SECTION MBR vstart=0x7c00         
   mov ax,cs      
   mov ds,ax
   mov es,ax
   mov ss,ax
   mov fs,ax
   mov sp,0x7c00
   mov ax,0xb800
   mov gs,ax

; 清屏
   mov ax, 0600h
   mov bx, 0700h
   mov cx, 0
   mov dx, 184fh
   int 10h

   mov eax,LOADER_START_SECTOR	 ; 起始扇區lba位址
   mov bx,LOADER_BASE_ADDR       ; 寫入的位址
   mov cx,1			             ; 待讀入的扇區數
   call rd_disk_m_16		     ; 以下讀取程式的起始部分(一個扇區)
  
   jmp LOADER_BASE_ADDR

;功能:讀取硬碟n個扇區
rd_disk_m_16:	   
      mov esi,eax	  ;備份eax
      mov di,cx		  ;備份cx
;讀寫硬碟:
;第1步:設定要讀取的扇區數
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;讀取的扇區數

      mov eax,esi	   ;恢複ax

;第2步:将LBA位址存入0x1f3 ~ 0x1f6

      ;LBA位址7~0位寫入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA位址15~8位寫入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      ;LBA位址23~16位寫入端口0x1f5
      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f	   ;lba第24~27位
      or al,0xe0	   ; 設定7~4位為1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;第3步:向0x1f7端口寫入讀指令,0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;第4步:檢測硬碟狀态
  .not_ready:
      ;同一端口,寫時表示寫入指令字,讀時表示讀入硬碟狀态
      nop
      in al,dx
      and al,0x88	   ;第4位為1表示硬碟控制器已準備好資料傳輸,第7位為1表示硬碟忙
      cmp al,0x08
      jnz .not_ready	   ;若未準備好,繼續等。

;第5步:從0x1f0端口讀資料
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax	   ; di為要讀取的扇區數,一個扇區有512位元組,每次讀入一個字,
			   ; 共需di*512/2次,是以di*256
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx
      mov [bx],ax
      add bx,2		  
      loop .go_on_read
      ret

   times 510-($-$$) db 0
   db 0x55,0xaa
           

由于MBR是占據了硬碟的第0扇區不可再使用,從第1扇區之後的扇區均可使用,此處我們把loader放到第2扇區,MBR從第2扇區中把它讀出來,并将loader的加載位址選為0x900的位置,編譯鏡像需要注意扇區位置。

# 編譯并連接配接include目錄
nasm -I include/ mbr.asm -o mbr.bin

# 第一個扇區中寫入512位元組的mbr引導
dd if=mbr.bin of=kernel.img bs=512 count=1 conv=notrunc
dd if=/dev/zero of=kernel.img seek=1 bs=512 count=4096

# 第二個loader加載器,seek跳過兩個扇區并寫入
nasm -I include/ loader.asm -o loader.bin
dd if=loader.bin of=kernel.img bs=512 count=1 seek=2 conv=notrunc
           
作業系統開發:編寫開機引導

至此雖然輸出效果與在MBR中直接操作顯示卡輸出結果一緻,但本質是不同的,此處代碼中MBR主要負責從硬碟中的第3個扇區中讀入Loader加載器到記憶體,并将CPU指針指向它,後期的輸出純粹是Loader加載器所為。

5. 實模式切入保護模式

保護模式最早出現在80286系列處理器中,之是以會出現保護模式是因為實位址模式中存在以下問題:

1.實模式下作業系統與使用者程式屬于同一特權級R0,無法區分系統程式與使用者程式。

2.使用者程式引用的位址都是指向真實的實體位址,是以邏輯位址就等于實體位址。

3.使用者程式可以直接修改段基址,當通路超過64KB的記憶體區域時需要手動切換段基址。

4.共20條位址線最大可用1MB記憶體,且一次隻能運作1個程式,無法充分利用計算資源。

為了克服記憶體通路限制,CPU廠商則開發出保護模式,在保護模式下實體位址不能被程式直接通路,在通路時需要将虛拟位址轉換為實體位址再去通路,而對于程式而言這一系列操作都是透明的。

這個位址轉換過程是由作業系統與處理器共同協作完成的,處理器在硬體上提供位址轉換部件,作業系統提供轉換過程中所需要的頁表。

實模式與保護模式

相對于實位址模式,保護模式對寄存器進行了一定的擴充,CPU擴充為32位後,其位址總線和資料總線也變為32位,尋址空間達到了4GB,為了讓一個寄存器可以通路到4GB空間,需要将寄存器寬度提升到32位。

除段寄存器外,通用寄存器、指令指針寄存器、标志寄存器都由原來的16位擴充到了32位,段寄存器16位就夠用了。

相對于實位址模式,保護模式大大提高了對記憶體段的保護能力,GDT全局描述符就是對特定記憶體段屬性進行描述的資料結構,該資料結構中的每一個表項稱為段描述符,大小為64位元組,用來描述各個記憶體段的起始位址、大小、權限等資訊。

由于全局描述符表GDT很大,是以預設将其放在了記憶體中,由GDTR寄存器指向它,GDTR是個48位的寄存器,通常使用lgdtr指令操作,控制該寄存器。

這樣,段寄存器中儲存的再也不是段基址了,裡面儲存的内容叫

段選擇子(selector)

該選擇子其實就是個數,用這個數來索引全局描述符表中的段描述符,如果把全局描述符表當成數組,那麼選擇子就是數組的下标。

GDT 全局描述符表

全局描述符表GDT是保護模式下記憶體段的登記表,這是不同于實位址模式下的顯著特征。

局部描述符表LDT是CPU廠商為了在硬體層面支援多任務的一個表,當今作業系統不使用。

在實位址模式下,尋址是按照

[段基址+段内偏移]

的形式進行,而在保護模式下為了保證相容性,其也必須遵循這一規範。

在實位址模式下,通路記憶體時隻要将段基址加載到段寄存器中,再結合偏移位址就行,段寄存器太小了,隻能存儲 16 位的資訊,甚至連 20 位位址都要借助左移 4 位來實作。

而進入到保護模式,各個寄存器都提升到了32位,且還需對特定的記憶體段增加一些額外的安全屬性,那麼将這些屬性放在記憶體中是最好的選擇。

  • 之是以需要增加全局描述符表,并為每個段增加段描述符,是因為實模式下存在以下問題。
  • 實模式下的使用者程式可以破壞存儲代碼的記憶體區域,是以要添加個記憶體段類型屬性來阻止這種行為。
  • 實模式下的使用者程式和作業系統是同一級别的,是以要添加個特權級屬性來區分使用者程式和系統。
  • 記憶體段是一片記憶體區域,通路記憶體就要提供段基址,是以要有段基址屬性。
  • 為了限制程式通路記憶體的範圍,還要對段大小進行限制,是以要有段界限屬性。

段描述符: 一個段描述符隻用來描述一個記憶體段的屬性,這些描述符被依次排列在GDT中,GDT全局描述符表相當于描述符數組,數組中每個元素都是一個描述符,每個描述符大小是8位元組,分為高32位與低32位,即兩個四位元組,GDT中最多可容納的描述符數量是65536/8=8192個,即 GDT 中可容納 8192 個段或門。

如下,段描述符結構示意參考,以及每個字段的大體含義。

作業系統開發:編寫開機引導
  • 段界限
    • 第0-15位與16-19位共同構成段界限,表示段的邊界,大小,範圍,段界限用20個二進制位來表示。
  • 段基址
    • 第0-7是段基址的16-23位,第24-31位是段基址的高32位,加上段描述符低32位中的段基址0-15位,就構成了32位的基位址。
  • Type字段
    • 第8-11位是type字段,共占用4位,用于表示記憶體段或調用門的子類型。
  • S字段
    • 第12位是S字段,用于訓示是否為系統段,為0是系統段,為1是資料段,通常與Type字段配合使用。
  • DPL字段
    • 第13-14位是DPL,即描述符特權級,通常是指所代表的記憶體段的特權級。
  • Present字段
    • 第15位是P字段,标志着指定段是否存在,如果段存在于記憶體中,P為1否則為0。
  • AVaiLable字段
    • 第20位是AVL字段,無用途,可随意使用此位。
  • L字段
    • 第21位是L字段,用于設定是否是64位代碼段,L為1表示64位代碼段,為0則為32位。
  • D/B字段
    • 第22位是DB字段,用來訓示有效位址(段内偏移位址)及操作數的大小。
  • Granularity字段
    • 第23位是G字段,用來指定段界限的機關大小,若G為0表示段界限的機關是1位元組,若G為1表示段界限的機關是4KB。

段選擇子: 保護模式下段寄存器中存儲的就是段選擇子,選擇子是一個索引值,用此索引值在段描述符表中索引相應的段描述符,這樣,便可以在段描述符中得到了記憶體段的起始位址和段界限值等相關資訊。

如下,段選擇子結構示意參考,以及每個字段的大體含義。

作業系統開發:編寫開機引導

由于段寄存器是16位,是以選擇子也是16位,每一個選擇子都會被分為3塊。

  • RPL字段
    • 第0-1位,用來存儲RPL(請求特權級) 通常為0、1、2、3四種特權級。
  • TI字段
    • 第2位,用來訓示選擇子是在GDT還是LDT中索引描述符,為0在GDT中,為1在LDT中。
  • 描述符索引
    • 第3-15位是描述符的索引值,此值主要用于在GDT中索引符合條件的段描述符。

選擇子的作用主要是确定段描述符,确定描述符的目的,一是為了特權級、界限等安全考慮,最主要的還是要确定段的基位址。

由于保護模式下段寄存器中已經預設是選擇子了,在尋址時直接用選擇子對應的

[段描述符中的段基址+段内偏移位址]

就是要通路的記憶體位址。

A20Gate 位址回繞線

位址回繞線是為了相容8086實模式而增加的,在實模式下位址線隻有20條,尋址空間隻能是1MB

(0x00000 - 0xFFFFF)

如果超出1MB的尋址範圍,那麼在預設開啟位址回繞的CPU上,會自動将超出1MB的部分回繞到0位址處,繼續從0位址處開始映射,位址回繞如下圖。

作業系統開發:編寫開機引導

對于隻有20位位址線的8086系列CPU而言,A20位址線預設是開啟的,不需要任何操作即可實作位址回繞,但80286有24 條位址線,即

A0-A23

,也就是說A20 位址線是開啟的,如果通路

0x100000-0x10FFEF

之間的記憶體,系統将直接通路這塊實體記憶體,并不會像8086那樣回繞到0,反之如果是關閉的,則通路超出

0x00000 - 0xfffff

的位址範圍後會自動回繞到0處。

如果A20Gate被打開,當通路到

0x100000-0x10FFEF

之間的位址時,CPU将真正通路這塊實體記憶體。

如果A20Gate被禁止,當通路

0x100000-0x10FFEF

之間的位址時,CPU将采用8086的位址回繞。

我們想進入保護模式,就需要突破第20條位址線(A20)去通路更大的記憶體空間。而這一切,隻有關閉了位址回繞才能實作,而關閉位址回繞,就需要打開A20Gate,打開A20Gate位址線隻需要将端口

0x92

的第1位置1即可。

CR0 控制寄存器

想要進入保護模式還差最後一步,通過控制CR系列寄存器切換CPU模式,CR寄存器是CPU的控制視窗,即可用來查詢CPU的内部狀态,也可用于直接控制CPU的運作機制,切入保護模式最需要關注的是CR0寄存器中的PE位。

如下圖是完整的CR0寄存器,以及重要寄存器解釋:

作業系統開發:編寫開機引導
  • 保護允許位PE (Protedted Enable)
    • 0位用于啟動保護模式,如果PE置1則保護模式啟動,PE置0則實模式啟用。
  • 監控協處理位MP (Moniter coprocessor)
    • 1位與3位配合,當TS=1時操作碼WAIT是否産生一個協處理器不能使用的出錯信号。
  • 任務轉換位TS (Task Switch)
    • 3位當一個任務轉換完成之後,自動将它置1,随着TS=1就不能使用協處理器。
  • 模拟協處理器位EM (Emulate coprocessor)
  • 2位如果EM=1則不能使用協處理器,如果EM=0則允許使用協處理器。
  • 微處理器擴充類型位ET (Processor Extension Type)
    • 4位儲存着處理器擴充類型的資訊,如果ET=0使用287協處理器,ET=1使用387浮點協處理器。
  • 寫保護位WP
    • 16位這一位置0就可以禁用寫保護,置1則可恢複寫保護。
  • 分頁允許位PE (Paging Enable)
    • 31位表示晶片上的分頁部件是否允許工作。

正式切入保護模式

在保護模式中,記憶體段都是平坦模式,也就是整個記憶體都在一個段内,進入保護模式之前我們需要手動在記憶體中建構出GDT及其内部的描述符,GDT隻是一片記憶體區域,裡面每隔8位元組即是一個段描述符,GDT結構如下。

GDT_BASE:   dd    0x00000000 
			   dd    0x00000000

   CODE_DESC:  dd    0x0000FFFF 
			   dd    DESC_CODE_HIGH4

   DATA_STACK_DESC:  dd    0x0000FFFF
					 dd    DESC_DATA_HIGH4

   VIDEO_DESC: dd    0x80000007	                  ; limit=(0xbffff-0xb8000)/4k=0x7
			   dd    DESC_VIDEO_HIGH4             ; 此時dpl已改為0

   GDT_SIZE    equ   $ - GDT_BASE
   GDT_LIMIT   equ   GDT_SIZE -	1 

   times 60 dq 0					          ; 預留60個描述符的slot

   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0         ; 相當于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	     ; 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	     ; 同上 

   ;以下是定義gdt的指針,前2位元組是gdt界限,後4位元組是gdt起始位址
   gdt_ptr  dw  GDT_LIMIT 
	        dd  GDT_BASE
           

GDT中的每個描述符簡單介紹.

  • GDT_BASE
    • 建構GDT的起始位址(此位是0位,不可用,是以直接填充為全0即可)
  • CODE_DESC/DATA_STACK_DESC/VIDEO_DESC
    • 代碼段描述符,資料段和棧段描述符,顯存描述符
  • GDT_SIZE/GDT_LIMIT
    • 計算出GDT大小,GDT_LIMIT得到段界限,為後續加載GDT做準備.
  • SELECTOR_CODE/SELECTOR_DATA/SELECTOR_VIDEO
    • 分别建構代碼段,資料段,顯存段的選擇子.
  • gdt_ptr
    • 定義GDT指針,此指針是lgdt加載GDT到gdtr寄存器用的.

接下來就是進入保護模式,進入保護模式需要三步,1.打開A20,2.加載GDT,3.CR0第0位置1,代碼如下。

in al,0x92
   or al,0000_0010B
   out 0x92,al

   lgdt [gdt_ptr]

   mov eax, cr0
   or eax, 0x00000001
   mov cr0, eax
           

最後還需要使用

jmp SELECTOR_CODE:p_mode_start

指令來實作重新整理流水線。

jmp  SELECTOR_CODE:p_mode_start

[bits 32]
p_mode_start:
   mov ax, SELECTOR_DATA
   jmp $
           

流水線是CPU為了提高執行效率而發展起來的加速技術,通常執行指令需要經過取指令,譯碼,執行指令,等操作,而運用流水線技術則将目前指令及其後面的幾條指令同時放在流水線中重疊執行。

由于實模式是16位的,而保護模式是32位,在切換時必須要清空目前流水線上面所有的16位指令集,以及錯誤的段屬性,隻有這樣才能保證後面的32位指令能夠被正确的執行。

此時我們既要改變代碼段描述符緩沖寄存器的值,又要清空以前的流水線,使用JMP指令則可以達到這兩種效果,JMP指令在執行無條件跳轉時會自動的将所有段寄存器初始化并清空目前流水線上的指令集。

作業系統開發:編寫開機引導
文章作者:

lyshark (王瑞)

文章出處:

https://www.cnblogs.com/LyShark/p/16517379.html

版權聲明:

本部落格文章與代碼均為學習時整理的筆記,文章

[均為原創]

作品,轉載請

[添加出處]

,您添加出處是我創作的動力!

轉載文章,請遵守

《中華人民共和國著作權法》

相關規定或遵守

《署名CC BY-ND 4.0國際》

禁止演繹規範,合理合規,攜帶原創出處轉載。

繼續閱讀