linux-0.11启动过程描述

简单描述

当PC的电源打开后,80X86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。PC机的BIOS将执行某些系统的检测,并在物理地址0处开始初始化中断向量。此后,它将可启动设备的第一个扇区(磁盘引导扇区,512字节)读入内存绝对地址0x7C00处,并跳转到这个地方。启动设备通常是软驱或是硬盘。

启动之前

Linux启动是需要启动盘的,这里假定启动盘就是当时的1.44MB的A盘。我们先来看下各个模块在启动盘的位置信息。

Linux 0.11内核在1.44MB磁盘上的分布情况

1.44MB磁盘共有2880个扇区(1.44*1000*1000/500=2880,要理解奸商的计算方式),bootsect.s代码是磁盘引导块程序,驻留在磁盘的第一个扇区中(引导扇区,0磁道(柱面),0磁头,第1个扇区),setup模块占用随后的4个扇区,而0.11内核system模块大约占随后的240个扇区。还剩下2630个扇区未被使用。这些剩余的未用空间可被利用来存放一个基本的根文件系统,从而可以创建处使用单张磁盘就能让系统运转起来的集成盘来。

启动过程

这里我们可以看到linux/boot/目录下有三个文件和启动相关。

BIOS和boot/bootsect.s

Linux最开始的部分是用8086汇编语言编写的(boot/bootsect.s),它将由ROM BIOS自检后读入到内存绝对地址0x7C00(31KB)处并执行之,bootsect执行时,就会把自己[1]移动到内存绝对地址0x90000(576KB)处,并把启动设备中后2KB字节代码(boot/setup.s)读入到内存0x90200处,然后利用BIOS终端0x13取磁盘参数表中当前启动引导盘的参数,接着在屏幕上显示“Load
system…”字符串。而后将system模块读入到内存地址0x10000(64KB)开始处,因为当时system模块的长度不会超过0x80000字节大小(即512KB),所以bootsect程序把system模块读入物理地址0x10000开始位置处时并不会覆盖在0x90000(576KB)处开始的bootsect和setup模块。随后确定根文件系统的设备号,若没有指定,则根据所保存的引导盘的每磁道扇区数判别出盘的类型和种类(是1.44MB A盘吗?)并保存其设备号于root_dev(引导块的508地址处),最后长跳转到setup程序的开始处(0x90200)执行setup程序。

从机器加电开始顺序执行的程序如下图:

从系统加点起所执行程序的顺序

boot/setup.s

boot/setup.s主要负责利用ROB BIOS终端读取机器系统数据,并将这些数据保存到0x90000开始的位置(覆盖掉了bootsect程序所在的位置),所去读取的参数和保留的内存位置如下表:(这些参数将被内核中的相关程序使用)

setup程序读取并保存的参数

然后setup程序将system模块移动到物理内存起始位置处[2],这样system模块中代码的地址也即等于实际的物理地址,便于对内核代码和数据进行操作。下图清晰地显示出Linux系统启动时这几个程序或模块在内存中的动态位置。其中,每一竖条框代表某一时刻内存中个程序的映像位置图。在系统加载期间将显示信息“Loading…”。然后控制权将传递给boot/setup.s中的代码,这是另一个实模式汇编语言程序。

启动引导时内核在内存中的位置和移动后的情况

接下来加载中断描述符表寄存器(idtr)和全局描述符表寄存器(gdtr),开启A20地址线,重新设置两个终端控制芯片8259A,将硬件中断号重新设置为0x20-0x2f。最后设置CPU的控制寄存器CR0(也称机器状态字),从而进入32位保护模式运行,并跳转到位于system模块最前端部分的head.s程序继续运行。

为了能够head.s在32位保护模式下运行,在本程序中临时设置了中断描述符表(IDT)和全局描述符表(GDT),并在GDT中设置了当前内核代码段的描述符和数据段的描述符。下面在head.s程序中会根据内核的需要重新设置这些描述符表。

