操作系统启动篇--01

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统启动篇--01相关的知识,希望对你有一定的参考价值。

操作系统启动篇--01


本系列参考哈工大MOOC整理而来


计算机起源

从白纸到图灵机

计算机怎么工作? 说到底就是一个计算模型

1936年,英国数学家A.C.图灵提出了一种模型

此时的控制器还无法自动进行计算,而是通过提前做法结果集映射,通过查表快速计算出的结果


从图灵机到通用图灵机

  • 只会做一道菜的厨师没有竞争力,一个能看懂菜谱并按照菜谱制作菜的厨师才有竞争力
  • 普通的图灵机就像看不懂菜谱的厨师,只会做番茄炒蛋。
  • 而通用图灵机则是能够读懂菜谱的厨师,可以读取读取不同的菜谱,制作出不同的菜肴
  • 而菜谱则对应计算机世界中的程序,通过载入不同的程序,从而去解释执行不同的程序,获得不同的效果

最后总结出来的思想就是大名鼎鼎的冯诺依曼思想


从通用图灵机到计算机

一个伟大的发明: 冯·诺依曼存储程序思想

  • 存储程序的主要思想:将程序和数据存放到计算机内部的存储器中,计算机在程序 的控制下一步一步进行处理
  • 计算机由五大部件组成:输入设备、输出设备、存储器、运算器、控制器


程序其实是由一堆指令组成的,因此程序载入后的解释执行的过程,其实总结就是四个字: “取指执行”


打开电源,计算机执行的第一句指令什么?

上面说了,计算机本质就是取指执行,那么计算机一插上电,就应该去取指执行才对,而去哪里取指令,这个由CS段寄存器和IP寄存器的初始值决定。

ROM只读存储器中的代码是生产过程中直接写入的,因此刚插上电的时候,内存中唯一有代码的也是这块区域,因此CS和IP的初始值默认也是被设置为了执行该块代码区域的起始位置。

BIOS和DOS中断例程的安装过程

Bios主要负责对硬件系统检测和初始化程序。

初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。(不清楚看上面汇编链接)

硬件系统检测和初始化完成后,调用int19h进行操作系统的引导,即去将磁盘0磁道0扇区读入0x7c00处,然后将CS和IP位置重新指向操作系统的代码起始处。(计算机交给操作系统来管理)

0磁道0扇区是操作系统的引导扇区,一共512字节


0x7c00处存放的代码

  • 就是从磁盘引导扇区读入的那512个字节
  • 引导扇区就是启动设备的第一个扇区,开机时按住del键可进入 启动设备设置界面,可 以设置为光盘启动!
  • 启动设备信息被设置在CMOS中…,CMOS: 互补金属氧化物半导 体(64B-128B)。用来存储实 时钟和硬件配置信息。

操作CMOS RAM芯片

  • 因此,硬盘的第一个扇区上存放着开机 后执行的第一段我们可以控制的程序

引导扇区代码: bootsect.s


把一开始从引导扇区读入的512个字节挪动到0x9000:0x000处,肯定是为了腾出0x07c0:0x0000这段空间来干别的事情。

     ip   cs
jmpi go,INITSEG

jmpi会重新设置cs:ip,相当于不是段内跳转,而是段间跳转,这里是跳到了0X9000处go地址标记处继续执行


jmpi go, INITSEG



0x13中断会将setup处的四个扇区读入到0x9000:0x0200(512)处,即上面转移位置后的引导扇区后面


读入setup模块后: ok_load_setup



这一部分主要做了两件事:

  1. 读取内存处指定位置保存的开启界面数据,打在屏幕上
  2. 继续读取system模块

read_it —> 读入system模块


最后,就是将system模块的代码读进内存,然后引导扇区程序执行结束,下面转入执行setup

jmpi 0,SETUPSEG

system模块被读取到了0x1000处

最后引导扇区执行结束后,设置ip=0,cs=SETUPSEG,SETUPSEG=0x9020,即setup扇区内存开始的地址


操作系统启动

setup模块,即setup.s

根据名字就可以想到: setup将完成OS启动前的设置

  • 通过15号中断,读入扩展内存大小到0x90002处,操作系统需要知道当前PC机物理内存大小

intel刚出来的时候,只有1M内存,因此把1M以后的内存称为扩展内存


将system模块移动到0地址处,读取到0x9000处结束

  • 将操作系统的代码移动到0地址开始处,然后我们的应用程序代码就都放在操作系统代码上面去执行

将system模块移动到0地址处,而引导模块和setup模块不需要移动是因为这两个模块一旦读取完毕后,就没用了,后面由system模块进行管理


进入保护模式

