探本溯源——深入领略Linux内核绝美风光之系统启动篇(二)

在前文结尾处我们提到内核映像的加载是由专用的bootloader比如LILO或是GRUB来实现的,而在x86架构下Linux内核通常使用其中之一的GRUB,它通过执行initrd文件来识别内核映像所在的文件系统进而执行加载,然而有一个需要注意的问题是,并非所有的物理地址空间对内核而言都是可用的,比如其中的某个物理地址范围可能被映射为I/O设备的共享内存,也可能其中的一个物理页框存放着BIOS数据,综合上述原因,GRUB必须建立一个物理地址映射来将内核加载至可用的物理内存中,而建立映射的这一过程则是根据协议完成的,不仅如此,实模式下的内核代码能够占用的内存空间,以及初始化过程中用来建立堆栈的物理内存大小都需要遵守相关的启动协议,有关LINUX/x86启动协议的详细信息可参考Documentation\x86目录下的boot.txt文件,该文件还详细解释了header.S中所定义的全局变量hdr的各个字段的含义,而其重要性单从源文件中各个字段后的注释规模便可见一斑。

物理内存布局
在深入剖析源代码之前,我们有必要首先明确启动阶段物理内存的分布情况,因为只有明白了执行过程背后所依据的一些基本事实才能对源代码有更为深刻的认识,而其中的一些事实源于软硬件的高速发展,另外一些则是为了满足兼容性的要求。

总的来说,内核映像由三部分构成,它们分别是:

  • 一个512字节的启动扇区——Kernel boot sector,也即在前文中所剖析的Linux内核自带的bootloader,但由于现在使用的bootloader为GRUB,为了避免产生歧义,所以冠以另外一个名称。该boot sector占用操作系统映像所在逻辑分区的第一个扇区,注意从bootsect的偏移0x1f1处开始存放hdr变量。
  • 实模式下的内核安装部分——Kernel setup,连续占用若干个扇区大小的内存空间,主要用于检测硬件环境并执行一系列的初始化,为保护模式下内核代码的运行完成一些前期的准备工作。
  • 保护模式下的内核代码,GRUB将其加载至从地址0x10 0000开始的物理内存中。

这里要注意的是实模式下的Kernel boot sector和Kernel setup虽然从逻辑上被分成两部分,但它们的分布是连续的,并且这两部分都处于第一个1MB的物理内存空间中,而其上所提到的保护模式下的内核代码的初始地址为0x10 0000,表示这部分代码从物理内存的第二个1MB开始安装,因此内核映像的实模式及保护模式这两个部分的分界线即物理地址0x10 0000。这里要特别注意的一点是,GRUB不可能单独在实模式下完成上述加载任务,因为在实模式下CPU只能寻址第一个1MB范围内的物理内存空间(原因请参见处理器体系结构及寻址模式一文)。有意思的是,在GRUB的次引导过程执行时将会临时性地切换到保护模式,完成内核映像的加载后再次回到实模式。因此虽然引导加载过程有两种模式的来回切换操作,但对于内核映像来说却是透明的,当控制权转交给内核后,它仍然首先从实模式下开始运行,但此时并不意味着超出第一个1MB范围的物理内存中没有任何内容,只是这些内容占用的内存空间无法被内核当前执行的指令所寻址,这使得保护模式下的内核代码不会被随意修改,也算是对保护模式中“保护”一词的另类诠释。

以下是GRUB将内核映像加载至内存空间后的分布图:


图1

上图同样位于Documentation\x86目录下的boot.txt中。在该文件中还展示了由zImage内核映像所使用的传统内存映射模型,但我们并不过多关注这类历史遗留问题,在后续的代码剖析过程中均以现阶段所使用的一些模型作为基准。在前文结尾处曾提到过BIOS例程将内核放入低地址0x0001 0000(小内核映像zImage)或者从高地址0x0010 0000(大内核映像bzImage)开始的RAM中,而现阶段所使用的Linux内核均编译为bzImage,因此保护模式下的内核代码被安装在从高地址0x0010 0000开始的RAM中。正如上图所示,我们也可以看到在完成POST以及一系列的初始化工作后,BIOS将bootloader加载至物理地址0000 7c00处。而令人困惑的是,内核的启动扇区bootsect的起始地址并未被严格限制,这其实是由于Linux内核允许使用多种bootloader所导致的,比如前文所提到的LILO以及GRUB,在现实情况中存在更多不同类型的bootloader,不同的bootloader可能将实模式的起始地址加载至不同的位置,然而在x86架构下的GRUB设置的起始地址正是0x9 0000

