JOS 内核启动过程


开机引导程序bootstrap

PC 的地址分布表layout:

 +------------------+  <- 0xFFFFFFFF (4GB)
 |      32-bit      |
 |  memory mapped   |
 |     devices      |
 |                  |
 /\/\/\/\/\/\/\/\/\/\
 /\/\/\/\/\/\/\/\/\/\
 |                  |
 |      Unused      |
 |                  |
 +------------------+  <- depends on amount of RAM
 |                  |
 |                  |
 | Extended Memory  |
 |                  |
 |                  |
 +------------------+  <- 0x00100000 (1MB)
 |     BIOS ROM     |
 +------------------+  <- 0x000F0000 (960KB)
 |  16-bit devices, |
 |  expansion ROMs  |
 +------------------+  <- 0x000C0000 (768KB)
 |   VGA Display    |
 +------------------+  <- 0x000A0000 (640KB)
 |                  |
 |    Low Memory    |
 |                  |
 +------------------+  <- 0x00000000


BIOS:
从上图可以看出,BIOS的地址是从0xf0000-0x100000(1M),这时候是实模式(real mode)寻址空间为20位,1M.具体物理地址的计算方法为physical address = 16 * segment + offset 。Intel把8088处理器设计成当PC 上电以后cs:ip的值固定为0xffff0,BIOS“硬连接”到地址为0x000f0000-0x000fffff的范围。
[f000:fff0] 0xffff0:    ljmp   $0xf000,$0xe05b

上电后执行的第一条指令一般是转移指令,因为只差16字节就到了寻址空间的最大处了,这16个字节的空间几乎什么都不能做,所以,一般回跳到更小的地址。比如这里的0xfe05b。

BIOS执行时会建立起一个中断描述符表(这个中断描述符表貌似不是以后的中断描述符表,也就是说是暂时的)、初始化各种类型的设备(比如VGA显示器)。当初始化PCI总线和所有BIOS检测到的重要的设备之后,BIOS会去查找一个可以启动的设备(硬盘,光盘,软盘,U盘等等)。找到之后,会把该设备的boot loader读进内存并且把控制转移给boot loader。对于硬盘启动器,这时就是把硬盘的第一个扇区(512B)加载进内存(加载到从地址为0x7c00开始的内存段),并且从0x7c00开始执行。0X7c00是行业标准。


The Boot Loader
Boot loader,顾名思义,就是在boot阶段去load。load什么呢?当然是kernel了!
对!在这个阶段,计算机要做的事情是把BIOS找到的启动盘里的操作系统内核加载进内存并且开始执行内核。因为所有程序只有加载进内存了才可能被执行(BIOS程序例外,因为硬件专门给BIOS分配了一段地址空间),所以,这个愿望的产生应该很容易理解了。
内核其实就是一个可执行程序,格式是elf。它被固定的放在boot loader所在扇区的后一个扇区。一般认为boot loader 小于512B,所以可以放在第一个扇区之内,而内核文件则从第二个扇区放起,至于到哪里结束,这就不一定了,但是在elf头中有指明,所以只要拿到elf的头就可以了。但是现在的boot loader越来越大,一个扇区可能已经装不下了,这种情况应该在写boot loader的时候会解决,这里不讨论它。
BIOS 把bootloader程序所在的扇区固定加载到0x7c00-0x7dff(512B),并且用一个jmp指令把CS:IP设置成0000:7c00. CPU的控制权就从BIOS传递到了bootloader。
前面说过,bootloader的任务是把真正的内核程序从硬盘读到内存当中,那么,读到哪里呢?读到从地址为0x100000(1M)开始的内存里!可是这个时候的cpu寻址空间只有20位,1M!这个时候处于实模式下,是不能访问高于1M 的地址空间的!怎么办?只能进入保护模式了,因为在保护模式下可以寻址高于1M 的空间。

所以,bootloader 实际做了两件事:进入保护模式和加载内核程序。


进入保护模式:
  lgdt    gdtdesc
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0


这里:
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULL                # null seg
  SEG(STA_X|STA_R, 0x0, 0xffffffff)    # code seg
  SEG(STA_W, 0x0, 0xffffffff)            # data seg

gdtdesc:                #48bit in total
  .word   0x17                            # sizeof(gdt) - 1(lower 16bit)
  .long   gdt                             # address gdt (higher 32bit)


从这里可以看出,内核只设置了数据段和代码段,并且起始地址都设为0x0,limit都设为最大值0xffffffff。也就是说,保护模式内寻址时,线性地址等于offset,在没有开启分页机制时,这个地址也等于物理地址。三种地址的关系见下图:

JOS 内核启动过程_第1张图片

bootloader分为两部分,开启保护模式是用汇编语言写的,读磁盘文件是用C语言写的。在进入C 语言环境之前还有一件事情要做,那就是设置堆栈。本着简单明了,又不浪费内存的原则,可以把bootloader的开始地址(start/0x7c00)设为sp的值。由于栈是向下增长的,所以不会和bootloader的代码相冲突。

另外,将控制保护模式开启的CR0_PE_ON置位以后,并没有真正进入保护模式,因为CS 、DS等的值还是原来的值。在movl    %eax, %cr0 语句之后不可能再用另外一个语句来设置CS的值了,因为下一步的寻址将是按照保护模式的寻址方式了,而CS 原来的值自然就不对了,从而会导致出错。一个很好的解决方案是使用一条ljmp语句来实现:

ljmp    $PROT_MODE_CSEG, $protcseg。

当然,DS 的值就可以直接用汇编语句设置咯:
 movw    $PROT_MODE_DSEG, %ax    # Our data segment selector
  movw    %ax, %ds                # -> DS: Data Segment
  movw    %ax, %es                # -> ES: Extra Segment
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS
  movw    %ax, %ss                # -> SS: Stack Segment


至此,就可以安心的去执行C语言代码了:

call bootmain

进入用C 语言编写的读内核代码的程序了。

加载内核:


首先加载包含内核的第一个扇区到0x10000,注意,不是1M,因为这里要得到的只是elf头,不是真正的内核代码。
在验证魔数之后,再通过读取elf头里面的参数(包括内核代码的偏移地址,内核代码的长度,内核代码加载到内存的位置等等)去加载真正的操作系统内核(elf里面的一个program)。
最后,跳转到elf头里面指定的e_entry所在的位置进行执行。
((void (*)(void)) (ELFHDR->e_entry))();


至此,内核启动了!大 功 告 成!!

你可能感兴趣的:(c,汇编,layout,null,语言,byte)