13.4.2 Head.S

Head.S也要先做一些屏蔽中断一类的准备工作,然后,它会对中断向量表做一定的处理:用一个默认的表项把所有的256个中断向量填满。这个默认表项指向一个特殊的中断服务程序,事实上,该程序什么都不作。为什么这样呢?这是因为,在 Linux系统初始化完成后,BIOS的中断服务程序是不会再被使用了。Linux采用了很完善的设备驱动程序使用机制,该机制使特定硬件设备的中断服务程序很容易被系统本身或用户直接调用,而且,调用时所需的参数通常都要比BIOS调用来得简单。由于设备驱动程序是专门针对一个设备的,所以,它所包含的中断服务程序在功能上通常也比BIOS中断要完善,所以,BIOS的中断向量在这里就被覆盖了。启动处在这个阶段,第一,还不需要开启中断,第二,相应的设备驱动程序还未被加载,所以把中断向量表置空显然是个合理的选择。事实上,直到初始化到了最后的阶段(start_kernel()被调用后),中断向量表才被各个中断服务程序重新填充。

Boot Loader读入内存中的启动参数和命令行参数,这些参数在启动过程中是必须的数据资料,他们不但在启动的过程中要使用,在启动后也是必须的,Head.S把它们保存在empty_zero_page 页中,这就涉及到了页机制,之所以先映射这个页,是因为以后运行的程序可能会占用启动参数和命令行参数的内存区,所以要先保存它们。

Head.S此后会检查CPU的类型:虽然Intel系列CPU保持兼容,但无疑后继产品会有很多新的特性和功能(如奔腾芯片支持4M大小的页)。作为一个操作系统,Linux 当然要对他们作出相应的支持并发挥他们更优越的处理能力,此外,由于Intel已成为业界标准,所以也有必要把与之兼容的设备归入一类(如AMDK6就与Intel奔腾芯片兼容)。这里只是对处理器类型进行判断,在Start_kernel()中要根据这里的结果对系统进行设置。此外,还要对协处理器进行检查,8038780487当然也该区别对待。

下面是一个非常重要的初始化步骤—页初始化。

Head.S调用了Setup_paging这个子函数,这个函数只对4MB大小的内存空间进行了映射,剩下的部分在start_kernel()中分配,这个函数并不复杂,它先把从线性地址0xC0000000处开始的两个大小为4KB的内存空间映射为两个页:一个为页目录表—swap_page_dir,另一个为零页—pg0,他们分别指向物理地址的0x10000x2000。此后设置页目录表的属性(可读写、存在、任意特权级可访问),并使它的表项指向为内核分配的4MB空间上的各个页。在Head.S里定义了几个比较特殊的页,除了上面提到的swap_page_dirpg0 以外,还有empty_bad_pageempty_bad_page_tableempty_zero_page等等。刚才提到了empty_zero_page,这里详细介绍一下这个页,以便对页机制及页初始化有更进一步的认识。

empty_zero_page这个页存放的是系统启动参数和命令行参数,他们各占2KB大小,即各占半页。该页的具体内容如下:

 

偏移量  数据类型            描述

------      ----        -----------

    0   32 bytes        一段屏幕显示信息

2   unsigned short  EXT_MEM_K extended memory size in Kb (BIOS15

中断得到的内存大小)

 0x20   unsigned short  CL_MAGIC, 命令行标志数 (=0xA33F)

 0x22   unsigned short  CL_OFFSET, 经计算所得的命令行程序的偏移地址,

              0x90000 + contents of CL_OFFSET

            (只在CL_MAGIC = 0xA33F时计算)

 0x40   20 bytes    APM_BIOS_INFO结构体

 0x80   16 bytes    hd0-disk-parameter (硬盘1参数,由BIOS40号中断得到)

 0x90   16 bytes    hd1-disk-parameter (硬盘1参数,由BIOS46号中断得到)

 

 0xa0   16 bytes        系统描述符表截为16字节的信息

            ( sys_desc_table_struct结构)

 0xb0 - 0x1df      未占用

0x1e0   unsigned long   ALT_MEM_K,可变化的内存大小

0x1f1   char        setup.s所占的扇区数

0x1f2   unsigned short  MOUNT_ROOT_RDONLY (if !=0)

0x1f4   unsigned short  压缩内核占用的空间

0x1f6   unsigned short  swap_dev (unused AFAIK)(交换分区)