上面setup模块读取完相关硬件参数然后将System模块移动到0地址处后,下面还需要做一件事,就是进入保护模式。

所谓进入保护模式: 主要是指setup模块最后执行的jmpi 0 8跳转指令。

大家猜猜这个跳转指令究竟会跳转到哪里呢?

  • 相信大家都能说出来,是跳转到system模块去执行,但是如果按照默认的cs<<4+ip的话,会跳转到80处,但是这可不是system模块的起始地址处,显然是一个非法地址,这样跳转只会导致死机。

操作系统必须严格按照顺序读取,先引导模块,再是setup模块,最后读取ststem模块,一旦读取过程中有一点偏差,就会死机

既然我们按照默认的cs:ip规则推导出来是死机的结果,显然这里一定存在猫腻,可能是cs:ip的寻址规则发生的改变,那么到底是不是这样呢?

  • 寻址方式发生改变

cs<<4+ip -->最大达到20位地址,最大访问地址空间为1M。

但是,这里内存为4G,因此16位机已经无法满足需求了,需要切换到32位模式。

如何切换到32位机呢? ---->那我们需要思考16位机和32位机的本质区别是什么

32位模式也叫做保护模式

  • 本质区别是对cs:ip的解释方式不一样,16位机情况下,会将cs:ip解释为cs<<4+ip
  • 32位机模式下,对应的解释程序需要用另外一条电路来实现,其实切换就是通过下面这条指令完成的
将cro赋值为1,即开启了保护模式
mov ax,#0x0001 mov cro,ax


保护模式下的地址翻译和中断处理

gdt是用硬件来实现的,主要追求的是块,此时cs不再是左移4位产生一个地址,而是选择子。

以前cs里面存放的是代码段地址,而现在存放的是查表的下标,真正的段基址,存放在表项中。

因此上面cs为8是选择下标为8的表项,然后让该表项中存放的段基址和偏移地址ip相加,得到一个32的物理地址。

但是,查表之前,必须保证表中存放好了相关的段基址,否则不就查不到了,这个存放的问题下面会进行解答.

中断程序也是改为去查询IDT表,和GDT实现原理一样。


将system移到0地址处…

  • 如果学习过x86汇编的小伙伴,肯定会产生疑问说,0地址处不应该用来存放中断向量表吗? 覆盖了0地址处的内容,后面的中断程序查表怎么搞呢?

上面提到了,查询GDT表之前,需要先初始化该表,确保相关段基址已经保存好了,那么具体的初始化过程如下:

gdt表通过上面一番操作就已经初始化好了,再回顾一下上面那条jmpi指令:

此时jmpi 0,8 ,这里cs为8,会跳到GDT表的第2行。

gdt每个表项占据64个字节


jmpi 0,8 //gdt中的8


GDT表项每个字节的含义如上,通过对比,可以得知,最终CS的值为0,加上偏移地址ip的值同样为0,因此最终是跳转到了0地址处,即system模块的开始处执行。


gdt是啥

引用: 关于GDT的理解

  • 为什么会出现GDT?

对于x86操作系统,由于电脑开机时是处于实模式状态下,在实模式状态下所有的内存都是允许被访问操作的,并且在实模式中只能操作20位的地址总线,也就意味着只能访问1MB大小的内存,而实模式下,使用的是16位的寄存器,所以在实模式下访问内存都需要一个16位段寄存器+16位偏移地址进行访问20位的地址空间。而到了32位系统时,则由于CPU无法访问高于1MB的内存就需要进行升级。此时就产生出了GDT。

  • 不考虑兼容性的情况下

假设下,如果不考虑向下兼容,则首先需要32位的地址总线,并且需要一个可以进行寻址的32位段寄存,并且由于寄存器和地址总线的位数相同,完全可以直接用寄存器的值进行寻址,而不需要使用段模式寻址。但是,这样会导致之前所有的程序都无法使用,因为过去原有的程序的寻址方式都发生了改变。并且由于32位寄存器可以访问32位的所有地址(也就意味着进程可以访问任意的物理地址),所以只要能控制该寄存器就可以访问任意的地址,也就对内存没有保护的作用。

  • GDT的产生

为了能够保证向下兼容并且能够对内存进行保护和划分,全局描述符表就产生了。

​ 我们知道在实模式下只能访问1MB的内存,并且使用的段寄存器+偏移地址的方式进行寻址,并且这种访存方式是不存在限制的。为了能够对内存的操作加以限制(也就对内存进行保护),首先想到的方式就是通过查询一个中间表G1,表G1存储了每个地址段的访问权限,例如程序A想要访问0x0F000的内存,则可以先查询表G1,查看该内存地址是否可以让程序A进行访问。通过这种方式就可以对内存进行保护,这样就解决了内存保护的问题。

