构建vmlinux.bin的规则在arch/x86/boot目录下的Makefile中:
/arch/x86/boot/Makefile:
OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S
$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)
根据前面对kbuild自定义函数if_changed的讨论可知,这里将执行命令cmd_objcopy。cmd_objcopy的定义如下:
/scripts/Makefile.lib:
cmd_objcopy = $(OBJCOPY) $(OBJCOPYFLAGS) $(OBJCOPYFLAGS_$(@F)) \
$< $@
其中OBJCOPY就是二进制工具objcopy,当然,因为我们使用的是交叉工具链,所以objcopy是i686-none-linux-gnu-objcopy,前面构建工具链时构建组件Binutils时已经构建:
/Makefile:
OBJCOPY = $(CROSS_COMPILE)objcopy
这里使用这个工具的目的是将ELF格式的文件转化为裸二进制格式。
cmd_objcopy中的“ < ” 、 “ <”、“ <”、“@”、“$(@F)”都是make的自动变量:
“$<”表示规则的依赖列表中的第一个依赖,这里是arch/x86/boot/compressed/vmlinux;
“$@”表示规则的目标,这里是arch/x86/boot/vmlinux.bin;
“$(@F)”表示构建目标去除目录后的文件名,这里是vmlinux.bin;
因此变量 OBJCOPYFLAGS_$(@F)展开为OBJCOPYFLAGS_ vmlinux.bin。替换各个变量后,cmd_objcopy最后展开大致为:
cmd_objcopy = i686-none-linux-gnu-objcopy -O binary -R .note \
-R .comment -S arch/x86/boot/compressed/vmlinux \
arch/x86/boot/vmlinux.bin
上述代码的意义已经显而易见了: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,其构建规则如下:
/arch/x86/boot/Makefile:
$(obj)/compressed/vmlinux: FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@
构建命令展开为:
make -f scripts/Makefile.build obj=arch/x86/boot/compressed
arch/x86/boot/compressed/vmlinux
Makefile.build将arch/x86/boot/compressed目录下的Makefile包含到Makefile.build中,生成完整的Makefile。但是这次,make没有如同构建各个子目录一样使用默认的构建目标,而是指定了构建目标为arch/x86/boot/compressed/vmlinux,其构建规则在arch/x86/boot/compressed目录下的Makefile中定义:
/arch/x86/boot/compressed/Makefile:
VMLINUX_OBJS = $(obj)/vmlinux.lds $(obj)/head_$(BITS).o \
$(obj)/misc.o $(obj)/string.o $(obj)/cmdline.o \
$(obj)/early_serial_console.o $(obj)/piggy.o
...
$(obj)/vmlinux: $(VMLINUX_OBJS) FORCE
$(call if_changed,ld)
对于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的规则:
/arch/x86/boot/compressed/Makefile:
cmd_mkpiggy = $(obj)/mkpiggy $< > $@ || ( rm -f $@ ; false )
$(obj)/piggy.S: $(obj)/vmlinux.bin.$(suffix-y) $(obj)/mkpiggy \
FORCE
$(call if_changed,mkpiggy)
看到上面的规则,我们恍然大悟,原来piggy.o是由piggy.S汇编而来,而piggy.S是编译内核时动态创建的,这就是我们找不到它的原因。piggy.S的第一个依赖vmlinux.bin.$(suffix-y)中的suffix-y表示内核压缩方式对应的后缀:
/arch/x86/boot/compressed/Makefile:
suffix-$(CONFIG_KERNEL_GZIP) := gz
suffix-$(CONFIG_KERNEL_BZIP2) := bz2
如果配置内核时指定采用gzip压缩方式,则suffix-y值为gz;如果指定bzip2压缩方式,则suffix-y值为bz2;等等。在本书中,我们配置的内核使用默认的压缩方式gzip,因此,suffix-y的值为gz。那么vmlinux.bin.gz是什么呢?我们看下面的脚本:
/arch/x86/boot/compressed/Makefile:
vmlinux.bin.all-y := $(obj)/vmlinux.bin
vmlinux.bin.all-$(CONFIG_X86_NEED_RELOCS) += \
$(obj)/vmlinux.relocs
$(obj)/vmlinux.bin.gz: $(vmlinux.bin.all-y) FORCE
$(call if_changed,gzip)
看到这里,相信读者已经不需要看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又是如何创建的?看下面的脚本:
/arch/arch/x86/boot/compressed/Makefile:
OBJCOPYFLAGS_vmlinux.bin := -R .comment -S
$(obj)/vmlinux.bin: vmlinux FORCE
$(call if_changed,objcopy)
我们再次看到了熟悉的objcopy,也就是说,arch/x86/boot/compressed目录下的vmlinux.bin是由vmlinux复制而来。而vmlinux没有任何修饰前缀,这说明其就是最顶层目录下的有效载荷。但是在这里我们也看到,这次复制过程只是删除了“.comment”段,以及符号表和重定位表(通过参数-S指定),而有效载荷vmlinux的格式依然是ELF格式的,如果不需要使用ELF格式的内核,这里追加一个“-O binary”即可。
接着我们再来看构建目标piggy.S的命令。进行变量替换后,cmd_mkpiggy展开为:
cmd_mkpiggy = arch/x86/boot/compressed/mkpiggy \
arch/x86/boot/compressed/vmlinux.bin.gz \
> arch/x86/boot/compressed/piggy.S
其中mkpiggy是内核自带的一个工具程序,源码如下:
/arch/x86/boot/compressed/mkpiggy.c:
int main(int argc, char *argv[])
{
...
printf(".section \".rodata..compressed\",\"a\",\
@progbits\n");
printf(".globl z_input_len\n");
printf("z_input_len = %lu\n", ilen);
printf(".globl z_output_len\n");
printf("z_output_len = %lu\n", (unsigned long)olen);
printf(".globl z_extract_offset\n");
printf("z_extract_offset = 0x%lx\n", offs);
...
printf(".incbin \"%s\"\n", argv[1]);
...
}
mkpiggy向屏幕打印了一堆文本。习惯上,我们会认为标准输出就是屏幕,但是回头再仔细观察一下cmd_mkpiggy的定义,其将标准输出重定向到了文件piggy.S,所以这里printf实际上是在组织汇编语句,然后输出到piggy.S中。也就是说,mkpiggy就是在“写”一个汇编程序。
根据代码可见,这个piggy.S非常简单,其使用汇编指令incbin将压缩的有效载荷vmlinux.bin.gz不加更改地直接包含进来。除了包含了压缩的内核映像外,piggy.S中还定义了解压vmlinux.bin.gz时需要的各种信息,包括压缩映像的长度、解压后的长度等,在解压内核时,解压代码将需要这些信息。下面是mkpiggy生成的一个具体的piggy.S示例:
.section ".rodata..compressed","a",@progbits
.globl z_input_len
z_input_len = 1721557
.globl z_output_len
z_output_len = 3421472
.globl z_extract_offset
z_extract_offset = 0x1b0000
.globl z_extract_offset_negative
z_extract_offset_negative = -0x1b0000
.globl input_data, input_data_end
input_data:
.incbin "arch/x86/boot/compressed/vmlinux.bin.gz"
input_data_end:
终于结束了这个让人眩晕的过程,让我们来回顾一下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为代表的一小部分非压缩代码组成。