我是如何學習寫一個操做系統(三):操做系統的啓動之保護模式

前言

上一篇其實已經說完了boot的大體工做,可是Linux在最後進入操做系統以前還有一些操做,好比進入保護模式。在我本身的FragileOS裏進入保護模式是在引導程序結束後完成的。git

實模式到保護模式屬於操做系統的一個大坎,因此須要先提一下github

從實模式到保護模式

實模式和保護模式都是CPU的工做模式,它們的主要區別就是尋址方式數組

實模式出現於早期8088CPU時期。當時因爲CPU的性能有限,一共只有20位地址線(因此地址空間只有1MB),以及8個16位的通用寄存器,以及4個16位的段寄存器。因此爲了可以經過這些16位的寄存器去構成20位的主存地址,必須採起一種特殊的方式。訪問內存的就變成了:數據結構

  物理地址 = 段基址 << 4 + 段內偏移app

隨着CPU的發展,能夠訪問的內存空間也從1MB變爲如今4GB,寄存器的位數也變爲32位。而且在實模式下,用戶程序對內存的訪問很是自由,沒有任何限制,隨隨便便就能夠修改任何一個內存單元。因此實模式已經不能知足時代的要求了,保護模式就應運而生了ide

保護模式的偏移值變成了32位,尋址方式仍然須要段寄存器,可是這些段寄存器存放的再也不是段基址了,而是相似一個數組的索引函數

而這個數組就是一個就作全局描述符表 *(GDT)*的東西,GDT中含有一個個表項,每個表項稱爲段描述符。性能

而咱們經過段寄存器裏的的這個索引,能夠找到對應的表項。段描述符存放了段基址、段界限、內存段類型屬性fetch

處理器內部有一個 48 位的寄存器,稱爲全局描述符表寄存器(GDTR)。也就是爲了來記錄GDT的this

段描述符

FragileOS裏進入保護模式

  • 根據上面的描述,在進入保護模式時就先須要構造一個GDT
  • 固然中間還須要一些其它的初始化,在後面詳細提
  • 而後再根據特定操做來讓CPU識別該進入保護模式了

一部分代碼

[SECTION .gdt]                              ; 利用宏定義定義gdt
                                            ; 段基址          段界限              屬性
LABEL_GDT:          Descriptor        0,            0,                 0
LABEL_DESC_CODE32:  Descriptor        0,            0fffffh,           DA_C    | DA_32 | DA_LIMIT_4K
LABEL_DESC_VIDEO:   Descriptor        0B8000h,      0fffffh,           DA_DRW
LABEL_DESC_VRAM:    Descriptor        0,            0fffffh,           DA_DRW  | DA_LIMIT_4K

in    al,  92h                         ; 切換到保護模式
or    al,  00000010b
out   92h, al

mov   eax, cr0
or    eax , 1
mov   cr0, eax
複製代碼

Linux啓動前的最後準備

如今來看看Linux在啓動前最後還作了什麼

得到系統數據和進入保護模式

setup.s主要的任務就是從BIOS拿到系統數據而後存放到一個內存位置

獲取當前光標的位置

mov	ax,#INITSEG	! this is done in bootsect already, but...
mov	ds,ax
mov	ah,#0x03	! read cursor pos
xor	bh,bh
int	0x10		! save it in known place, con_init fetches
mov	[0],dx		! it from 0x90000.
複製代碼

獲取內存大小

mov	ah,#0x88
int	0x15
mov	[2],ax
複製代碼

檢查如今的顯示方式

mov	ah,#0x0f
int	0x10
mov	[4],bx		! bh = display page
mov	[6],ax		! al = video mode, ah = window width
複製代碼

進入保護模式

進入保護模式的代碼也在setup中

首先先把內核SYSTEM部分移動到0位置,在以前它是被讀入在0x10000位置

mov	ax,#0x0000
	cld			! 'direction'=0, movs moves forward
do_move:
	mov	es,ax		! destination segment
	add	ax,#0x1000
	cmp	ax,#0x9000
	jz	end_move
	mov	ds,ax		! source segment
	sub	di,di
	sub	si,si
	mov 	cx,#0x8000
	rep
	movsw
	jmp	do_move
複製代碼

而後就是加載上面說的全局描述符表和中斷向量表

中斷向量表前面沒有提過,可是比較簡單,有點相似GDT,就是 操做系統必須維護一份中斷向量表,每個表項紀錄一箇中斷處理程序(ISR,Interrupt Service Routine)的地址

end_move:
	mov	ax,#SETUPSEG	! right, forgot this at first. didn't work :-)
	mov	ds,ax
	lidt	idt_48		! load idt with 0,0
	lgdt	gdt_48		! load gdt with whatever appropriate
複製代碼

再接着就是打開A20地址線,若是不打開A20地址線,即便在保護模式下最大尋址仍是1M

call	empty_8042
mov	al,#0xD1		! command write
out	#0x64,al
call	empty_8042
mov	al,#0xDF		! A20 on
out	#0x60,al
call	empty_8042
複製代碼

初始化8259A芯片,8259A是專門爲了對8085A和8086/8088進行中斷控制而設計的芯片,它是能夠用程序控制的中斷控制器。單個的8259A能管理8級向量優先級中斷。 對於對硬件的初始化其實就是依照CPU的固定套路

部分代碼

mov	al,#0x11		! initialization sequence
out	#0x20,al		! send it to 8259A-1
.word	0x00eb,0x00eb		! jmp $+2, jmp $+2
out	#0xA0,al		! and to 8259A-2
複製代碼

