接下来还是会做一些在进入保护模式之前的准备
27 # Switch from real to protected mode, using a bootstrap GDT 28 # and segment translation that makes virtual addresses 29 # identical to their physical addresses, so that the 30 # effective memory map does not change during the switch. 31 lgdt gdtdesc # 把关于GDT表的一些信息存放到CPU的GDTR寄存器中(包括起始地址+长度 32 movl %cr0, %eax 33 orl $CR0_PE_ON, %eax 34 movl %eax, %cr0这部分把gdtdesc送入全局映射描述符表寄存器GDTR中。GDT表是处理器工作于实模式下一个非常重要的表。这里的gdtdesc表示了一个标识符,标识这一个内存地址。从这个内存地址开始之后的6个字节分别存放着GDT表的长度和起始地址。
1 # Bootstrap GDT 2 .p2align 2 # force 4 byte alignment 3 gdt: 4 SEG_NULL # null seg 5 SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg 6 SEG(STA_W, 0x0, 0xffffffff) # data seg 7 8 gdtdesc: 9 .word 0x17 # sizeof(gdt) - 1 10 .long gdt # address gdt其中第3行的gdt是一个标识符,标识从这里开始就是GDT表了。可见这个GDT表中包括三个表项(4,5,6行),分别代表三个段,null seg,code seg,data seg。由于xv6其实并没有使用分段机制,也就是说数据和代码都是写在一起的,所以数据段和代码段的起始地址都是0x0,大小都是0xffffffff=4GB。
在第4~6行是调用SEG()子程序来构造GDT表项的。这个子函数定义mmu.h中,形式如下:
#define SEG(type,base,lim) \ .word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \ .byte (((base) >> 16) & 0xff), (0x90 | (type)), \ (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)gdb表中的每一个表项的结构如下所示
struct gdt_entry_struct { limit_low: resb 2 base_low: resb 2 base_middle: resb 1 access: resb 1 granularity: resb 1 base_high: resb1 } endstruc这个表项一共8字节,其中limit_low就是limit的低16位。base_low就是base的低16位,依次类推。
在gdtdesc处就要存放这个GDT表的信息了,其中0x17是这个表的大小-1 = 0x17 = 23,至于为什么不直接存表的大小24,根据查询是官方规定的。紧接着就是这个表的起始地址gdt。
在load完gdt表之后下面的操作就是进入保护模式之前的最后操作了
32 movl %cr0, %eax 33 orl $CR0_PE_ON, %eax 34 movl %eax, %cr0这里就是在修改CRO寄存器的值,其中CRO寄存器的bit0是保护模式启动位,把这一位设置成1代表保护模式启动。
35 ljmp $PROT_MODE_CSEG, $protcseg这里的跳转就表示跳转到保护模式。在保护模式就变成了32位地址模式
protcseg: # Set up the protected-mode data segment registers 36 movw $PROT_MODE_DSEG, %ax # Our data segment selector 37 movw %ax, %ds # -> DS: Data Segment 38 movw %ax, %es # -> ES: Extra Segment 39 movw %ax, %fs # -> FS 40 movw %ax, %gs # -> GS 41 movw %ax, %ss # -> SS: Stack Segment因为规定我们在加载完GDTR寄存器之后必须要重新加载所有的段寄存器。因此下面这些代码就是在加载段寄存器
随后我们就要为跳转到main.c文件中的bootmain函数做准备(因为boot.S的最后一条指令就是call bootmain)
跳转到main.c文件
在main.c文件做的第一件事就是把内核的第一个页读取到内存地址0x10000处。其实第一个页就是操作系统映射文件到elf。读取完内核的elf文件。关于elf文件的解释首先会通过魔数来判断一下这个内核是否合理。对应下面的代码
// read 1st page off disk readseg((uint32_t) ELFHDR, SECTSIZE*8, 0); // is this a valid ELF? if (ELFHDR->e_magic != ELF_MAGIC) goto bad;在elf文件中包含Program Header Table。这个表格存放着程序中所有段的信息。通过这个表我们才能找到要执行的代码段,数据段等等。所以我们要先获得这个表。
这条指令就可以完成这一点,首先elf表示elf表的起址,而phoff字段代表Program Header Table距离表头的偏移量。所以ph可以被指定为Program Header Table表头。
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);这里的eph表示一共有多少段。这段代码就是逐段把操作系统内核从硬盘中读到内存中
而后同样通过ELFHEADER的
((void (*)(void)) (ELFHDR->e_entry))();e_entry字段指向的是这个文件的执行入口地址。所以这里相当于开始运行这个文件。也就是内核文件。 自此就把控制权从boot loader转交给了操作系统的内核。
4. The Kernel对实验指导内容的一些翻译