操作系统启动篇--01
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统启动篇--01相关的知识,希望对你有一定的参考价值。
操作系统启动篇--01
本系列参考哈工大MOOC整理而来
- 本系列需要汇编语言作为前置基础,不清楚汇编的,可以读一下: 汇编语言导学篇—01
计算机起源
从白纸到图灵机
计算机怎么工作? 说到底就是一个计算模型
1936年,英国数学家A.C.图灵提出了一种模型
此时的控制器还无法自动进行计算,而是通过提前做法结果集映射,通过查表快速计算出的结果
从图灵机到通用图灵机
- 只会做一道菜的厨师没有竞争力,一个能看懂菜谱并按照菜谱制作菜的厨师才有竞争力
- 普通的图灵机就像看不懂菜谱的厨师,只会做番茄炒蛋。
- 而通用图灵机则是能够读懂菜谱的厨师,可以读取读取不同的菜谱,制作出不同的菜肴
- 而菜谱则对应计算机世界中的程序,通过载入不同的程序,从而去解释执行不同的程序,获得不同的效果
最后总结出来的思想就是大名鼎鼎的冯诺依曼思想
从通用图灵机到计算机
一个伟大的发明: 冯·诺依曼存储程序思想
- 存储程序的主要思想:将程序和数据存放到计算机内部的存储器中,计算机在程序 的控制下一步一步进行处理
- 计算机由五大部件组成:输入设备、输出设备、存储器、运算器、控制器
程序其实是由一堆指令组成的,因此程序载入后的解释执行的过程,其实总结就是四个字: “取指执行”
打开电源,计算机执行的第一句指令什么?
上面说了,计算机本质就是取指执行,那么计算机一插上电,就应该去取指执行才对,而去哪里取指令,这个由CS段寄存器和IP寄存器的初始值决定。
ROM只读存储器中的代码是生产过程中直接写入的,因此刚插上电的时候,内存中唯一有代码的也是这块区域,因此CS和IP的初始值默认也是被设置为了执行该块代码区域的起始位置。
Bios主要负责对硬件系统检测和初始化程序。
初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。(不清楚看上面汇编链接)
硬件系统检测和初始化完成后,调用int19h进行操作系统的引导,即去将磁盘0磁道0扇区读入0x7c00处,然后将CS和IP位置重新指向操作系统的代码起始处。(计算机交给操作系统来管理)
0磁道0扇区是操作系统的引导扇区,一共512字节
0x7c00处存放的代码
- 就是从磁盘引导扇区读入的那512个字节
- 引导扇区就是启动设备的第一个扇区,开机时按住del键可进入 启动设备设置界面,可 以设置为光盘启动!
- 启动设备信息被设置在CMOS中…,CMOS: 互补金属氧化物半导 体(64B-128B)。用来存储实 时钟和硬件配置信息。
- 因此,硬盘的第一个扇区上存放着开机 后执行的第一段我们可以控制的程序
引导扇区代码: 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
这一部分主要做了两件事:
- 读取内存处指定位置保存的开启界面数据,打在屏幕上
- 继续读取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的主要内容,如果未能解决你的问题,请参考以下文章
(计算机组成原理)第四章指令系统-第二节3:数据寻址之偏移寻址(基址寻址变址寻址和相对寻址)