JOS在啟動(dòng)的過(guò)程(bootload過(guò)程)中一共經(jīng)歷了三個(gè)階段,分別是BIOS加載以及硬件檢測(cè)、實(shí)模式向保護(hù)模式的轉(zhuǎn)變以及加載kernel。故本文組織過(guò)程也按照啟動(dòng)過(guò)程依次敘述。
一、BIOS加載
計(jì)算機(jī)系統(tǒng)在電源鍵開(kāi)啟后,首先接管整個(gè)系統(tǒng)的是BIOS。BIOS主要完成以下幾個(gè)內(nèi)容:
- 對(duì)計(jì)算機(jī)的硬件進(jìn)行檢測(cè)
- 加載啟動(dòng)文件
在更進(jìn)一步的敘述BIOS之前,我們需要簡(jiǎn)單的了解x86的物理地址空間。下圖是一個(gè)典型的x86地址空間,這里需要注意的是,BIOS的內(nèi)存空間是從0x000F 0000 ~ 0x0010 0000共64KB。在啟動(dòng)電源后,BIOS將會(huì)加載到上述的內(nèi)存空間內(nèi)。

按照慣例,BIOS將會(huì)被加載到CS:0xF000,IP:0xFFF0 處,即0x000F FFF0。由于此地址已經(jīng)非常接近于0x0010 0000,留給BIOS執(zhí)行的內(nèi)存空間過(guò)少,所以BIOS加載執(zhí)行的第一條指令就是:
ljmp $0xF000, $0xE05B將自己移動(dòng)到較低的地址空間,以保證足夠的內(nèi)存使其能夠繼續(xù)執(zhí)行。在BIOS運(yùn)行的過(guò)程中,將設(shè)立終中斷述表以及初始化各種設(shè)備,當(dāng)完成上述過(guò)程后,BIOS搜索能夠啟動(dòng)的設(shè)備(如軟盤(pán)、硬盤(pán)、CD-ROM等),對(duì)該設(shè)備的第一個(gè)扇區(qū)進(jìn)行讀取,最后將控制權(quán)轉(zhuǎn)移給扇區(qū)中存放的bootloader。
這里需要說(shuō)明的是:(1)在上述啟動(dòng)的過(guò)程中,CPU是運(yùn)行在實(shí)模式下; (2)對(duì)于硬盤(pán)來(lái)說(shuō),最基本存儲(chǔ)單元是扇區(qū)(sector),每個(gè)扇區(qū)容量為512個(gè)字節(jié)。對(duì)于一個(gè)可啟動(dòng)的硬盤(pán),其第一個(gè)扇區(qū)必須是bootloader,故bootloader不能占據(jù)過(guò)大的空間。
二、保護(hù)模式
在JOS中,bootloader將完成兩部分工作:
- 實(shí)模式轉(zhuǎn)變?yōu)楸Wo(hù)模式
- 加載內(nèi)核文件
在這部分中,我們將簡(jiǎn)要的敘述實(shí)模式向保護(hù)模式的轉(zhuǎn)變過(guò)程(/boot/boot.S)。保護(hù)模式下,系統(tǒng)將具有更大的尋址空間,并提供虛擬內(nèi)存、分段、分頁(yè)等機(jī)制。保護(hù)模式下的JOS介紹將會(huì)在后續(xù)的文章中介紹。
在這里需要注意的是,開(kāi)啟保護(hù)模式涉及到了cr0寄存器下的PE位(即第0位),當(dāng)PE置1后,CPU將開(kāi)啟保護(hù)模式,此時(shí)保護(hù)模式下的分段保護(hù)機(jī)制將會(huì)被一同開(kāi)啟(分頁(yè)機(jī)制沒(méi)有開(kāi)啟),故在開(kāi)啟保護(hù)模式前,需要設(shè)置好全局描述符表。
JOS中對(duì)于保護(hù)模式的開(kāi)啟有以下代碼:
lgdt gdtdesc # 加載全局描述符表
movl %cr0, %eax
orl $CR0_PE_ON, %eax # CR0_PE_ON = 0x1
movl %eax, %cr0 # 開(kāi)啟保護(hù)模式
gdt為預(yù)先設(shè)定好的全局描述符表。對(duì)于全局描述符表的介紹將會(huì)在下一篇文章中涉及,在這里需要知道的是,全局描述符表的第0項(xiàng)存儲(chǔ)的內(nèi)容必須為空(即為0)
gdt:
SEG_NULL # 空項(xiàng)
SEG(STA_X | STA_R, 0x0, 0xffffffff) # 代碼段
SEG(STA_W, 0x0, 0xffffffff) # 數(shù)據(jù)段
gdtdesc:
.word 0x17 # 在全局描述符表中共設(shè)置了三項(xiàng),每項(xiàng)8字節(jié),共24字節(jié),故在此處設(shè)為(24-1),即0x17
.long gtd
對(duì)于SEG_NULL、SEG()定義如下:
#define SEG_NULL \
.word 0, 0; \
.byte 0, 0, 0, 0
#define SEG(type, base, lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
可以看出,在gdt中,將代碼段和數(shù)據(jù)段全部映射到了4GB內(nèi)存空間中,這對(duì)于啟動(dòng)過(guò)程來(lái)說(shuō),是完全夠用的。
三、加載內(nèi)核
這一部分中,主要完成的工作就是將內(nèi)核文件加載到內(nèi)存中(/boot/main.c),并將控制權(quán)限交給內(nèi)核。在更進(jìn)一步的介紹之前,首先闡述ELF文件格式。對(duì)于ELF文件格式的定義在<inc/elf.h>中。我們無(wú)需深入的了解ELF文件格式(如希望深入了解的話,在MIT6.828的指定文獻(xiàn)中列出了ELF文件的詳細(xì)格式內(nèi)容),實(shí)際上來(lái)說(shuō),ELF類似于一個(gè)超大的“結(jié)構(gòu)體”,每一個(gè)部分都存放了一定的內(nèi)容,而對(duì)于該內(nèi)容的描述在“頭部”中存放。這里給出了JOS下<inc/elf.h>中的定義以及解釋。
struct Elf {
uint32_t e_magic; // must equal ELF_MAGIC
uint8_t e_elf[12];
uint16_t e_type; // 表示該文件類型
uint16_t e_machine; // 運(yùn)行該程序需要的體系結(jié)構(gòu)
uint32_t e_version; // 文件版本
uint32_t e_entry; // 程序入口地址
uint32_t e_phoff; // Program header table在文件中的偏移量(以字節(jié)計(jì)數(shù))
uint32_t e_shoff; // Section header table在文件中的偏移量
uint32_t e_flags; // 對(duì)于IA32來(lái)說(shuō),計(jì)為0
uint16_t e_ehsize; // 表示ELF header大小
uint16_t e_phentsize; // Program header table中每一項(xiàng)目的大小
uint16_t e_phnum; // Program header table有多少個(gè)項(xiàng)目
uint16_t e_shentsize; // Section header table中每一項(xiàng)目的大小
uint16_t e_shnum; // Section header table有多少個(gè)項(xiàng)目
uint16_t e_shstrndx; // 包含節(jié)名稱的字符串是第幾個(gè)節(jié)(0開(kāi)始計(jì)數(shù))
};
struct Proghdr {
uint32_t p_type; // 當(dāng)前Program header所描述的段的類型
uint32_t p_offset; // 段的第一個(gè)字節(jié)在文件中的偏移
uint32_t p_va; // 段的一個(gè)字節(jié)在內(nèi)存中的虛擬地址
uint32_t p_pa; // 在物理內(nèi)存定位的相關(guān)系統(tǒng)中,此項(xiàng)是為物理地址保留的
uint32_t p_filesz; // 段在文件中的長(zhǎng)度
uint32_t p_memsz; // 段在內(nèi)存中的長(zhǎng)度
uint32_t p_flags; // 與段相關(guān)的標(biāo)志
uint32_t p_align; // 根據(jù)此值來(lái)確定段在文件以及內(nèi)存中如何對(duì)齊
};
有了上述的認(rèn)識(shí),就可以很容易的讀懂下述代碼。下述代碼主要是將內(nèi)核讀取到磁盤(pán)中,并最后將控制權(quán)移交給內(nèi)核。
#define SECTSIZE 512
#define ELFHDR ((struct Elf *) 0x10000) // scratch space
void readsect(void*, uint32_t);
// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
void readseg(uint32_t, uint32_t, uint32_t);
void
bootmain(void)
{
struct Proghdr *ph, *eph;
// read 1st page off disk
// 可以看出,內(nèi)核加載于0x10000處之上,一共加載了512字節(jié) * 8 = 4K,即分頁(yè)模式下一個(gè)完整的頁(yè)的大小
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
// is this a valid ELF? JOS中要求ELF文件的第一項(xiàng)必須為ELF_MAGIC
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// load each program segment (ignores ph flags)
// 加載代碼段, 可以看出每個(gè)代碼段都規(guī)定了加載的位置以及大小
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
// call the entry point from the ELF header
// note: does not return!
// 移交控制權(quán),e_entry即為入口函數(shù), 此函數(shù)不會(huì)返回,如果返回則意味著執(zhí)行出現(xiàn)了某種問(wèn)題,此后系統(tǒng)進(jìn)入死循環(huán),需要手動(dòng)重啟
((void (*)(void)) (ELFHDR->e_entry))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
}
對(duì)于內(nèi)核加載部分還是很容易理解的,不過(guò)由于從磁盤(pán)加載內(nèi)核過(guò)程中涉及到了大量的磁盤(pán)操作,而這些操作過(guò)于底層化,同時(shí)使用了c語(yǔ)言嵌套匯編(inb, insb等),這些函數(shù)的定義全部在<inc/x86.h>中,感興趣的話可以去閱讀。
以上內(nèi)容就是JOS啟動(dòng)的過(guò)程,其中主要涉及了實(shí)模式向保護(hù)模式的轉(zhuǎn)變以及內(nèi)核加載的過(guò)程。內(nèi)容還是相對(duì)容易理解的。在下一篇文章中,將會(huì)涉及JOS內(nèi)存機(jī)制的建立。我也會(huì)按照MIT6.828實(shí)驗(yàn)的順序依次寫(xiě)完。加油:{
PS:如果有想一同學(xué)習(xí)內(nèi)核/JOS的童鞋,歡迎聯(lián)系:zfzhang1992@126.com