另外我们也可以看到实模式下的内核代码以及该代码所创建的堆栈的大小至多为8KB,之所以需要创建堆栈是为了提供C语言的运行环境,因为代码的执行过程需要使用栈来保存局部变量,函数调用则需要借助栈来传递参数以及保存返回地址,其中还有可能涉及到动态内存的分配,以及将内存分配给内核命令行。这样一来,实模式下的内核代码需要使用的内存空间将会达到8KB*2=16KB,而该段的起始地址为0x9 0000,因此这段内存空间的结尾处的地址至多将会达到0xA 0000。而这是不允许,因为现代机器中的许多BIOS例程需要使用从起始地址0x9 A000开始的额外内存空间,该内存空间即扩展的BIOS数据区(Extended BIOS Data Area,EBDA)。若内核被GRUB安装至较高的内存空间,那么执行代码将被BIOS修改。事实上实模式下的Linux内核正是基于这一原则所设计的,我们将在后文的源代码剖析过程中看到其具体的实现细节。

我们在前文提到过,代码背后所依据的某些事实是为了满足历史遗留问题所提出的兼容性要求,这一点可从上图中物理地址范围0xA 0000~0x10 0000的内存占用情况得知。追溯至上世纪80年代,当时IBM所推出的第一台PC机可供寻址的物理内存总共为1MB。而这1MB中的低640KB供DOS以及应用程序使用,而高端的384KB则被留作它用,其中低端的640KB被称为常规内存,高端的384KB则被称为保留内存,这两种不同的内存类型通过物理地址0xA 0000得以分隔,此后这个分界线便被确定下来并沿用至今。

简单总结一下,在x86体系结构中,RAM的第一个1MB内存空间包含如下两个“独特”的地方:

  • 物理地址0x0000~0x1000以及0x9 A000~0xA 0000所占内存由BIOS使用,存放加电自检(Power-On Self-Test,POST)期间检查到的系统硬件配置。有些类型的BIOS甚至在系统初始化之后依然将数据写入该内存。
  • 从0xA 0000~0x10 0000范围内的物理内存通常保存BIOS例程,并且映射ISA图形卡上的内部内存。这个区域就是IBM兼容PC上从640KB到1MB之间的著名的洞——图1中的I/O memory hole:物理地址存在但被保留,不能由操作系统使用。

内核将上述地址范围的物理内存所占页框标记为保留,它们连同内核代码以及已初始化或未初始化的内核数据一起,在整个机器的执行周期中常驻内存,而绝不能被动态分配或由内核调度程序交换到磁盘上。


图2

上图形象地显示了常驻物理内存中各个区域的使用情况,需要注意的一点是保护模式下的内核代码所占页框的总数依赖于对应的配置方案,因此上图中与之对应的区域只是用符号简单的加以表示。而实模式下的内核代码仅在启动期间执行硬件检测及一系列初始化操作之后便不再被使用,因此当实模式下的内核代码跳转到保护模式之后,这部分内存空间即可由内核代码用于其他用途。

实模式下内核的初始化变量——安装头(setup header)
该安装头为从Kernel boot sector中偏移0x1f1处开始的hdr变量,主要存放初始化期间将会使用到的一些数据。我将把该变量中各个字段的含义集中罗列在这里,在后文中讲到内核执行初始化过程中使用到这些数据时不再单独详细描述。另外要注意的是有些字段的存在同样属于历史遗留性问题,对于这些内容我们直接一带而过。下表是这些字段的概要说明:

偏移量/大小 字段名
  • 5
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值