本系列是学习《Linux内核设计的艺术》等的读书笔记,有理解错误或不当的地方欢迎指出
该书选用Linux0.11源代码,虽然源码只有约两万行,但却是一个实实在在,不折不扣的现代操作系统。因为它具有现代操作系统最重要的特征:支持实时多任务,是后续版本的真正鼻祖。
该书出版于2013年,计算机发展到现在也快9年了。当初的软盘早已不见踪影,固态硬盘已成为电脑的必选硬件。CPU也从当年32位发展成为64位,许许多多的改变让我们对这庞大的系统无从入手。因此从简到繁是更好的选择。我很庆幸能发现这本书,让我学习到了许许多多的知识,而不是操作系统那抽象的理论知识。
电脑在没有通上电源的时候,内存里什么也没有,而CPU运行程序恰恰是在内存中取指令。当你打开开机键,给计算机通上电的那一瞬间。内存中没有存储任何指令,CPU该如何运行?
秘诀是:0xFFFF0
CPU厂商将CPU硬件逻辑设计位加电瞬间强行将CS(code segment 代码段寄存器)置为0xF000,IP(instruction pointer:指令寄存器)置为0xFFF0。CS左移4位与IP相加,CS:IP就指向了0xFFFF0这个位置,而该位置正是BIOS程序的入口地址。(该CPU寻址模式也称为实模式寻址方式,寻址总线为20位即最大支持1M的空间)
当CPU指向0xFFFF0时,意味着BIOS已经开始工作。此时IP指向的是BIOS芯片的ROM,内存仍然空空如也。随着BIOS程序的执行,BIOS会检测内存、CPU、显卡、硬盘等。但BIOS需要执行启动(boot)操作系统更为终重要的工作:在内存中建立中断向量表和中断服务程序。
BIOS在内存的最开始位置用1KB的内存空间构建中断向量表(0x00000~0x003FF)。用随后256字节的内存空间构建BIOS数据区(0x00400~0x004FF),并在大约57KB以后的位置(0x0E05B)加载了8KB左右的与中断向量表相对应的中断处理程序。
此时,计算机将要执行boot操作——把操作系统加载至内存。对于Linux0.11,将分三步加载操作系统的内核代码。第一步:由BIOS中断int 0x19把硬盘的第一扇区bootsect的512字节内容加载到内存,第二步、第三步:在bootsect的引导下,分别把其后的4个扇区和随后的240个扇区的内容加载到内存。
BIOS将该过程抽象,与操作系统无关。BIOS只关心“找硬盘”并“加载第一扇区”,其余不必知道。第一扇区中的程序由bootsect.s中的汇编程序汇编而成,随着第一扇区程序的载入,标志着Linux0.11代码即将发挥作用。由于当时计算机内存较小,需要对内存进行精细的规划,才有下面一系列的妙不可言的操作。
bootsect启动程序将自身从内存0x7C000(BOOTSEG)处复制到内存0x90000~0x901FF(INITSEG)。然后执行很神奇的汇编代码,将CS:IP跳转到INITSEG处按照原来的执行顺序继续执行。
jmpi go, INITSEG
go : mov ax, cs
接下来,bootsect程序将setup程序加载到内存中。 借助BIOS提供的int 0x13中断向量所指向的中断服务,将硬盘第二扇区开始的四个扇区即setup程序加载到内存的SETUPSEG(0x90200)处。随后还要加载240个扇区的代码,仍然使用BIOS提供的int 0x13中断。这次加载的扇区是之前的60倍,所消耗的时间也会增加几十倍。为了防止用户误以为机器故障,linus设计了显示一行屏幕信息"Loading system ..."以提示用户计算机正在加载系统。
bootsect借着BIOS中断int 0x13将240个扇区的system模块加载进内存的SYSSEG(0x10000)处往后120KB空间中。到此为止,操作系统的代码已全部加载进入内存,bootsect的主体工作已经做完,不过还需要再次确认根设备号,并将信息写入内存0x901FC中。
确认完根设备号后。bootsect程序的任务都已经完成。然后通过执行"jmpi 0, SETUPSEG"跳转到0x90200处,此时CS:IP指向setup程序的第一条指令。它要做的第一件事就是利用BIOS提供的中断服务程序从设备上提取内核运行所需的数据,并分别从中断向量0x41和0x46向量值所指向的内存地址处获取硬盘参数表1、2,把它们存放在0x9000:0x0080和0x9000:0x0090处。
这些数据被加载到内存0x90000~0x901FC,覆盖bootsect程序所在的部分区域。机器系统数据所占空间为0x90000~0x901FD,共510字节。即原来bootsect只有2字节未被覆盖,这种谨慎的内存规划风格是很值得学习的。到此为止,操作系统内核程序加载工作已经完成,系统将从实现从实模式到保护模式的转变,使Linux0.11真正成为现代操作系统。
先关闭中断,将CPU的标志寄存器(EFLAGS)中的中断允许标志(IF)置为0。程序在接下来的执行过程中,系统都不再对中断进行响应,直到保护模式下的中断服务体系被重建完毕才会打开中断。而那时,响应中断的服务程序不再是BIOS所提供的,取而代之的是由系统自身提供的中断服务程序。关中断,开中断,是为了保证程序运行的原子性,不会被打断,类似于进程的互斥锁。
setup程序做了一个影响深远的动作:将位于0x10000的内核程序(system模块)复制到内存地址起始位置0x00000处。
这个复制动作将BIOS建立的中断向量表以及BIOS数据区完全覆盖,操作系统不再具备中断响应处理能力。回收刚刚结束使用寿命的程序所占用的内存空间,让内核代码占据内存物理地址最开始,最天然、有利的位置。一箭三雕,神仙操作。
setup程序继续为保护模式做准备,对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置。随后打开A20,CPU可以进行32位寻址,最大寻址空间为4GB———(0x0000 0000~0xFFFF FFFF)。完成以上工作,setup程序将对中断控制器8259A重新编程,建立保护模式下的中断机制。int 0x00~int 0x1F被intel保留作为内部中断,不可屏蔽。
setup程序将CR0寄存器第0位(PE)置为1,将处理器工作方式设置为保护模式。CPU工作方式转变为保护模式,一个重要的特征就是要根据GDT决定后续执行哪里的程序。此时,setup的工作已经完成,后续工作将由head程序完成。
head程序与bootsect和setup加载方式不同,bootsect和setup这两段程序是分别加载、分别执行的。而head程序是先将head.s汇编成目标代码,将用C语言编写的内核编译成目标代码,然后链接成system模块。即system即包含head模块,也包含内核模块,head在前、内核模块在后。所以实际上,head程序就在0x00000这个位置上,在内存中占有2KB+184B的空间。
head程序所做是为了适应保护模式做准备。除了为main函数准备之外,head程序用自身程序代码在其所在的内存空间创建了内核的分页机制——即在0x000000位置创建了页目录表、页表、缓冲区、GDT、IDT,并将head程序已经执行过的代码所占内存空间覆盖,内存分布如下图所示。(head程序废弃原有的GDT,重设一套GDT)。head程序将L6标号和main函数入口地址压栈,栈顶为main函数地址,目的是使head程序执行完后通过ret指令就可以直接执行main函数。如果main函数异常退出,就会返回到标号L6处继续执行,此时还可以做一些系统调用。另外,main函数退出,如果还有进程存在,仍然可以进行轮转。
程序执行一般是通过call和ret指令配合使用完成函数调用,call指令会将EIP的值自动压栈,保护返回现场然后执行被调用的程序。等执行到被执行函数的ret指令时,自动出栈给EIP并还原现场,继续执行call下一条指令。但是操作系统已经是最底层的系统,调用main不需要返回。因此linus采用了伪call调用,手动压栈和跳转。
此时仍处于在关闭中断的状态!!!
本章主要分为两大部分。第一部分为加载操作系统;第二部分为32位保护、分页模式下的main函数执行做准备。从借助bios将bootsect.s文件加载至内存,相继加载了setup.s和system文件,从而完成操作系统的加载。然后设置IDT、GDT、页目录表、页表以及机器系统数据,为32位保护、分页模式下的main函数执行做准备,开始执行main函数。
该书的第一章已经结束,其中还有很多知识我没有弄明白,还有许多的汇编操作我都跳过没看,了解了一个大概的流程不关心其具体实现。现在,操作系统上的一大朵乌云开始慢慢离开。接着下一章节,设备环境初始化及激活进程0。