boot/head.s

head.s程序在被编译生成目标文件后会与内核其他程序一起被链接成system模块,位于system模块的最前面开始部分,这也就是为什么称其为头部(head)程序的原因。system模块将被放置在磁盘上setup模块之后开始的扇区中,即从磁盘上第6个扇区开始放置。一般情况下Linux0.11内核的system模块大约有120KB左右,因此在磁盘上大约占240个扇区。

从这里开始,内核完全都是在保护模式下运行了。head.s汇编程序与前面的语法格式不同,它采用的是AT&T的汇编语言格式,并且需要使用GNU的gas和gld进行编译链接。因此要注意代码中赋值的方向是从左到右。

这段程序实际上处于内存绝对地址0处开始的地方。这个程序的功能比较单一。首先是加载各个数据段寄存器,重新设置中断描述符表idt,共256项,并使各个表项均指向一个只报错误的哑中断子程序ignore_int。中断描述符表中每个描述符项也占8字节,其格式如下:

中断描述符表IDT中的中断门描述符格式

在设置好中断描述符表之后,本程序又重新设置了全局段描述符表gdt。实际上新设置的GDT表与原来在setup.s程序中设置的GDT表描述符除了在段限长上有些区别以外(原为8MB,现为16MB),其他内容完全一样。当然我们也可以在setup.s程序中就把描述符的段限长直接设置成16MB,然后直接把原GDT表移动到内存适当位置处。因此这里重新设置GDT的主要原因是为了把gdt表放在内存内核代码比较合理的地方。前面设置的GDT表处于内存0x902XX处。这个地方将在内核初始化后用作内存高速缓冲区的一部分。

接着使用物理地址0与1MB开始处的字节内容相比较的方法,检测A20地址线是否已真的开启。如果没有开启,则在访问高于1MB物理内存地址时CPU实际只会循环访问(IP MOD 1Mb)地址处的内容,也即与访问从0地址开始对应字节的内容都相同。如果检测下来发现没有开启,则进入死循环。然后程序测试PC机是否含有数学协处理芯片(80287、80387或兼容芯片),并在控制寄存器CR0中设置相应的标志位。

接着设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处(也是本程序所处的物理内存位置,因此这段程序将被覆盖掉),紧随后面放置共可寻址16MB的4个页表,并分别设置它们的表项。页目录表项和页表项格式如下。

页目录表项和页表项结构

这里每个表项的属性标志都被设置成0x07(P=1、U/S=1、R/W=1),表示该页存在、用户可读写。这样设置内核页表属性的原因是:CPU的分页机制和分页管理都有保护方法。分页机制中页目录表和页表项中设置的保护标志(U/S、R/W)需要与段描述符中的特权级(PL)保护机制一起组合使用。但段描述符中的PL其主要作用。CPU会首先检查段保护,然后再检查页保护。如果当前特权级CPL<3(例如0),则说明CPU正在以超级用户(Supervisor)身份运行。此时所有页面都能访问,并可随意进行内存读写操作。入宫CPL=3,则说明CPU正在以用户(User)身份运行。此时只有属于User的页面(U/S=1)可以访问,并且只有标记为可读写的页面(W/R=1)是可写的。而此时属于超级用户的页面(U/S=0)则既不可写、也不可以读。由于内核代码有些特别之处,即其中包含有任务0和任务1的代码和数据。因此这里把页面属性为0x7就可保护这两种任务代码不仅可以在用户态下执行,而且又不能随意访问内核资源。

最后,head.s程序利用返回指令将预先放置在对战中的/init/main.c程序的入口地址弹出,去运行main()程序。

head.s程序执行结束后,已经正式完成内存页目录和页表的设置,并重新设置了内核实际使用的中断描述符表idt和全局描述符表gdt。另外还为软盘驱动程序开辟了1KB字节的缓冲区。此时system模块在内存中的详细映像如下:

system模块在内存中的映像示意图