​现在则需要在向下兼容的基础上访问更大的地址空间,由于向下兼容的缘故,所以段寄存器使用的是16位的,那么如何使用16位的段寄存器去寻址32位的内存空间呢?还是同样的方法,就是让段地址+偏移地址不在访问真实地址(这里指程序想要访问的物理地址),而是访问一个中间表G2,而G2则在内存中保存了想要访问的真实地址段,获取到真正的物理地址后由后续的32位寄存器进行访存操作就可以通过16位段寄存器完成4GB的内存访问。而将中间表G1和中间表G2合并就是全局描述符表(GDT)。而此时的段寄存器就不再是用于存储段地址了(此时段寄存器变为段选择子),而是用于存储访问GDT的下标,所以GDT的最大长度取决于段寄存器使用多少位来表示下标。

  • 总结

GDT保存了内存的访问权限、拓展高地址段、段地址大小等数据。在x86中,每个GDT项大小为64位,而段选择子使用13位用于偏移查询,所以GDT有8192(2的13次方)个GDT项。当采用段式内存管理时,则此时GDT项中的地址存储的就是真正的物理地址。而在页式管理中,GDT中存储的数据就不再是物理地址了,而是页表的地址和偏移(所以也把这种地址叫做线性地址);之后再从页表获取到的地址则是真正的物理地址。


跳到system模块执行…


我们知道操作系统0磁道0扇区一定是存放Boot扇区的代码,如果不是的话,那么一上来尝试去读取的时候就会产生不可预料的结果。

Boot扇区读取结束后,会去读取setup扇区的内容,最后是system扇区。

操作系统是由一堆源码组合而成的,但是只有在确保其组成是有序并且符合规定的,才能确保操作系统的正常运行。

但是如何确保大型软件的合成结构的呢? —> makefile

对于操作系统而言,除了要编写操作系统源码之外,还需要去编写操作系统的控制代码,即makefile.


将操作系统的一堆源代码交给makefile编译成一个Image镜像,然后放入0磁道0扇区中。

然后就是从0磁道0扇区开始去读取,完成操作系统的初始化和启动过程。

makefile是一种树状结构,其中各个父子模块之间存在大量依赖关系,makefile就是通过这些依赖关系来确保系统结构的正确性。

对于System模块来说,他会将他所依赖的模块都链接起来,组成system模块的内容。

对于system模块中依赖的各个模块而言,他们又会依赖其他子模块,例如: head.o模块会依赖head.s子模块。

system是由一堆.c文件组成的,这些.c文件经过链接后会形成一堆.o文件,这些.o文件链接起来组成system模块.

当boot,setup和system模块都组装好后,通过tools/build组成一个镜像.

而对于system模块而言,head.s是其第一部分的代码。


head.s //一段在保护模式下运行的代码


关于汇编…head.s的汇编和前面不一样?


after_page_tables //设置了页表之后


虽然说main函数返回时,操作系统会进入死机状态,但实际上main函数永远都不会返回,因为操作系统需要一直处于运行状态。


进入main函数

main函数是不会退出返回的,上面给出的main代码还少了两句,具体可以参考linux 0.11源码。


看一看mem_init…

这里我们来看看内存的初始化都干了啥


mem_init方法负责初始化相关内存页表,这里end_mem参数是setup阶段拿到的内存大小,该内存大小会存入90002的位置,而这里end_num实际就是该处的地址值。

将没有使用过的内存全部置空,而上面从0地址处开始使用过的一段内存就是上面移动到0地址处的system模块,也就是操作系统代码


小结

计算机启动读取BIOS,BIOS会去读取0磁道0扇区的boot扇区到内存中,boot扇区将setup模块和system模块读入内存。

setup模块获取到相关参数后启动保护模式。

head初始化gdt,idt表等,然后调用main函数

main函数负责初始化相关组件。

总结一句话: 先把操作系统从磁盘读入内存,然后再初始化,主要是建立相关数据结构,让操作系统知道硬件的样貌

高性能云服务器 精品线路独享带宽,毫秒延迟,年中盛惠 1 折起

以上是关于操作系统启动篇--01的主要内容,如果未能解决你的问题,请参考以下文章

操作系统启动篇--01

游戏升级64位基址找不到

Windows 服务(托管 WCF 服务)在启动时立即停止

(计算机组成原理)第四章指令系统-第二节3:数据寻址之偏移寻址(基址寻址变址寻址和相对寻址)

厨师:aws load_balancer_options 粘性

厨师:为啥跳过“include_recipe”步骤中的资源?