JOS在啟動的過程(bootload過程)中一共經歷了三個階段,分別是BIOS加載以及硬件檢測、實模式向保護模式的轉變以及加載kernel。故本文組織過程也按照啟動過程依次敘述。
一、BIOS加載
計算機系統在電源鍵開啟后,首先接管整個系統的是BIOS。BIOS主要完成以下幾個內容:
- 對計算機的硬件進行檢測
- 加載啟動文件
在更進一步的敘述BIOS之前,我們需要簡單的了解x86的物理地址空間。下圖是一個典型的x86地址空間,這里需要注意的是,BIOS的內存空間是從0x000F 0000 ~ 0x0010 0000共64KB。在啟動電源后,BIOS將會加載到上述的內存空間內。
按照慣例,BIOS將會被加載到CS:0xF000,IP:0xFFF0 處,即0x000F FFF0。由于此地址已經非常接近于0x0010 0000,留給BIOS執行的內存空間過少,所以BIOS加載執行的第一條指令就是:
ljmp $0xF000, $0xE05B
將自己移動到較低的地址空間,以保證足夠的內存使其能夠繼續執行。在BIOS運行的過程中,將設立終中斷述表以及初始化各種設備,當完成上述過程后,BIOS搜索能夠啟動的設備(如軟盤、硬盤、CD-ROM等),對該設備的第一個扇區進行讀取,最后將控制權轉移給扇區中存放的bootloader。
這里需要說明的是:(1)在上述啟動的過程中,CPU是運行在實模式下; (2)對于硬盤來說,最基本存儲單元是扇區(sector),每個扇區容量為512個字節。對于一個可啟動的硬盤,其第一個扇區必須是bootloader,故bootloader不能占據過大的空間。
二、保護模式
在JOS中,bootloader將完成兩部分工作:
- 實模式轉變為保護模式
- 加載內核文件
在這部分中,我們將簡要的敘述實模式向保護模式的轉變過程(/boot/boot.S)。保護模式下,系統將具有更大的尋址空間,并提供虛擬內存、分段、分頁等機制。保護模式下的JOS介紹將會在后續的文章中介紹。
在這里需要注意的是,開啟保護模式涉及到了cr0寄存器下的PE位(即第0位),當PE置1后,CPU將開啟保護模式,此時保護模式下的分段保護機制將會被一同開啟(分頁機制沒有開啟),故在開啟保護模式前,需要設置好全局描述符表。
JOS中對于保護模式的開啟有以下代碼:
lgdt gdtdesc # 加載全局描述符表
movl %cr0, %eax
orl $CR0_PE_ON, %eax # CR0_PE_ON = 0x1
movl %eax, %cr0 # 開啟保護模式
gdt為預先設定好的全局描述符表。對于全局描述符表的介紹將會在下一篇文章中涉及,在這里需要知道的是,全局描述符表的第0項存儲的內容必須為空(即為0)
gdt:
SEG_NULL # 空項
SEG(STA_X | STA_R, 0x0, 0xffffffff) # 代碼段
SEG(STA_W, 0x0, 0xffffffff) # 數據段
gdtdesc:
.word 0x17 # 在全局描述符表中共設置了三項,每項8字節,共24字節,故在此處設為(24-1),即0x17
.long gtd
對于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中,將代碼段和數據段全部映射到了4GB內存空間中,這對于啟動過程來說,是完全夠用的。
三、加載內核
這一部分中,主要完成的工作就是將內核文件加載到內存中(/boot/main.c),并將控制權限交給內核。在更進一步的介紹之前,首先闡述ELF文件格式。對于ELF文件格式的定義在<inc/elf.h>中。我們無需深入的了解ELF文件格式(如希望深入了解的話,在MIT6.828的指定文獻中列出了ELF文件的詳細格式內容),實際上來說,ELF類似于一個超大的“結構體”,每一個部分都存放了一定的內容,而對于該內容的描述在“頭部”中存放。這里給出了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; // 運行該程序需要的體系結構
uint32_t e_version; // 文件版本
uint32_t e_entry; // 程序入口地址
uint32_t e_phoff; // Program header table在文件中的偏移量(以字節計數)
uint32_t e_shoff; // Section header table在文件中的偏移量
uint32_t e_flags; // 對于IA32來說,計為0
uint16_t e_ehsize; // 表示ELF header大小
uint16_t e_phentsize; // Program header table中每一項目的大小
uint16_t e_phnum; // Program header table有多少個項目
uint16_t e_shentsize; // Section header table中每一項目的大小
uint16_t e_shnum; // Section header table有多少個項目
uint16_t e_shstrndx; // 包含節名稱的字符串是第幾個節(0開始計數)
};
struct Proghdr {
uint32_t p_type; // 當前Program header所描述的段的類型
uint32_t p_offset; // 段的第一個字節在文件中的偏移
uint32_t p_va; // 段的一個字節在內存中的虛擬地址
uint32_t p_pa; // 在物理內存定位的相關系統中,此項是為物理地址保留的
uint32_t p_filesz; // 段在文件中的長度
uint32_t p_memsz; // 段在內存中的長度
uint32_t p_flags; // 與段相關的標志
uint32_t p_align; // 根據此值來確定段在文件以及內存中如何對齊
};
有了上述的認識,就可以很容易的讀懂下述代碼。下述代碼主要是將內核讀取到磁盤中,并最后將控制權移交給內核。
#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
// 可以看出,內核加載于0x10000處之上,一共加載了512字節 * 8 = 4K,即分頁模式下一個完整的頁的大小
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
// is this a valid ELF? JOS中要求ELF文件的第一項必須為ELF_MAGIC
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// load each program segment (ignores ph flags)
// 加載代碼段, 可以看出每個代碼段都規定了加載的位置以及大小
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!
// 移交控制權,e_entry即為入口函數, 此函數不會返回,如果返回則意味著執行出現了某種問題,此后系統進入死循環,需要手動重啟
((void (*)(void)) (ELFHDR->e_entry))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
}
對于內核加載部分還是很容易理解的,不過由于從磁盤加載內核過程中涉及到了大量的磁盤操作,而這些操作過于底層化,同時使用了c語言嵌套匯編(inb, insb等),這些函數的定義全部在<inc/x86.h>中,感興趣的話可以去閱讀。
以上內容就是JOS啟動的過程,其中主要涉及了實模式向保護模式的轉變以及內核加載的過程。內容還是相對容易理解的。在下一篇文章中,將會涉及JOS內存機制的建立。我也會按照MIT6.828實驗的順序依次寫完。加油:{
PS:如果有想一同學習內核/JOS的童鞋,歡迎聯系:zfzhang1992@126.com