0x1f8   unsigned short  RAMDISK_FLAGS(虚盘的标志)

0x1fa   unsigned short  VGA-Mode

0x1fc   unsigned short  ORIG_ROOT_DEV (high=Major low=minor)根设备的从                                                   次设备号

0x1ff   char        AUX_DEVICE_INFO

0x200   short       jump to start of setup code aka "reserved" field.

0x202   4 bytes     SETUP-header的标志, ="HdrS"

            目前的版本是0x0241...

0x206   unsigned short  header的标志

0x208   8 bytes     setup.S 获得boot loaders信息的内存区

0x210   char        LOADER_TYPE = 0, 老式引导程序,

            否则由boot loader自己设置

            0xTV: T=0 LILO

                1 Loadlin

                2 bootsect-loader

                3 SYSLINUX

                4 ETHERBOOT

                V = 引导程序版本号

0x211   char        loadflags(调入标志):

            bit0 = 1: 读入高内存区 (bzImage)

            bit7 = 1: boot loader设置了堆和指针。

0x212   unsigned short  (setup.S)

0x214   unsigned long   KERNEL_START loader从何处调入内核

0x218   unsigned long   INITRD_START, 调入内存的image的位置

0x21c   unsigned long   INITRD_SIZE, 虚盘上的image的大小

0x220   4 bytes     (setup.S)

0x224   unsigned short  setup.S 的堆和指针

0x226 - 0x7ff      setup.S 的程序体

 

0x800   string 2K max 命令行参数

           

     这个页中的内容是从启动的多个程序中总结出来的,从中我们可以看出,页的大小为0x8004K,页内的数据按照一定的偏移量顺序排列,对页的读写操作也要通过这些偏移量来完成。在你分析Linux的启动时,通过和这张表的对照,希望你能对在这种汇编语言环境中准确的定位读写及对页与段的安排,有一个更明确的认识。

     此外,你应该发现,物理地址的0x0000没有被页映射,这部分空间保存着中断向量表,全局描述符表等等系统的比较重要的数据结构,他们不必要进行页交换,而且,对线性空间的访问页机制也完全不能察觉,所以,没有必要用页来映射这个空间。这样做还有另外一层用意:系统中凡是无效的指针(NULL),都可以自动对应到这个空间。

     关于页初始化更详细的内容请参见第六章。那么,段机制Head.S中有没有变化呢?我们知道,一旦系统进入保护模式,那么,系统的多任务特性就完全可以体现了。这里,段机制的多任务特性,当然要进一步地体现出来了。下面是Head.S中定义的全局描述符表:

     ENTRY(gdt_table)

         .quad 0x0000000000000000        /* NULL descriptor */

         .quad 0x0000000000000000        /* not used */

         .quad 0x00cf9a000000ffff        /* 0x10 kernel 4GB code at 0x00000000 */

         .quad 0x00cf92000000ffff        /* 0x18 kernel 4GB data at 0x00000000 */

         .quad 0x00cffa000000ffff        /* 0x23 user   4GB code at 0x00000000 */

         .quad 0x00cff2000000ffff        /* 0x2b user   4GB data at 0x00000000 */

         .quad 0x0000000000000000        /* not used */

         .quad 0x0000000000000000        /* not used */

         /*

          * The APM segments have byte granularity and their bases

          * and limits are set at run time.

          */

         .quad 0x0040920000000000        /* 0x40 APM set up for bad BIOS's */

         .quad 0x00409a0000000000        /* 0x48 APM CS    code */

         .quad 0x00009a0000000000        /* 0x50 APM CS 16 code (16 bit) */

         .quad 0x0040920000000000        /* 0x58 APM DS    data */

         .fill NR_CPUS*4,8,0             /* space for TSS's and LDT's */

 

     关于这部分内容的详细解释请参见第二章2.3.6节。有了新的全局描述符表和中断向量表,当然要重新装入描述符,所以lgdtlidt又被执行了一遍,而以前预取的指令当然要重取,各个寄存器也要重新赋值,核心的堆栈自然也要重置。此外,虽然此时并没有用户以和任务,但还是可以用lldt让局部描述符表的寄存器指向空,以便初始化的顺利进行。

     到这一步,保护机制下内存管理,中断管理的框架已经建好了,下一步,就是怎么样具体实现操作系统的功能了,于是,Head.S调用/init/main.C中的start_kernel函数,把控制权交给启动的下一部分代码。