3.2.4 vmlinux.bin的构建过程

根据图3-1可知,kbuild将有效载荷与内核的非压缩部分装配为vmlinux.bin。我们前面已经看到了有效载荷vmlinux的构建过程,这一节我们讨论二级推进系统的构建,并看看二级推进系统是如何与有效载荷进行装配的。构建vmlinux.bin的规则在arch/x86/boot目录下的Makefile中:

3.2.4 vmlinux.bin的构建过程 - 图1

根据前面对kbuild自定义函数if_changed的讨论可知,这里将执行命令cmd_objcopy。cmd_objcopy的定义如下:

3.2.4 vmlinux.bin的构建过程 - 图2

其中OBJCOPY就是二进制工具objcopy,当然,因为我们使用的是交叉工具链,所以objcopy是i686-none-linux-gnu-objcopy,前面构建工具链时构建组件Binutils时已经构建:

3.2.4 vmlinux.bin的构建过程 - 图3

这里使用这个工具的目的是将ELF格式的文件转化为裸二进制格式。

cmdobjcopy中的“$<”、“$@”、“$(@F)”都是make的自动变量,“$<”表示规则的依赖列表中的第一个依赖,这里是arch/x86/boot/compressed/vmlinux;“$@”表示规则的目标,这里是arch/x86/boot/vmlinux.bin;“$(@F)”表示构建目标去除目录后的文件名,这里是vmlinux.bin,因此变量OBJCOPYFLAGS$(@F)展开为OBJCOPYFLAGS_vmlinux.bin。替换各个变量后,cmd_objcopy最后展开大致为:

3.2.4 vmlinux.bin的构建过程 - 图4

上述代码的意义已经显而易见了:arch/x86/boot目录下的vmlinux.bin是由arch/x86/boot/compressed目录下的vmlinux通过工具i686-none-linux-gnu-objcopy复制而来。

为了指导加载器加载ELF文件,ELF文件中附加了很多信息,如ELF文件头、Program Header Table、符号表、重定位表等。但是这些对内核是没有意义的,Bootloader加载内核时不需要ELF文件中附加的这些信息,变量OBJCOPYFLAGS_vmlinux.bin中的"-O binary"指定objcopy将复制后内核转换为裸二进制格式;选项(如".note"、".comment")则表明将这些段也删除。读者可能会问,转化为裸二进制格式时还会保留如".note"、".comment"等段吗?当然了,转化为裸二进制格式只不过把为ELF格式附加的东西去除掉了,比如ELF的头、Section Header Table、Program Header Table等,但是并不会删除保存具体内容的段。

显然,构建的焦点转换为arch/x86/boot/compressed下的vmlinux,其构建规则如下:

3.2.4 vmlinux.bin的构建过程 - 图5

3.2.4 vmlinux.bin的构建过程 - 图6

构建命令展开为:

3.2.4 vmlinux.bin的构建过程 - 图7

Makefile.build将arch/x86/boot/compressed目录下的Makefile包含到Makefile.build中,生成完整的Makefile。但是这次,make没有如同构建各个子目录一样使用默认的构建目标,而是指定了构建目标为arch/x86/boot/compressed/vmlinux,其构建规则在arch/x86/boot/compressed目录下的Makefile中定义:

3.2.4 vmlinux.bin的构建过程 - 图8

对于32位系统,变量BITS为32。由上述Makefile可见,arch/x86/boot/compressed目录下的vmlinux是由该目录下的head_32.o、misc.o、string.o、cmdline.o、early_serial_console.o以及piggy.o链接而成的。其中vmlinux.lds是指导链接过程的脚本。

在一份刚刚解压且没有进行任何编译动作之前的内核源码中,除了piggy.o,我们可以找到上述依赖列表中任何一个目标文件的源文件,比如head_32.o对应源文件head_32.S,misc.o对应源文件misc.c等。而我们却找不到piggy.o对应的源文件,比如piggy.c或piggy.S亦或其他。但是仔细观察,我们会发现在arch/x86/boot/compressed目录下的Makefile中有一个创建文件piggy.S的规则:

3.2.4 vmlinux.bin的构建过程 - 图9

