Bootloader引导
Bootloader部分的代码主要负责主核的启动,保护模式的设置等。代码主要在bootasm.S、bootmain.c中。
1.bootasm.S
bootasm.S主要工作就是设置CPU进入32位保护模式。
1.1 关主核CPU中断,清零各个段寄存器
1.2 使能A20地址线
为了访问1MB以上的内存空间,需要使能A20地址线。
1.3 设置GDT全局描述符表
GDTR是一个48位的寄存器,各个位域的意义如下:
其中32位线性基地址指的是GDT表的起始物理地址,表长度是GDT表的大小-1。来看看xv6用于启动引导的GDT内容:
GDT表中的每一项表示一个段描述符,用来表示一段内存的属性,每一项占8个字节:
特别注意的是:GDT表中第一项必须是空条目,否则会报错。
SEG_NULLASM和SEG_ASM的宏定义在asm.h中:
通过宏定义可知,黄色部分P=1,DPL=00,S=1,标明设置的是数据段或者代码段,非系统段。黄色部分G=1,D/B=1,表示段限长的单位是4KB。
SEG_ASM(type,base,lim)可以设置段类型,段基址,段大小。TYPE的具体设置如下:
再来看看xv6的GDT描述符项:
可以看出,代码段和数据段的基地址都是0,段大小是4GB。
1.4 使能保护模式
使能保护就是将CR0寄存器的PE位置1:
1.5 进入32位模式
最后一步进入32位模式,通过一个长跳转指令进入,这会使得处理器重新加载CS,这时CS中的值代表的是段选择子,段选择子的具体结构如下:
在xv6中的具体代码如下:
1.6 重新设置段寄存器
进入32位后的第一件事就是重新设置其他段寄存器:
如图所示,DS,ES,SS用的是数据段选择子,FS,GS段用的是空选择子。
1.7 设置栈指针
这里初始的栈指针被设置为0x7c00,也就是bootasm.S被加载的地方,然后就调用c函数bootmain:
2. bootmain.c
bootmain.c只干一件事,将elf格式的内核从硬盘加载到内存中,然后跳转到入口执行。
2.1 内核链接脚本
框里的地址表示的是内核代码段的链接(运行)地址,用AT()括起来的地址表示内核的加载地址即0x100000,表示内核的代码段、数据段等内容应该要拷贝到0x100000地址处,但是内核的链接地址是0x80100000,所以内核被启动时,会将虚拟地址0x80100000映射到0x100000地址处。
2.2 bootmain主要流程
- 首先从磁盘的第一个扇区开始读取4KB的数据到0x10000地址处,内核是从磁盘第一个扇区开始存放的。
- 校验是否是elf头。
- 解析elf文件,将代码段和数据段等信息拷贝到对应的加载地址处。
- 跳转到内核的入口代码处执行。
2.3 readseg函数实现
- 首先根据起始地址pa和大小count计算出结束地址epa。
- pa -= offset % SECTSIZE;这一句是为了计算出从硬盘拷贝扇区到内存中的实际起始地址。因为硬盘读数据是按扇区大小来读的,也就是每次至少512字节数据。但是offset并不一定是512的整数倍,所以要对pa做校正。同时,这里也有一个风险,那就是如果offset不是512倍数,那么pa就要往低地址处校正,但是在pa往下的地址中可能存在有用的数据,如果pa往下校正,就可能破坏这些有用的数据。
- offset = (offset / SECTSIZE) + 1;计算offset在实际哪个硬盘分区中。offset表示的是离内核起始处的偏移,而内核是从硬盘第一个扇区开始的,所以要加一。
- 然后就循环读数据,每次读一个扇区也就是512字节。注意这里有可能实际读的字节数比count多。
2.4 读硬盘数据流程
各个端口的解释:
- 0x1F7端口表示硬盘的状态。
- 0x1F2表示要操作的硬盘扇区数目。
- 0x1F3~0x1F6表示起始操作的硬盘扇区号。
- 0x1F0表示要写的或者要读的数据。
其中状态端口和扇区偏移端口的具体位域描述:
具体的硬盘操作知识请见《操作系统真相还原》3.5章节。
具体的读数据是通过insl函数完成的,由于每次通过数据端口只能读4个字节的数据,所以要读一个扇区的话要读(512 / 4)次。
2.5 处理elf文件
其中ph->paddr表示编译时为每个段分配的内存物理加载地址,起始地址在链接脚本中指定。
3. 内核加载后的内存分布
2020年12月30日 23:30 1F
屌