启动部分识别主机的某些特性以及VGA卡的类型。如果需要,它会要求用户为控制台选择模式。然后将整个系统从地址0x10000移至0x0000处[3],进入保护模式并跳转至系统的余下部分(在0x0000处)。此时所有32位运行方式的设置启动被完成:IDT、GDT以及LDT被加载,处理器和协处理器也已确认,分页工作也设置好了;最终调用init/main.c中的main()程序。

boot/head.s可能是整个内核中最有诀窍的代码。

注意点

上面的所有过程都不能出错,一旦出错,计算机就会死机,在操作系统还没有完全运转之前是处理不了出错的。

根文件系统问题[4]

仅在内存中加载上述内核代码模块并不能让Linux系统运行起来。作为完整可运行的Linux系统还需要有一个基本的文件系统支持,即根文件系统。Linux
0.11内核仅支持MINIX的1.0文件系统。根文件系统通常在另一个软盘上或者在一个硬盘分区中。为了通知内核所需要的根文件系统在什么地方,bootsect.s程序的第43行上给出了根文件系统所在的默认块设备号。在内核初始化时会使用编译内核时放在引导扇区第509、510(0x1fc–0x1fd)字节中的指定设备号。

疑问点

  1. 是谁将bootsect移动到内存绝对地址0x90000(576KB)处,是bootsect自身?

    答:按照上述理解与分析,搬移bootsect程序的应该是它自身。

  2. 系统上电时BOIS已经在起始位置(也就是物理地址0位置)初始化了中断向量,此处将system模块还移动到物理内存其实位置处不会覆盖之前的中断向量?

    答:这是因为BIOS ROM中的中断调用(大小为0x400直接,也就是1KB)是用来获取机器的一些参数(例如显示模式、硬盘参数表等)。而启动setup程序时,这些参数已经获取完毕,就可以直接被覆盖掉了,这也是bootsect不把system模块直接加载到物理地址0x0000开始处的原因。(真的是这样吗?系统启动之后就不再需要这些中断了?可是微机实验中的中断都是可用的啊?)

  3. 上面已经说是setup完成的将system模块(也就是整个系统)移动到物理内存起始位置,但是此处由说是head进行的搬移,似乎出现了矛盾?

  4. 为什么需要有文件系统才能使Linux系统运行起来,内核模块应该运行起来了吧?

从硬盘启动系统

若需要从硬盘设备启动系统,那么通常需要使用其他多操作系统引导程序来引导系统加载。比如:Shoelace、LILO或Grub等多操作系统引导程序。此时bootsect.s所完成的任务会由这些程序来完成。bootsect程序就不会被执行了。因为如果从硬盘启动系统,那么通常内核映像文件Image会存放在活动分区的根文件系统中。因此你就需要知道内核映像文件Image处于文件系统中的位置以及是什么文件系统。即你的引导扇区程序需要能够识别并访问文件系统,并从中读取内核映像文件。

从硬盘启动的基本流程是:系统上电后,可启动硬盘的第1个分区(主引导记录MBR - Master Boot Record)会被BIOS加载到内存0x7c00处并开始执行。改程序会首先把自己向下移动到内存0x600处,然后根据MBR中分区表信息所指明活动分区中的第1个扇区(引导扇区)加载到内存0x7c00处,然后开始执行之。如果直接使用这种方法来引导系统就会碰到这样一个问题:即根文件系统不能与内核映像文件Image共存。

我所想到的解决方法有两个。一种办法是专门设置一个小容量的活动分区来存放内核映像文件Image。而相应的根文件系统则放在另一个分区中。这样虽然浪费了硬盘的4个主分区之一,但应该能在对bootsect.s程序作最少修改的前提下做到从硬盘启动系统。另一个办法是把内核映像文件Image与根文件系统组合存放在一个分区中,即内核映像文件Image放在分区开始的一些扇区中,而根文件系统则从随后某一指定扇区开始存放。这两种方法均需要对代码进行一些修改。