看到上面的规则,我们恍然大悟,原来piggy.o是由piggy.S汇编而来,而piggy.S是编译内核时动态创建的,这就是我们找不到它的原因。piggy.S的第一个依赖vmlinux.bin.$(suffix-y)中的suffix-y表示内核压缩方式对应的后缀:

3.2.4 vmlinux.bin的构建过程 - 图10

如果配置内核时指定采用gzip压缩方式,则suffix-y值为gz;如果指定bzip2压缩方式,则suffix-y值为bz2;等等。在本书中,我们配置的内核使用默认的压缩方式gzip,因此,suffix-y的值为gz。那么vmlinux.bin.gz是什么呢?我们看下面的脚本:

3.2.4 vmlinux.bin的构建过程 - 图11

看到这里,相信读者已经不需要看cmd_gzip的定义了。根据变量vmlinux.bin.all-y的值,vmlinux.bin.all-y中包括arch/x86/boot/compressed目录下的vmlinux.bin。如果内核被配置为可重定位的,那么vmlinux.bin.all-y中还包括记录重定位信息的vmlinux.relocs。也就是说,如果内核被配置可重定位,则vmlinux.bin.gz是由vmlinux.bin和vmlinux.relocs压缩而来的,否则只是vmlinux.bin由压缩而来。

那么arch/x86/boot/compressed目录下的vmlinux.bin又是如何创建的?看下面的脚本:

3.2.4 vmlinux.bin的构建过程 - 图12

我们再次看到了熟悉的objcopy,也就是说,arch/x86/boot/compressed目录下的vmlinux.bin是由vmlinux复制而来。而vmlinux没有任何修饰前缀,这说明其就是最顶层目录下的有效载荷。但是在这里我们也看到,这次复制过程只是删除了".comment"段,以及符号表和重定位表(通过参数-S指定),而有效载荷vmlinux的格式依然是ELF格式的,如果不需要使用ELF格式的内核,这里追加一个"-O binary"即可。

至此,我们明白了vmlinux.bin.gz就是有效载荷的压缩。

接着我们再来看构建目标piggy.S的命令。进行变量替换后,cmd_mkpiggy展开为:

3.2.4 vmlinux.bin的构建过程 - 图13

其中mkpiggy是内核自带的一个工具程序,源码如下:

3.2.4 vmlinux.bin的构建过程 - 图14

3.2.4 vmlinux.bin的构建过程 - 图15

mkpiggy向屏幕打印了一堆文本。习惯上,我们会认为标准输出就是屏幕,但是回头再仔细观察一下cmd_mkpiggy的定义,其将标准输出重定向到了文件piggy.S,所以这里printf实际上是在组织汇编语句,然后输出到piggy.S中。也就是说,mkpiggy就是在“写”一个汇编程序。

根据代码可见,这个piggy.S非常简单,其使用汇编指令incbin将压缩的有效载荷vmlinux.bin.gz不加更改地直接包含进来。除了包含了压缩的内核映像外,piggy.S中还定义了解压vmlinux.bin.gz时需要的各种信息,包括压缩映像的长度、解压后的长度等,在解压内核时,解压代码将需要这些信息。下面是mkpiggy生成的一个具体的piggy.S示例:

3.2.4 vmlinux.bin的构建过程 - 图16

终于结束了这个让人眩晕的过程,让我们来回顾一下vmlinux.bin的构建过程:

1)kbuild使用objcopy,将顶层Makefile构建好的内核映像vmlinux复制到arch/x86/boot/compressed目录下,删除了".comment"段、符号表和重定位表,并命名为vmlinux.bin;

2)kbuild压缩内核映像vmlinux.bin,笔者采用默认的压缩方式gzip,所以压缩后的内核映像为vmlinux.bin.gz;

3)kbuild借助内核自带的程序mkpiggy构建一个汇编程序piggy.S,该汇编程序就是vmlinux.bin.gz加上一些解压内核时需要的信息;

4)kbuild将head_32.o、misc.o以及包含压缩映像的piggy.o等目标文件链接为vmlinux.bin,保存到arch/x86/boot目录下。

可见,vmlinux.bin由压缩的vmlinux加上以head_32.o为代表的一小部分非压缩代码组成。vmlinux就是我们提到的有效载荷,而这部分非压缩代码就是我们所谓的二级推进系统。