开机引导程序bootstrap
PC 的地址分布表layout:
BIOS:+------------------+ <- 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的地址是从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是行业标准。
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,在没有开启分页机制时,这个地址也等于物理地址。三种地址的关系见下图:
另外,将控制保护模式开启的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 语言编写的读内核代码的程序了。
加载内核:
在验证魔数之后,再通过读取elf头里面的参数(包括内核代码的偏移地址,内核代码的长度,内核代码加载到内存的位置等等)去加载真正的操作系统内核(elf里面的一个program)。
最后,跳转到elf头里面指定的e_entry所在的位置进行执行。
((void (*)(void)) (ELFHDR->e_entry))();
至此,内核启动了!大 功 告 成!!