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