本系列参考哈工大MOOC整理而来
计算机怎么工作? 说到底就是一个计算模型
1936年,英国数学家A.C.图灵提出了一种模型
此时的控制器还无法自动进行计算,而是通过提前做法结果集映射,通过查表快速计算出的结果
最后总结出来的思想就是大名鼎鼎的冯诺依曼思想
一个伟大的发明: 冯·诺依曼存储程序思想
程序其实是由一堆指令组成的,因此程序载入后的解释执行的过程,其实总结就是四个字: “取指执行”
上面说了,计算机本质就是取指执行,那么计算机一插上电,就应该去取指执行才对,而去哪里取指令,这个由CS段寄存器和IP寄存器的初始值决定。
ROM只读存储器中的代码是生产过程中直接写入的,因此刚插上电的时候,内存中唯一有代码的也是这块区域,因此CS和IP的初始值默认也是被设置为了执行该块代码区域的起始位置。
BIOS主要负责对硬件系统检测和初始化程序。
初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。(不清楚看上面汇编链接)
硬件系统检测和初始化完成后,调用int19h进行操作系统的引导,即去将磁盘0磁道0扇区读入0x7c00处,然后将CS和IP位置重新指向操作系统的代码起始处。(计算机交给操作系统来管理)
0磁道0扇区是操作系统的引导扇区,一共512字节
把一开始从引导扇区读入的512个字节挪动到0x9000:0x000处,肯定是为了腾出0x07c0:0x0000这段空间来干别的事情。
ip cs
jmpi go,INITSEG
jmpi会重新设置cs:ip,相当于不是段内跳转,而是段间跳转,这里是跳到了0X9000处go地址标记处继续执行
0x13中断会将setup处的四个扇区读入到0x9000:0x0200(512)处,即上面转移位置后的引导扇区后面
这一部分主要做了两件事:
最后,就是将system模块的代码读进内存,然后引导扇区程序执行结束,下面转入执行setup
jmpi 0,SETUPSEG
system模块被读取到了0x1000处
最后引导扇区执行结束后,设置ip=0,cs=SETUPSEG,SETUPSEG=0x9020,即setup扇区内存开始的地址
根据名字就可以想到: setup将完成OS启动前的设置
intel刚出来的时候,只有1M内存,因此把1M以后的内存称为扩展内存
将system模块移动到0地址处,读取到0x9000处结束
将system模块移动到0地址处,而引导模块和setup模块不需要移动是因为这两个模块一旦读取完毕后,就没用了,后面由system模块进行管理
上面setup模块读取完相关硬件参数然后将System模块移动到0地址处后,下面还需要做一件事,就是进入保护模式。
所谓进入保护模式: 主要是指setup模块最后执行的jmpi 0 8跳转指令。
大家猜猜这个跳转指令究竟会跳转到哪里呢?
操作系统必须严格按照顺序读取,先引导模块,再是setup模块,最后读取ststem模块,一旦读取过程中有一点偏差,就会死机
既然我们按照默认的cs:ip规则推导出来是死机的结果,显然这里一定存在猫腻,可能是cs:ip的寻址规则发生的改变,那么到底是不是这样呢?
cs<<4+ip -->最大达到20位地址,最大访问地址空间为1M。
但是,这里内存为4G,因此16位机已经无法满足需求了,需要切换到32位模式。
如何切换到32位机呢? ---->那我们需要思考16位机和32位机的本质区别是什么
32位模式也叫做保护模式
将cro赋值为1,即开启了保护模式
mov ax,#0x0001 mov cro,ax
gdt是用硬件来实现的,主要追求的是块,此时cs不再是左移4位产生一个地址,而是选择子。
以前cs里面存放的是代码段地址,而现在存放的是查表的下标,真正的段基址,存放在表项中。
因此上面cs为8是选择下标为8的表项,然后让该表项中存放的段基址和偏移地址ip相加,得到一个32的物理地址。
但是,查表之前,必须保证表中存放好了相关的段基址,否则不就查不到了,这个存放的问题下面会进行解答.
中断程序也是改为去查询IDT表,和GDT实现原理一样。
上面提到了,查询GDT表之前,需要先初始化该表,确保相关段基址已经保存好了,那么具体的初始化过程如下:
gdt表通过上面一番操作就已经初始化好了,再回顾一下上面那条jmpi指令:
此时jmpi 0,8 ,这里cs为8,会跳到GDT表的第2行。
GDT表项每个字节的含义如上,通过对比,可以得知,最终CS的值为0,加上偏移地址ip的值同样为0,因此最终是跳转到了0地址处,即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是其第一部分的代码。
虽然说main函数返回时,操作系统会进入死机状态,但实际上main函数永远都不会返回,因为操作系统需要一直处于运行状态。
main函数是不会退出返回的,上面给出的main代码还少了两句,具体可以参考linux 0.11源码。
这里我们来看看内存的初始化都干了啥
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函数负责初始化相关组件。
总结一句话: 先把操作系统从磁盘读入内存,然后再初始化,主要是建立相关数据结构,让操作系统知道硬件的样貌