当前位置: 首页 > 工具软件 > vm86 > 使用案例 >

Linux内核构建之vmlinux.bin

周兴朝
2023-12-01

转载于 https://book.2cto.com/201309/33426.html

构建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中的“ &lt; ” 、 “ &lt;”、“ <@”、“$(@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为代表的一小部分非压缩代码组成。

 类似资料: