上一篇其實已經說完了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
段描述符
一部分代碼
[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在啓動前最後還作了什麼
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和分頁管理機制
中斷描述符表把每一箇中斷或異常編號和一個指向中斷處理事件服務程序的描述符聯繫起來。同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位來做爲尋找表項的索引
因此說CPU尋址一共須要進行兩步:
head.s這部分其實已是進入了內核部分了,可是在Linux0.12裏仍是把它歸爲Boot部分。這一部分的主要工做是從新設置GDT和IDT,而後在設置管理內存的分頁處理機制 (在進入保護模式後,Linux用的就是AT&T的彙編語法了,最顯著的差異就是源操做數和目的數的位置對調了)
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
複製代碼
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
複製代碼
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須要的數據結構。這幾篇文章至關於講述了一臺計算機啓動的時候都發生了什麼。