最後的最後,終於能夠正式進入保護模式,能夠看到這裏進入保護模式的方法和我上面的move cr0 ax不太同樣,Linux之因此使用這種方法是爲了兼容286以前的CPU,另外須要注意的是在進入保護模式以後須要立馬執行一條段間跳轉來讓CPU刷新指令隊列,這裏跳轉的描述就已是用段值來描述了,段指的第三位到第十五位用來指向GDT裏的索引(1000),也就是跳到第2個段描述符裏記錄的地址

mov	ax,#0x0001	! protected mode (PE) bit
lmsw	ax		! This is it!
jmpi	0,8		! jmp offset 0 of segment 8 (cs)
複製代碼

第二個GTD段描述符,因此上面也就是跳轉到內存0處

.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
.word	0x0000		! base address=0
.word	0x9A00		! code read/exec
.word	0x00C0		! granularity=4096, 386
複製代碼

IDT和分頁管理機制

再往下就是正式進入到了內核部分,在此以前須要再提一下IDT和分頁管理機制

IDT

中斷描述符表把每一箇中斷或異常編號和一個指向中斷處理事件服務程序的描述符聯繫起來。同GDT和LDT同樣,IDT是一個8-字節的描述符數組。和GDT、LDT不一樣的是,IDT的第一項能夠包含一個描述符。爲了造成一個在IDT內的索引,處理器把中斷、異常標識號乘以8之後來作爲IDT的索引。由於只有256個編號,IDT沒必要包含超過256個描述符。它能夠包含比256更少的項,只是那些須要使用的中斷、異常的項。

IDT能夠在內存的任意位置。處理器經過IDT寄存器(IDTR)來定位IDT。指令LIDT和SIDT用來操做IDTR。

分頁機制

將用戶程序(進程)的邏輯地址空間分紅若干個頁(4KB)並編號,同時將內存的物理地址也分紅若干個塊或頁框 4KB)並編號,這樣也就是爲了讓全部的應用程序看都像是獨佔一片內存,起始地址都是爲0,最後再創建一個頁表存儲着頁到頁框也就是真實內存地址的映射

在內存裏有一個寄存器(PTR)來存儲頁表

映射的完成

  • 進程訪問某個邏輯地址
  • 由線性地址的頁號,以及頁表寄存器中的始址,找到頁表並找到對應的頁表項
  • 由頁表項上的塊號,找到物理內存中的塊號
  • 根據塊號,和線性地址的頁內地址,找到物理地址

咱們經過設置CR0寄存器的PG位來開啓分頁功能,而其它操做就都由CPU來完成,固然前提是咱們有一張頁表

兩級頁表結構

爲了減小內存的佔用量,80X86採用了分級頁表

頁目錄有2的十次方個4字節的表項,這些表項指向對應的二級表,線性地址的最高10位做爲頁目錄用來尋找二級表的索引

二級頁表裏的表項含有相關頁面的20位物理基地址,二級頁表使用線性地址中間10位來做爲尋找表項的索引

  • 進程訪問某個邏輯地址
  • 由線性地址中的頁號,以及外層頁表寄存器(CR3)中的外層頁表始址,找到二級頁表的始址
  • 由二級頁表的始址,加上線性地址中的外層頁內地址,找到對應的二級頁表中的頁表項
  • 由頁表項中的物理塊號,加上線性地址中的頁內地址,找到對物理地址

因此說CPU尋址一共須要進行兩步:

  1. 首先將給定一個邏輯地址 (實際上是段內偏移量)
  2. CPU利用段式內存管理單元,先將爲個邏輯地址轉換成一個線程地址 (也就是前面說的GDT)
  3. 再利用其頁式內存管理單元,轉換爲最終物理地址。(二級頁表)

進入到了內核部分

head.s這部分其實已是進入了內核部分了,可是在Linux0.12裏仍是把它歸爲Boot部分。這一部分的主要工做是從新設置GDT和IDT,而後在設置管理內存的分頁處理機制 (在進入保護模式後,Linux用的就是AT&T的彙編語法了,最顯著的差異就是源操做數和目的數的位置對調了)

  • 設置IDT
setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea idt,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr
	ret
複製代碼
  • 設置GDT
setup_gdt:
	lgdt gdt_descr
	ret

gdt_descr:
	.word 256*8-1		# so does gdt (not that that's any
	.long gdt		# magic number, but it works for me :^)

	.align 8	
複製代碼
  • 這裏就是已經準備跳入C語言的main部分了,也就是彙編裏的函數調用,先把main的地址壓入棧中,當下一個函數執行完ret的時候,就會去執行main了
after_page_tables:
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $main
	jmp setup_paging
L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.
複製代碼
  • 最後就是設置分頁機制了

STOS指令:將AL/AX/EAX的值存儲到[EDI]指定的內存單元 CLD清除方向標誌和STD設置方向標誌,當方向標誌是0,該指令經過遞增的指針數據每一次迭代以後(直到ECX是零或一些其它條件,這取決於REP前綴的香味)工做,而若是該標誌是1,指針遞減。

setup_paging:
	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */
	cld;rep;stosl
	movl $pg0+7,pg_dir		/* set present bit/user r/w */
	movl $pg1+7,pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,pg_dir+12		/*  --------- " " --------- */
	movl $pg3+4092,%edi
	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */
	std
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax
	jge 1b
	xorl %eax,%eax		/* pg_dir is at 0x0000 */
	movl %eax,%cr3		/* cr3 - page directory start */
	movl %cr0,%eax
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			/* this also flushes prefetch-queue */
複製代碼

小結

這一節主要是描述了保護模式和一些CPU須要的數據結構。這幾篇文章至關於講述了一臺計算機啓動的時候都發生了什麼。

  • 經過引導程序boot來加載真正的內核代碼
  • 得到一些硬件上的系統參數保存在一些內存裏供後面使用
  • 最後是初始化像GDT、IDT等,而後設置分頁等等
相關文章
相關標籤/搜索