通过上一章节,大家都看到了,最终每个Linux系统都有一个类似/boot/vmlinuz-2.6.x的文件,就是所谓的内核映像。而大家对从源代码到内核映像的形成有了一个较完整的认识。前面也提到了,系统启动的时候,grub会把内核映像加载到内存,然后执行它。总结一下Grub的这一个过程,就是:
1. 调用一个BIOS过程显示“Loading”信息。
2. 调用一个BIOS过程从磁盘装入内核映像的初始部分,即将内核映像的第一个512字节加载到物理地址0x00090000开始的内存中,而将setup程序的代码(参见后面的内存布局)从地址0x00090200开始存入RAM中。
3. 调用一个BIOS过程从磁盘中装载其余的内核映像,并把内核映像放入从低地址0x00010000(适用于使用make zImage编译的小内核映像)或者从高地址0x00100000(适用于使用make bzImage编译的大内核映像,也就是我们现在的情况)开始的RAM中。大内核映像的支持虽然本质上与其他启动模式相同,但是它却把数据放在不同的物理内存地址,以避免ISA黑洞问题。
4. 跳转到arch/x86/boot/header.S的_start处开始执行。
注意grub的stage2会临时地打开保护模式,完成上述的第3步,把内核映像vmlinuz-2.6.x除setup的其余部分从磁盘整体加载到内存,然后再回到实模式下。所以此时,在内存中的内核映像分成了两个部分,实模式部分和保护模式部分,其分界线就是物理地址0x100000。而实模式部分也分成了两部分,bootsect部分和setup部分。
针对多种体系结构,linux使用不同的bootloader,因为我看的代码是x86下的,所以我们全文的内核引导程序都指的是grub。其实每个bootloader都很复杂,比如grub如何读,把它加载到哪,怎么执行内核。
第一个问题,grub是怎么读内核映像的?好吧,再说一次,BIOS启动后,grub首先根据系统盘的第一个块,即512字节中的数据(bootsect)来判断识别文件系统的代码(512字节显然不能识别文件系统)在磁盘中的什么块,这些数据是grub安装的时候记录的,然后执行下一个步骤,即识别系统盘的文件系统。
识别系统盘的文件系统主要是通过initrd程序来完成,前面也提到了,initrd就像个小操作系统,但它的作用仅仅是让grub能够获得磁盘驱动程序以识别内核映像所在的文件系统,所以initrd.img压缩包并不大,只有3.5MB左右。initrd程序还是很复杂的,只不过不是我们的研究重点,有兴趣的同志可以尝试解压缩initrd-2.6.18-194.el5.img文件(先要改名成.gz文件,然后用gunzip程序解压缩),并将其mount到一个指定位置。这时你就可以看到,这个内存文件系统提供若干命令和脚本。其中最重要的是提供系统初始化的linuxrc脚本。必须注意的是这里使用的shell是nash而不是bash,nash是专门为linuxrc可执行脚本设计的,因此你也有必要看一看nash的man文档。
现在就假设grub已经能识别文件系统了。接下来,首先grub通过该文件系统得到内核映像/boot/vmlinuz-2.6.34.1(以后就简称vmlinuz了)文件。怎么处理它呢,这就要协议,(协议的详细信息参考linux-2.6.34.1/Documentation/x86/boot.txt)。总的来说vmlinuz由三部分构成(我机器上的vmlinuz文件的大小约为1.2MB):
- 第一个是512字节的bootsect(第一个块)
- 第二个是setup代码,若干不多个512字节(一会再说它多大)
- 保护模式下的内核代码
第一个512就是通常的启动扇区,对应于ULK3的远古时代(但是它有点特殊,因为它现在并不用作为启动扇区了,一会儿会看到),以前是在arch/x86/boot/bootsect.S中,但是现在这个文件消失了,它到哪儿去了呢?
第二部分是实模式下setup.bin中的代码,对于ULK3中说的中世纪。也就是说,vmlinuz的第二部分,以前是arch/x86/boot/setup.S代码,但是这个文件也失踪了。OK,揭开谜底了。第一段bootsect.S和第二段代码setup.S(的部分)合并到arch/x86/boot/header.S中了,除此之外,后面还会看到,还有部分c代码,主要是用来建立保护模式的。
在链接脚本setup.ld中我们看到,setup.bin的入口程序是_start,即跳过了它的第一个512字节。而setup.bin程序就是我们前面所说的setup程序,它内容来自boot目录下的其它一些源文件(参考boot/Makefile)。这段代码的大小,在通过setup.ls链接脚本链接setup.elf的时候会得到,然后这个数据会写入第一个512字节中的偏移(也是整个vmlinuz的偏移)位置为0x01F1处的一个字节:
[root@localhost ~]# od -j 0x01F1 -N1 /boot/vmlinuz-2.6.34.1 -D
0000761 21
0000762
解释一下,od命令的目的是输出文件内容。我们看到vmlinuz文件的0x01F1处的内容为21,这个值表示第一部分和第二部分总共占据的块的个数。由于一个块是512字节,所有,表示21*512 = (1+20)*512,就说明这个位于实模式下的setup程序的长度为20*512 = 10K。
注意!回忆我们在制作bzImage一节中讲到连接setup.bin的链接脚本,hdr的位置正好就是0x1f1。整个初始化阶段,最重要的东西,就是这个位于vmlinuz第一个512字节的497偏移的hdr。我们可以来看看arch/x86/boot/header.S下,从96行开始:
hdr:
setup_sects: .byte 0 /* Filled in by build.c */
root_flags: .word ROOT_RDONLY
syssize: .long 0 /* Filled in by build.c */
ram_size: .word 0 /* Obsolete */
……
这个hdr的第一个变量就是setup_sects,占一个字节,用来存放整个一二部分的大小。其值再编译的时候由build程序写入。回忆一下,最后制作bzImage是就是利用的该程序。
第三部分就是内核映像的除setup后的其它部分,其入口是arch/i386/boot/compressed目录下head_32.S中的startup_32。因为第三部分代码是进入保护模式后执行的,所以被称为保护模式内核代码。
好了,在对内核文件的构成有个了解之后来看看加载过程,
先看一段文字(Documentation/i386/boot.txt):
我们可以看到,上面是内核提供的文档对grub加载vmlinuz之后的物理内存布局的描述。按照协议,bootloader被加载到了0x1000处,由于每种bootloader的大小不同,协议中设为X,而grub设置的就是0x90000,前面是提到过的。紧接着vmlinuz的前两部分在一起,也就是setup.bin的实模式部分代码。而第三部分在0x100000开始。
注意,grub加载vmlinuz后的内存布局非常重要,主要分为实模式部分和保护模式部分。理解了这部分,大家才会看懂我后面的讲解。