JOS(1) BootLoader

JOS在啟動的過程(bootload過程)中一共經歷了三個階段,分別是BIOS加載以及硬件檢測、實模式向保護模式的轉變以及加載kernel。故本文組織過程也按照啟動過程依次敘述。

一、BIOS加載

計算機系統在電源鍵開啟后,首先接管整個系統的是BIOS。BIOS主要完成以下幾個內容:

  • 對計算機的硬件進行檢測
  • 加載啟動文件

在更進一步的敘述BIOS之前,我們需要簡單的了解x86的物理地址空間。下圖是一個典型的x86地址空間,這里需要注意的是,BIOS的內存空間是從0x000F 0000 ~ 0x0010 0000共64KB。在啟動電源后,BIOS將會加載到上述的內存空間內。

繪圖1.jpg

按照慣例,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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. 背景 原本計劃自己學習寫個操作系統的,但是工欲善其事必先利其器,先學習下別人是怎么做出來的,自己再動手,自然...
    pingpong_龘閱讀 17,464評論 5 16
  • 一、溫故而知新 1. 內存不夠怎么辦 內存簡單分配策略的問題地址空間不隔離內存使用效率低程序運行的地址不確定 關于...
    SeanCST閱讀 7,880評論 0 27
  • 轉載聲明:本文雖然不是本人100%原創,但也是辛辛苦苦整理的,可以轉載,但請注明出處 這篇文章是關于折騰Windo...
    SOMCENT閱讀 8,119評論 3 37
  • 無非是一堆虛妄之火 燃就的一抔矜持的灰燼 2017.6.6
    查文瑾閱讀 508評論 2 4
  • 五顏六色的紙片脫離了黑色,展現著獨特的顏色。五顏六色的花瓣脫離了花蕾,凋謝時換得了憐憫。五顏六色的彩虹脫離了天空,...
    bo_ss閱讀 197評論 0 0