3.1 内核映像的组成
在讨论内核构建前,我们先来简单了解一下内核映像的组成,如图3-1所示。
图 3-1 内核映像bzImage的组成
如果将内核的映像比作航天器,则setup.bin部分就类似于火箭的一级推进子系统。最初,这部分负责将内核加载进内存,并为后面内核保护模式的运行建立基本的环境。但后来加载内核的功能被分离到Bootloader中,setup.bin则退化为辅助Bootloader将内核加载到内存。
紧接着,包围在32位保护模式部分外的是非解压缩部分。这部分可以看作是火箭的二级推进子系统,负责将压缩的内核解压到合适的位置,并进行内核重定位,在完成这个环节后,其从内核映像脱离。
最后是内核的32位保护模式部分vmlinux。这部分相当于航天器的有效载荷,即类似于最后运行的卫星或者宇宙飞船,只有这部分最后留在轨道内(内存中)运行。内核构建时,将对有效载荷vmlinux进行压缩,然后与二级推进系统装配为vmlinux.bin。
下面我们就来看看内核映像的各个组成部分。
3.1.1 一级推进系统——setup.bin
在进行内核初始化时,需要一些信息,如显示信息、内存信息等。曾经,这些信息由工作在实模式下的setup.bin通过BIOS获取,保存在内核中的变量boot_params中,变量boot_params是结构体boot_params的一个实例。如setup.bin中收集显示信息的代码如下:
store_video_mode首先调用函数intcall获取显示方面的信息,并将其保存在boot_params的screen_info中。intcall是调用BIOS中断的封装,0x10是BIOS提供的显示服务(Video Service)的中断号,代码如下:
在代码中我们并没有看到熟悉的调用BIOS中断的身影,如"int$0x10",但是我们看到了一个特殊的字符——0xcd。正如其后面的注释所言,0xcd就是x86汇编指令INT的机器码,如表3-1所示。
根据x86的INT指令说明,0xcd后面跟着的1字节就是BIOS中断号,这就是上面代码中标号为3处分配1字节的目的。
在函数intcall的开头,首先比较寄存器al中的值与标号3处占用的1字节,若相等则直接向前跳转至标号1处,否则将寄存器al中的值复制到标号3处的1个字节空间。那么寄存器al中保存的是什么呢?
在默认情况下,GCC使用栈来传递参数。但是我们可以使用关键字"attribute(regparm(n))"修饰函数,或者通过向GCC传递命令行参数"-mregparm=n"来指定GCC使用寄存器传递参数,其中n表示使用寄存器传递参数的个数。在编译setup.bin时,kbuild使用了后者,编译脚本如下所示:
如此,函数的第一个参数通过寄存器eax/ax传递,第二个参数通过ebx/bx传递,等等,而不是通过栈传递了。因此,上面的寄存器al中保存的是函数intcall的第一个参数,即BIOS中断号。
在完成信息收集后,setup.bin将CPU切换到保护模式,并跳转到内核的保护模式部分执行。如我们前面讨论的,setup.bin作为一级推进系统,即将结束历史使命,所以内核将setup.bin收集的保存在setup.bin的数据段的变量boot_params复制到vmlinux的数据段中。
但是随着新的BIOS标准的出现,尤其是EFI的出现,为了支持这些新标准,开发者们制定了32位启动协议(32-bit boot protocol)。在32位启动协议下,由Bootloader实现收集这些信息的功能,内核启动时不再需要首先运行实模式部分(即setup.bin),而是直接跳转到内核的保护模式部分。因此,在32位启动协议下,不再需要setup.bin收集内核初始化时需要的相关信息。但是这是否意味着可以彻底放弃setup.bin呢?
事实上,除了收集信息功能外,setup.bin被忽略的另一个重要功能就是负责在内核和Bootloader之间传递信息。例如,在加载内核时,Bootloader需要从setup.bin中获取内核是否是可重定位的、内核的对齐要求、内核建议的加载地址等。32位启动协议约定在setup.bin中分配一块空间用来承载这些信息,在构建映像时,内核构建系统需要将这些信息写到setup.bin的这块空间中。所以,虽然setup.bin已经失去了其以往的作用,但还不能完全放弃,其还要作为内核与Bootloader之间传递数据的桥梁,而且还要照顾到某些不能使用32位启动协议的场合。