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
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
BIOS执行时会建立起一个中断描述符表(这个中断描述符表貌似不是以后的中断描述符表,也就是说是暂时的)、初始化各种类型的设备(比如VGA显示器)。当初始化PCI总线和所有BIOS检测到的重要的设备之后,BIOS会去查找一个可以启动的设备(硬盘,光盘,软盘,U盘等等)。找到之后,会把该设备的boot loader读进内存并且把控制转移给boot loader。对于硬盘启动器,这时就是把硬盘的第一个扇区(512B)加载进内存(加载到从地址为0x7c00开始的内存段),并且从0x7c00开始执行。0X7c00是行业标准。
所以,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,在没有开启分页机制时,这个地址也等于物理地址。三种地址的关系见下图:
bootloader分为两部分,开启保护模式是用汇编语言写的,读磁盘文件是用C语言写的。在进入C 语言环境之前还有一件事情要做,那就是设置堆栈。本着简单明了,又不浪费内存的原则,可以把bootloader的开始地址(start/0x7c00)设为sp的值。由于栈是向下增长的,所以不会和bootloader的代码相冲突。另外,将控制保护模式开启的CR0_PE_ON置位以后,并没有真正进入保护模式,因为CS 、DS等的值还是原来的值。在movl %eax, %cr0 语句之后不可能再用另外一个语句来设置CS的值了,因为下一步的寻址将是按照保护模式的寻址方式了,而CS 原来的值自然就不对了,从而会导致出错。一个很好的解决方案是使用一条ljmp语句来实现:
ljmp $PROT_MODE_CSEG, $protcseg。
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 语言编写的读内核代码的程序了。
加载内核:
((void (*)(void)) (ELFHDR->e_entry))();