前言
上一篇其實已經(jīng)說完了boot的大致工作,但是Linux在最后進入操作系統(tǒng)之前還有一些操作,比如進入保護模式。在我自己的FragileOS
<https://github.com/dejavudwh/FragileOS>里進入保護模式是在引導(dǎo)程序結(jié)束后完成的。
實模式到保護模式屬于操作系統(tǒng)的一個大坎,所以需要先提一下
從實模式到保護模式
實模式和保護模式都是CPU的工作模式,它們的主要區(qū)別就是尋址方式
實模式出現(xiàn)于早期8088CPU時期。當(dāng)時由于CPU的性能有限,一共只有20位地址線(所以地址空間只有1MB),以及8個16位的通用寄存器,以及4個16位的段寄存器。所以為了能夠通過這些16位的寄存器去構(gòu)成20位的主存地址,必須采取一種特殊的方式。訪問內(nèi)存的就變成了:
物理地址 = 段基址 << 4 + 段內(nèi)偏移
隨著CPU的發(fā)展,可以訪問的內(nèi)存空間也從1MB變?yōu)楝F(xiàn)在4GB,寄存器的位數(shù)也變?yōu)?2位。并且在實模式下,用戶程序?qū)?nèi)存的訪問非常自由,沒有任何限制,隨隨便便就可以修改任何一個內(nèi)存單元。所以實模式已經(jīng)不能滿足時代的要求了,保護模式就應(yīng)運而生了
保護模式的偏移值變成了32位,尋址方式仍然需要段寄存器,但是這些段寄存器存放的不再是段基址了,而是類似一個數(shù)組的索引
而這個數(shù)組就是一個就做全局描述符表 (GDT)的東西,GDT中含有一個個表項,每一個表項稱為段描述符。
而我們通過段寄存器里的的這個索引,可以找到對應(yīng)的表項。段描述符存放了段基址、段界限、內(nèi)存段類型屬性
處理器內(nèi)部有一個 48 位的寄存器,稱為全局描述符表寄存器(GDTR)。也就是為了來記錄GDT的
段描述符
FragileOS里進入保護模式
* 根據(jù)上面的描述,在進入保護模式時就先需要構(gòu)造一個GDT
* 當(dāng)然中間還需要一些其它的初始化,在后面詳細提
* 然后再根據(jù)特定操作來讓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啟動前的最后準(zhǔn)備
現(xiàn)在來看看Linux在啟動前最后還做了什么
獲得系統(tǒng)數(shù)據(jù)和進入保護模式
setup.s主要的任務(wù)就是從BIOS拿到系統(tǒng)數(shù)據(jù)然后存放到一個內(nèi)存位置
獲取當(dāng)前光標(biāo)的位置
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.
獲取內(nèi)存大小
mov ah,#0x88 int 0x15 mov [2],ax
檢查現(xiàn)在的顯示方式
mov ah,#0x0f int 0x10 mov [4],bx ! bh = display page mov [6],ax ! al = video
mode, ah = window width
進入保護模式
進入保護模式的代碼也在setup中
首先先把內(nèi)核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,就是 操作系統(tǒng)必須維護一份中斷向量表,每一個表項紀(jì)錄一個中斷處理程序(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進行中斷控制而設(shè)計的芯片,它是可以用程序控制的中斷控制器。單個的8259A能管理8級向量優(yōu)先級中斷。
對于對硬件的初始化其實就是依照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,另外需要注意的是在進入保護模式之后需要立馬執(zhí)行一條段間跳轉(zhuǎn)來讓CPU刷新指令隊列
,這里跳轉(zhuǎn)的描述就已經(jīng)是用段值來描述了,段指的第三位到第十五位用來指向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段描述符,所以上面也就是跳轉(zhuǎn)到內(nèi)存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和分頁管理機制
再往下就是正式進入到了內(nèi)核部分,在此之前需要再提一下IDT和分頁管理機制
IDT
中斷描述符表把每個中斷或異常編號和一個指向中斷處理事件服務(wù)程序的描述符聯(lián)系起來。同GDT和LDT一樣,IDT是一個8-字節(jié)的描述符數(shù)組。和GDT、LDT不同的是,IDT的第一項可以包含一個描述符。為了形成一個在IDT內(nèi)的索引,處理器把中斷、異常標(biāo)識號乘以8以后來做為IDT的索引。因為只有256個編號,IDT不必包含超過256個描述符。它可以包含比256更少的項,只是那些需要使用的中斷、異常的項。
IDT可以在內(nèi)存的任意位置。處理器通過IDT寄存器(IDTR)來定位IDT。指令LIDT和SIDT用來操作IDTR。
分頁機制
將用戶程序(進程)的邏輯地址空間分成若干個頁(4KB)并編號,同時將內(nèi)存的物理地址也分成若干個塊或頁框
4KB)并編號,這樣也就是為了讓所有的應(yīng)用程序看都像是獨占一片內(nèi)存,起始地址都是為0,最后再建立一個頁表存儲著頁到頁框也就是真實內(nèi)存地址的映射
在內(nèi)存里有一個寄存器(PTR)來存儲頁表
映射的完成
* 進程訪問某個邏輯地址
* 由線性地址的頁號,以及頁表寄存器中的始址,找到頁表并找到對應(yīng)的頁表項
* 由頁表項上的塊號,找到物理內(nèi)存中的塊號
* 根據(jù)塊號,和線性地址的頁內(nèi)地址,找到物理地址
我們通過設(shè)置CR0寄存器的PG位來開啟分頁功能,而其它操作就都由CPU來完成,當(dāng)然前提是我們有一張頁表
兩級頁表結(jié)構(gòu)
為了減少內(nèi)存的占用量,80X86采用了分級頁表
頁目錄有2的十次方個4字節(jié)的表項,這些表項指向?qū)?yīng)的二級表,線性地址的最高10位作為頁目錄用來尋找二級表的索引
二級頁表里的表項含有相關(guān)頁面的20位物理基地址,二級頁表使用線性地址中間10位來作為尋找表項的索引
* 進程訪問某個邏輯地址
* 由線性地址中的頁號,以及外層頁表寄存器(CR3)中的外層頁表始址,找到二級頁表的始址
* 由二級頁表的始址,加上線性地址中的外層頁內(nèi)地址,找到對應(yīng)的二級頁表中的頁表項
* 由頁表項中的物理塊號,加上線性地址中的頁內(nèi)地址,找到對物理地址
所以說CPU尋址一共需要進行兩步:
* 首先將給定一個邏輯地址 (其實是段內(nèi)偏移量)
* CPU利用段式內(nèi)存管理單元,先將為個邏輯地址轉(zhuǎn)換成一個線程地址 (也就是前面說的GDT)
* 再利用其頁式內(nèi)存管理單元,轉(zhuǎn)換為最終物理地址。(二級頁表)
進入到了內(nèi)核部分
head.s這部分其實已經(jīng)是進入了內(nèi)核部分了,但是在Linux0.12里還是把它歸為Boot部分。這一部分的主要工作是重新設(shè)置GDT和IDT,然后在設(shè)置管理內(nèi)存的分頁處理機制
(在進入保護模式后,Linux用的就是AT&T的匯編語法了,最顯著的差別就是源操作數(shù)和目的數(shù)的位置對調(diào)了)
* 設(shè)置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
* 設(shè)置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
* 這里就是已經(jīng)準(zhǔn)備跳入C語言的main部分了,也就是匯編里的函數(shù)調(diào)用,先把main的地址壓入棧中,當(dāng)下一個函數(shù)執(zhí)行完ret的時候,就會去執(zhí)行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.
* 最后就是設(shè)置分頁機制了
STOS指令:將AL/AX/EAX的值存儲到[EDI]指定的內(nèi)存單元
CLD清除方向標(biāo)志和STD設(shè)置方向標(biāo)志,當(dāng)方向標(biāo)志是0,該指令通過遞增的指針數(shù)據(jù)每一次迭代之后(直到ECX是零或一些其它條件,這取決于REP前綴的香味)工作,而如果該標(biāo)志是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 */
小結(jié)
這一節(jié)主要是描述了保護模式和一些CPU需要的數(shù)據(jù)結(jié)構(gòu)。這幾篇文章相當(dāng)于講述了一臺計算機啟動的時候都發(fā)生了什么。
* 通過引導(dǎo)程序boot來加載真正的內(nèi)核代碼
* 獲得一些硬件上的系統(tǒng)參數(shù)保存在一些內(nèi)存里供后面使用
* 最后是初始化像GDT、IDT等,然后設(shè)置分頁等等
熱門工具 換一換