从cpu加电到加载OS内核的详细过程(清华大学ucore-lab1总结一)

时间:2021-09-12 12:16:43

      结合最近学习清华的OS课,先用“人话”来高度抽象的描述一下我自己的理解。CPU在系统加电也就是我们按下电源开关后,开始初始化他的寄存器,主要是cs和eip(本文基于x86架构),然后在ROM中找到一个叫BIOS(Basic Input Output System),加载到RAM中然后开始执行他,他在进行完设备的自检和初始化之后,就根据他自己内部的“我该去哪个设备启动加载程序”表,将其中第一个设备的主引导扇区加载到内存中来,也就是将系统的控制权转交给了这个程序,然后他在正确的引用了bootloader,也就是我们常说的“引导程序”,这个引导程序就会在内存的I/O区域中读入硬盘(os的位置)扇区的信息,得到操作系统内核的ELF文件,加载到内存中来,然后将控制权转交给OS,就大功告成了。


(一)关于BIOS的那些事

  1、我们的第一条指令的地址从何而来?

  cpu在加电之后首先会初始化寄存器,主要是CF寄存器和EIP寄存器,因为他们合起来组成了第一条指令的线性地址。

  The address FFFFFFF0H is beyond the 1-MByte addressable range of the processor while in real-address mode. The processor is initialized to this starting address as follows. The CS register has two parts: the visible segment selector part and the hidden base address part. In real-address mode, the base address is normally formed by shifting the 16-bit segment selector value 4 bits to the left to produce a 20-bit base address. However, during a hardware reset, the segment selector in the CS register is loaded with F000H and the base address is loaded with FFFF0000H. The starting address is thus formed by adding the base address to the value in the EIP register (that is, FFFF0000 + FFF0H = FFFFFFF0H).

【参考IA-32 Intel Architecture Software Developer’s Manual Volume 3: System Programming Guide Section 9.1.4】

  由于386的段base address不是像8086/8088那样直接存储在段寄存器中然后左移四位形成,而是通过在段寄存器中存入段选择子(也叫段选择符、段选择器),然后再查找GDT或LDT获取段的基地址和段界限以及属性等信息——为了避免每次都存储器访问时都进行上述的过程,在处理器的内部特地为每个段配置了一个76位的高速缓冲寄存器,称其为段描述符高速缓冲寄存器,这些寄存器对程序员是透明的(不可见的)。然后最开始的CS寄存器提供的基地址就是从这儿得来的。

从cpu加电到加载OS内核的详细过程(清华大学ucore-lab1总结一)

  2、BIOS都做了啥?

  第一条指令是一个跳转执行,跳转到BIOS的首地址去执行,系统将控制权转交给BIOS。BIOS首先进行硬件的自检和初始初始化之后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控 制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了bootloader。

  加载bootloader:

  bootloader的中文译名叫“引导程序”,他所在硬件的扇区(512B)叫“引导扇区(boot sector)”,这里简单的介绍下硬盘的扇区结构。

  硬盘是有很多个512bytes的扇区组成的,这些扇区又进一步划分为多个“分区”,每个分区拥有的扇区在物理上必须是连续的,每个分区的第一个扇区成为起始扇区或者引导扇区。主引导扇区(boot sector)位于硬盘的0磁头0柱面1扇区,由主引导记录(Master Boot Record)分区表(Disk Partition Table)组成。其中主引导记录的作用就是根据分区表检查分区是否正确以及确定哪个分区为引导分区,并在程序结束后将引导分区的起始扇区加载到内存中。

  从cpu加电到加载OS内核的详细过程(清华大学ucore-lab1总结一)

  每个起始扇区(包括主引导扇区)的最后两个字节都是“0X55AA”作为其结束的标志。


 

(一)关于bootloader的那些事  

  1、bootloader都干了啥

  • 切换到保护模式,启用分段机制
  • 读磁盘中ELF执行文件格式的ucore操作系统到内存
  • 显示字符串信息
  • 把控制权交给ucore操作系统

  2、实模式和保护模式

  一篇博文搞懂一切:http://www.cnblogs.com/immortal-worm/p/5867418.html

  3、关于A20Gate

  这里只从一个较高的抽象级给大家介绍一下这个概念。(A20Gate就是的cpu从低到高0计数开始数的第20根地址线)

  首先在实模式下,我们知道cpu只有20根地址线可以用来寻址,也就是说理论上我们最大的寻址范围是2^20也就是1111 1111 1111 1111 1111=1M的空间,第21位是1,后面的全是0。但是实际上我们都知道x86是通过【段寄存器*4+EIP寄存器】来得到线性地址的。很明显这样做的最大寻址空间就变成0xffff*4 + 0xffff = 0x10ffef也就是1 0000 1111 1111 1110 1111——这个值是大于理论上的1M的,那么在实模式下,我们对于超出1M的地址,引入了一种叫wrap-around的技术,即对于的超出的部分以1M位模进行取余,这样就不会出现越界的情况。此时我们不难发现在实模式下A20Gate是处于关闭状态的,原因很简单,因为它是第21根地址线,而实模式只打开了20根。

  那么现在bootloader要将实模式转换到保护模式下,首先要做的就是打开A20Gate,这里涉及到和键盘的8042芯片交互,在了解具体的打开方式之前,要先储备一点x86的IO端口相关的知识。

  基础知识之IO端口:http://www.cnblogs.com/immortal-worm/p/5867690.html

 

  多数PC都使用键盘控制器(8042芯片)来处理A20Gate。

  从理论上讲,打开A20Gate的方法是通过设置8042芯片输出端口(64h)的2nd-bit,但事实上,当你向8042芯片输出端口进行写操作的时候,在键盘缓冲区中,或许还有别的数据尚未处理,因此你必须首先处理这些数据。

 

  流程如下:

 

   1. 禁止中断;

 

   2. 等待,直到8042 Inputbuffer为空为止;

 

   3. 发送禁止键盘操作命令到8042Input buffer;

 

   4. 等待,直到8042 Inputbuffer为空为止;

 

   5. 发送读取8042 OutputPort命令;

 

   6. 等待,直到8042 Outputbuffer有数据为止;

 

   7. 读取8042 Outputbuffer,并保存得到的字节;

 

   8. 等待,直到8042 Inputbuffer为空为止;

 

   9. 发送Write 8042Output Port命令到8042 Input buffer;

 

   10. 等待,直到8042 Inputbuffer为空为止;

 

   11. 将从8042 OutputPort得到的字节的第2位置1(OR 2),然后写入8042 Input buffer;

 

   12. 等待,直到8042 Inputbuffer为空为止;

 

   13. 发送允许键盘操作命令到8042Input buffer;

 

   14. 打开中断。

  4、配置GDT表

  GDT表的详细介绍在前文保护模式中已经给出,现在我们只要用lgdt指令将gdt表的位置加载到dgtr寄存器中即可。

  5、使能保护模式

  386之后x86架构的cpu设置了控制寄存器,其中CR0的D0位决定了cpu访问内存的模式。在打开了A20Gate、配置好了GDT表之后,将其D0位置1即可。

   6、从硬盘中读取ELF格式的操作系统

  在使CPU进入到保护模式之后,就要开始从硬盘的相应位置去读取ELF格式的OS内核了,这个环节主要分两步,一个是从硬盘中读出ELF文件,然后从ELF文件的header进入读取整个os的内核。