admin 管理员组

文章数量: 887021

说起操作系统是如何启动的,首先有必要了解一下操作系统诞生的历史背景。通过了解历史背景,我们才能明确操作系统基本的工作逻辑。

操作系统诞生的历史背景

1936年,著名计算机学家图灵提出了图灵机的架构,控制器通过读写数据实现控制和运算的功能。后来,又提出了通用图灵机的架构,相比图灵机,通用图灵机可以写入控制器的逻辑,通过改变控制器逻辑,实现多种功能。

1946年,著名数学家冯·诺伊曼提出了冯·诺依曼结构,也称普林斯顿结构。这是一种将程序指令存储器和数据存储器合并在一起的存储器结构。程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同。同时他也提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构。

通过了解操作系统诞生的历史背景(这里只是两件最具代表性的历史事件,感兴趣的小伙伴可以自行查阅资料去了解其他的历史),我们知道计算机的工作逻辑就是取指执行,即从存储器中取指令并执行取到的指令,这也是操作系统的核心思想,其具体过程如图:

存储在硬盘等存储设备中的程序首先通过总线传入内存。CPU作为计算机的“大脑”,需要通过程序计数器PC(即图中IP)从内存中读取指令,然后解释并执行指令。

操作系统的宏观认知

操作系统到底是什么呢?操作系统本质上是一款软件,不过这款软件与我们平时用的QQ、浏览器等软件有着本质区别。如何理解呢?

计算机是基于CPU、内存、磁盘以及键盘、鼠标、显示器等硬件设备组成的一个设备,用户的大部分需求也是通过这些硬件设备来最终呈现的。操作系统的作用就是将用户在上层发出的一些需求,通过一些API接口与硬件设备建立联系,实现预期的功能。即操作系统是一款建立在APP与硬件设备之间的软件,功能十分强大,同时也很复杂。

操作系统的启动过程

操作系统的启动是从开启电源开始的,那么通电后操作系统执行的第一条指令是什么呢?

引导扇区

通过第一部分的讲解,我们已经了解到操作系统的核心思想是取指执行。当接通电源后,内存中固化了一段代码(这段代码无需从存储器中读取)。这段代码的执行流程如下:

1.设置段地址cs=0xFFFF(0x表示16进制),偏移地址ip=0x0000;

2.系统当前处于实模式,寻址方式为pc=cs<<4+ip(cs左移4位加ip),此时pc=0xFFFF0,对应着ROM BIOS映射区(BIOS,Basic Input Output System,基本输入输出系统);

3.检查RAM,键盘,显示器,磁盘;

4.将0磁0扇区(共512字节)读入0x7C00地址处(这就是引导扇区);

5.设置段地址cs=0x07C0,ip=0x0000(【想一想为什么设置成这个值?】可以根据第2条计算一下其pc值就明白了。pc=cs<<4+ip=0x7C00,系统接下来从0x7C00开始执行,即引导扇区的地址)。

引导扇区的代码bootsect.s是一段汇编代码,这个扇区的主要作用及实现方式概括如下:

1.将0x7C00的512字节移动到0x90000处。why?在后续的执行过程中,setup模块会将system模块移动到0x0000地址处,来布置操作系统。若操作系统过长,会使其与bootsect模块发生地址冲突,因此预留一段空间。

2.执行BIOS13号中断,在当前地址处偏移512字节,即0x90200处读入setup模块的4个扇区;

3.执行BIOS10号中断,显示LOGO;

4.设置一个单独的函数,再执行BIOS13号中断,读入system扇区。why单独设置一个函数?因为system扇区可能比较大,甚至跨磁道。

5.转入setup模块执行。

setup模块

setup共有4个扇区,主要实现两个功能:完成启动操作系统前的设置、接管硬件、初始化;进入保护模式。

1.在setup.s文件中,执行以下操作,实现设置及接管硬件的功能:

执行BIOS15号中断,得到扩展内存(1M之后的内存)的大小,存入0x90002地址处,以便管理内存;
执行BIOS10号中断,得到光标信息,存入0x90000地址处;
······
将system模块的代码移动到0地址处,这也是操作系统执行的起点地址。

2.进入保护模式:

在setup.s文件的最后,通过jmpi 0,8指令使得操作系统进入保护模式。大家可能会有疑惑,在实模式下这条指令的执行逻辑是,段地址cs设为8,偏移地址ip设为0,此时pc=cs<<4+ip=80,而0x0080地址并不是system最开始的地址0x0000(前文说到了setup模块将system模块移到了0x0000地址处),因此一定会执行错误。

这是因为此时操作系统的寻址方式已经改变了。实模式下,cs和ip都是16位的寄存器,这就导致最高的地址只能是20位(cs<<4+ip),因此在正常运行操作系统时需要改变其寻址方式。如何改变?通过cr0寄存器。将cr0寄存器中的最后一位置为1,此时操作系统将进入保护模式。

在保护模式下,cs不再是段地址,而是选择子,作为查GDT表(Global Description Table)的索引,通过GDT表确定物理内存。在setup模块初始化时对GDT表进行了初始化,代码如下:

GDT: .word  0,0,0,0
      .word  0x07FF,0x0000,0x9A00,0x00C0
      .word  0x07FF,0x0000,0x9200,0x00C0

GDT表中每一行数据是444=64位,占8B字节。因此jmpi 0,8指令实际上选择了GDT表中的第二行。GDT表项如下图:

将GDT表第二行的数据一次填入GDT表项中,取红字部分作为物理内存的地址。最终确定物理内存的地址为0x0000,这个确定的过程由硬件完成。

在保护模式下,中断的确定也不再是BIOS中断,而是通过查IDT表(Interrupt Description Table)确定。

system模块

system模块包括head.s和main.c。

在head.s文件中,对保护模式再次进行了初始化,设置一些数据结构(在setup模块中的初始化是为了转到保护模式而进行的初始化,因此在head.s中需要再初始化),也将GDT、IDT表进行了初始化,最后跳到main.c函数执行。

在从head.s跳到main.c的过程中,涉及到了汇编文件到C文件的转换。这里补充一点关于函数调用的知识。看如下代码:

A(p1,p2,p3){  B();  100:  ······  }

这里涉及到函数调用的知识,可以看我的博客【操作系统】用户级线程(协程)执行原理

操作系统为A函数分配一个栈帧。执行A函数时,将p1,p2,p3从后往前依次压栈。调用B函数之前,首先将B函数下一行执行的地址(100)作为返回地址压栈,再跳转到B函数执行。B函数(B函数也有自己的栈帧)执行到最后的右大括号}时,执行ret指令,将A栈帧的返回地址弹出,设置pc=100,此时程序将继续从100地址处执行A程序接下来的指令。

main.c函数的三个参数分别是envp,argv,argc(在main函数并没有用到)。依照上面的原理,执行main函数时,首先将envp,argv,argc压入main函数的栈帧中,再压入一个L6地址。当main函数调用了其他函数时,将返回地址压入栈帧中,再去执行调用函数。

这里L6地址的指令为L6: jmp L6,是死循环命令,这也就要求main函数一直处于运行过程中,即操作系统一直在工作状态。事实上,操作系统在启动后也是一直在工作状态的。

操作系统需要将源码有序组织成一个Image镜像文件,进而实现预期功能。这就是著名的Makefile。现在依次有了bootsect、setup、system模块及其对应的.s或.c文件,通过build就可以构建Image镜像文件。操作系统也就正式启动了!!!

参考书籍:《Linux内核0.11完全注释》(赵炯)

参考视频:操作系统(哈工大李治军老师)32讲-2,3

本文标签: 操作系统 这一 也能 详解 看得懂