JOS(1) BootLoader

JOS在启动的过程(bootload过程)中一共经历了三个阶段,分别是BIOS加载以及硬件检测、实模式向保护模式的转变以及加载kernel。故本文组织过程也按照启动过程依次叙述。

一、BIOS加载

计算机系统在电源键开启后,首先接管整个系统的是BIOS。BIOS主要完成以下几个内容:

  • 对计算机的硬件进行检测
  • 加载启动文件

在更进一步的叙述BIOS之前,我们需要简单的了解x86的物理地址空间。下图是一个典型的x86地址空间,这里需要注意的是,BIOS的内存空间是从0x000F 0000 ~ 0x0010 0000共64KB。在启动电源后,BIOS将会加载到上述的内存空间内。

JOS(1) BootLoader_第1张图片
绘图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文件格式的定义在中。我们无需深入的了解ELF文件格式(如希望深入了解的话,在MIT6.828的指定文献中列出了ELF文件的详细格式内容),实际上来说,ELF类似于一个超大的“结构体”,每一个部分都存放了一定的内容,而对于该内容的描述在“头部”中存放。这里给出了JOS下中的定义以及解释。

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等),这些函数的定义全部在中,感兴趣的话可以去阅读。
以上内容就是JOS启动的过程,其中主要涉及了实模式向保护模式的转变以及内核加载的过程。内容还是相对容易理解的。在下一篇文章中,将会涉及JOS内存机制的建立。我也会按照MIT6.828实验的顺序依次写完。加油:{
PS:如果有想一同学习内核/JOS的童鞋,欢迎联系:[email protected]

你可能感兴趣的:(JOS(1) BootLoader)