内核映像的形成——制作bzImage

2.2.6 制作bzImage

回到顶层Makefile849行,随着vmlinux的链接工作结束,在主目录下生成了vmlinux文件,vmlinux目标也就结束了。那么就会来到arch/x86/Makefile155bzImage目标。看到这个目标,就知道后面的工作就算打包压缩vmlinux并制作bzImage了。看到156行,因为CONFIG_X86_DECODER_SELFTEST没有设置,所以会直接执行159行以后的命令:

159        $(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE)

160        $(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot

161        $(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/$@

 

160行是建立一个arch/i386/boot目录,161行是建立一个符号链接,重中之重是159行,其中$(boot)宏来自143行,$(KBUILD_IMAGE)宏来自153所以翻译159行:

@make  -f  scripts/Makefile.build  obj=arch/x86/boot  arch/x86/boot/bzImage

 

前面提到vmlinux 的入口地址为phys_startup_32,该函数是工作在32-bit 段寻址的保护模式,但是问题是系统自加电那一刻起,就运行于16-bit 实模式。所以我们需要一些辅助程序从16-bit 实模式转到32-bit64-bit保护模式,设置好必须的参数后才能开启分页模式转到32-bit64-bit分页保护模式。前半部分是由arch/x86/boot/setup.bin 实现的,后半部分则是由arch/x86/kernel/head.o实现的。setup.bin除了向保护模式转换外,还有很多其它的事情要做。

 

vmlinux 的链接过程来看vmlinux 是一个已编好的kernel,和普通的ELF 可执行文件没有什么区别:

[root@localhost linux-2.6.34.1]# file ./vmlinux

vmlinux: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), statically linked, not stripped

 

我们知道,C和汇编源文件被编译成ELF 文件后,通常会包含有连接器(Linker)或加载器(Loader)所需要的ELF headerProgram header tableSection header table。这些ELF header 和一些section 的作用是告诉内核ELF Loader 如何载入ELF 可执行文件。但是, Linux内核作为一种特殊的ELF 文件,没有ELF Loader的帮助,需要特殊辅助程序去装载它。它的装载地址是固定的,前面讲了的,0xC0100000

 

这时,为了保证通用性而存在的ELF header 和一些section对内核的装载就没有意义了。为了使内核尽可能小,可以把这些信息去掉。所以通常采用objcopy命令来去掉原来ELF文件中的headersection,同时转化为raw binary格式的文件。即便如此,通过objcopy 处理过的vmlinux 一般需要压缩以后再重新链接,当然必须把解压缩的程序也同时链接进来。

 

这个压缩的过程着实奇怪:压缩后,然后再解压缩,岂不是浪费启动时间?这还是因为当x86系列处理器启动初期,处于实模式状态,可以寻得的地址空间十分有限,仅仅是1MB,如果内核过大,就无法加载。更新的内核(从2.6.30开始)甚至提供了更高压缩率的格式:bzipLZMA

 

由上面的分析可以看出, bzImage 至少包含kernel booting 辅助程序和压缩的vmlinux。这可以从bzImage 的规则链得到验证:

刚才看到了,arch/x86/Makefile中关于bzImage的规则调用命令为:

make -f scripts/Makefile.build obj=arch/x86/boot arch/x86/boot/bzImage

 

注意:这个命令为Makefile.build 指定了目标arch/x86/boot/bzImage ,而不是使用缺省的__build

 

arch/x86/boot/MakefilebzImage的规则如下:

81 $(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin $(obj)/tools/build FORCE

82         $(call if_changed,image)

83         @echo 'Kernel: $@ is ready' ' (#'`cat .version`')'

 

其中obj= arch/x86/boot,所以arch/x86/boot/bzImage 依赖的目标是:arch/x86/boot/setup.binarch/x86/boot/vmlinux.binarch/x86/boot/tools/buildtools/build 已加到hostprogs-y中去了,会生成相应的主机程序。tools/build主机程序的主要工作是将arch/x86/boot/setup.binarch/x86/boot/vmlinux.bin 拼接在一起,后面会看到。下面我们将分别讲述setup.bin vmlinux.bin的生成过程。

 

先来梳理一下前面讲的系统启动流程。BIOS从启动设备(Hard Disk MBR)中装载grubstage1是简单的bootsector,只占一个扇区;然后是stage2,有很多扇区,要分好几个阶段才能完全装入。

 

bootsector,装载setup.bin,然后跳入setup.bin的入口函数_start(arch/x86/boot/header.S)setup.bin主要作用就是完成一些系统检测和环境准备的工作,其函数调用顺序为:

_start

->start_of_setup, 这两个函数都定义在arch/x86/boot/header.S

->main(arch/x86/boot/main.c)

->goto_protect_mode(arch/x86/boot/pm.c)

->protect_mode_jump(arch/x86/pmjump.S)

 

main.c 主要功能为检测系统参数如: Detect memory layout, set video mode 等,后面会详细分析,最后调用goto_protect_mode,设置32-bit 64-bit保护段式寻址模式。注意: main.o 编译为16位实模式程序。goto_protect_modea则会调用protected_mode_jump

 

而在arch/x86/pmjump.S中,protected_mode_jump最后几行就相当于ljmpl __BOOT_CS:0,实际地址为0x00000000,也就是vmlinux.bin中的startup_32函数。注意,整个内核中有两个startup_32函数,一个在arch/x86/boot/compressed/head_32.S,另一个呢,是在arch/x86/kernel/head_32.S。那么vmlinux.bin的中的是哪个呢?别忘了,之前递归编译各对象,并且链接vmlinux时,没有用到arch/x86/boot对象,说明arch/x86/kernel/head_32.Sstartup_32函数是被链接进了vmlinux,随后,vmlinux将会被压缩,所以,这一个startup_32就不存在了。

 

看到arch/x86/boot/compressed/head_32.S的入口函数startup_32startup_32会解压缩kernel

126 /*

 127 * Do the decompression, and jump to the new kernel..

 128 */

 129        leal    z_extract_offset_negative(%ebx), %ebp

 130                                /* push arguments for decompress_kernel: */

 131        pushl   %ebp            /* output address */

 132        pushl   $z_input_len    /* input_len */

 133        leal    input_data(%ebx), %eax

 134        pushl   %eax            /* input_data */

 135        leal    boot_heap(%ebx), %eax

 136        pushl   %eax            /* heap area */

 137        pushl   %esi            /* real mode pointer */

 138        call    decompress_kernel

 139        addl    $20, %esp

167/*

 168 * Jump to the decompressed kernel.

 169 */

 170        xorl    %ebx, %ebx

 171        jmp     *%ebp

看到138行,这段汇编语句调用decompress_kernel函数对内核进行解压缩,前面129~137行为该函数准备参数,这个函数及其参数的具体分析后边会介绍,这里只是说明有这么一个过程。最后两条命令会跳入解压缩后kernel 入口函数:startup_32 (arch/x86/kernel/head_32.S)

 

那么arch/x86/boot/compressed/head_32.S是如何加入到vmlinux.bin中的呢?马上谈到,现在先继续看到arch/x86/boot/Makefilesetup.bin的目标生成规则链为:

113 LDFLAGS_setup.elf       := -T

114 $(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE

115        $(call if_changed,ld)

 

117 OBJCOPYFLAGS_setup.bin  := -O binary

118 $(obj)/setup.bin: $(obj)/setup.elf FORCE

119        $(call if_changed,objcopy)

 

if_changed跟我们的if_changed_rule一样,都是定义于Makefile.lib中,所以我们还是去查看.setup.elf.cmd,看看其最终链接命令为:

cmd_arch/x86/boot/setup.elf := ld -m elf_i386   -T arch/x86/boot/setup.ld arch/x86/boot/a20.o arch/x86/boot/bioscall.o arch/x86/boot/cmdline.o arch/x86/boot/copy.o arch/x86/boot/cpu.o arch/x86/boot/cpucheck.o arch/x86/boot/edd.o arch/x86/boot/header.o arch/x86/boot/main.o arch/x86/boot/mca.o arch/x86/boot/memory.o arch/x86/boot/pm.o arch/x86/boot/pmjump.o arch/x86/boot/printf.o arch/x86/boot/regs.o arch/x86/boot/string.o arch/x86/boot/tty.o arch/x86/boot/video.o arch/x86/boot/video-mode.o arch/x86/boot/version.o arch/x86/boot/video-vga.o arch/x86/boot/video-vesa.o arch/x86/boot/video-bios.o -o arch/x86/boot/setup.elf

 

.setup.bin.cmd的内容:

cmd_arch/x86/boot/setup.bin := objcopy  -O binary arch/x86/boot/setup.elf arch/x86/boot/setup.bin

 

值得一提的是形成setup.elf 的链接脚本arch/i386/boot/setup.ld

   1/*

   2 * setup.ld

   3 *

   4 * Linker script for the i386 setup code

   5 */

   6OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")

   7OUTPUT_ARCH(i386)

   8ENTRY(_start)

   9

  10SECTIONS

  11{

  12        . = 0;

  13        .bstext         : { *(.bstext) }

  14        .bsdata         : { *(.bsdata) }

  15

  16        . = 497;

  17        .header         : { *(.header) }

  18        .entrytext      : { *(.entrytext) }

  19        .inittext       : { *(.inittext) }

  20        .initdata       : { *(.initdata) }

  21        __end_init = .;

  22

  23        .text           : { *(.text) }

  24        .text32         : { *(.text32) }

  25

  26        . = ALIGN(16);

  27        .rodata         : { *(.rodata*) }

  28

  29        .videocards     : {

  30                video_cards = .;

  31                *(.videocards)

  32                video_cards_end = .;

  33        }

  34

  35        . = ALIGN(16);

  36        .data           : { *(.data*) }

  37

  38        .signature      : {

  39                setup_sig = .;

  40                LONG(0x5a5aaa55)

  41        }

  42

  43

  44        . = ALIGN(16);

  45        .bss            :

  46        {

  47                __bss_start = .;

  48                *(.bss)

  49                __bss_end = .;

  50        }

  51        . = ALIGN(16);

  52        _end = .;

  53

  54        /DISCARD/ : { *(.note*) }

  55

  56        /*

  57         * The ASSERT() sink to . is intentional, for binutils 2.14 compatibility:

  58         */

  59        . = ASSERT(_end <= 0x8000, "Setup too big!");

  60        . = ASSERT(hdr == 0x1f1, "The setup header has the wrong offset!");

  61        /* Necessary for the very-old-loader check to work... */

  62        . = ASSERT(__end_init <= 5*512, "init sections too big!");

  63

  64}

 

setup.ld 定义了输出文件的section 组织顺序:由于header.o 位于setup.elf 最前部,只有arch/x86/boot/header.o 定义了这些section

.bstext.bsdata.header.inittext.initdata

 

后面会提到header是整个实模式阶段的开端,0-496字节为.bstext .bsdata段。497开始为.header.inittext.initdata.text段的数据。497 - 511 15个字节为缺省参数, tools/build 在生成bzImage 的过程中会改变某些缺省值。

 

_start函数进入点正好在512偏移地址处:

[root@localhost boot]# nm setup.elf |grep " _start"

0000000000000200 R _start

 

arch/x86/boot/video-*.c 都定义有__videocard,其最终定义为:

#define __videocard struct card_info __attribute__((section(".videocards")))

所有以__videocard属性修饰的数据都会放入.videocards段。

 

再看到45行,.bss段并不会占用实际存储空间,setup.bin.signature位于文件末尾:

[root@localhost boot]# hexdump -C setup.bin | tail -2

00003730  28 36 00 00 55 aa 5a 5a                           |(6..U.ZZ|

00003738

 

最后5962行有三个很重要的ASSERT关键字,对连接后的setup.bin这么一个c程序进行检查,如果_end 地址大于0x8000hdr不等于0x1f1或者__end_init大于5*512,就会发出一些警告信息。注意这个hdr,叫做安装头(setup head),是由bootloader设置的一些系统主要安装信息。这个东西很重要,初始化很大一段程序会围绕着它工作。

 

下面再来看看vmlinux.binvmlinux.bin的目标生成规则链为:

86 $(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE

87         $(call if_changed,objcopy)

121 $(obj)/compressed/vmlinux: FORCE

122        $(Q)$(MAKE) $(build)=$(obj)/compressed $@

 

arch/x86/boot/compressed/Makefilevmlinux规则为:

26$(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/head_$(BITS).o $(obj)/misc.o $(obj)/piggy.o FORCE

27         $(call if_changed,ld)

28         @:

 

compressed目录下Makefile的编译过程要复杂一些,我们看到:

targets := vmlinux.lds vmlinux vmlinux.bin vmlinux.bin.gz vmlinux.bin.bz2 vmlinux.bin.lzma vmlinux.bin.lzo head_$(BITS).o misc.o piggy.o

所以大致分为几步:

1.       通过编译vmlinux.lds.S 生成链接脚本vmlinux.lds

2.       使用objcopy命令从顶层目录拷贝刚刚生成的vmlinuxarch/x86/boot/compressed/目录中,并删除其中的.comment段,也就是把注释给删掉。

3.       根据编译选项,选择一个压缩程序,对上一步的vmlinux.bin进行压缩,由于我使用的默认配置是CONFIG_KERNEL_GZIP,所以生成的是vmlinux.bin.gz文件。

4.       编译head_32.S生成head_32.o

5.       编译misc.c,生成misc.o

6.       编译piggy.S汇编源文件,生成piggy.o

7.       使用arch/x86/boot/compressed/vmlinux.lds链接脚本将head_32.omisc.opiggy.o链接生成arch/x86/boot/compressed/vmlinux

 

arch/x86/boot/compressed/目录中的Makefile执行完毕后,会在其目录下生成一个新的vmlinux。随后arch/x86/boot/目录的Makefile使用objcopy命令拷贝刚刚生成的vmlinux除掉ELF header.note段等无用信息后便在arch/x86/boot/目录下生成另一个二进制格式的vmlinux.bin

 

一定要注意,此时就有了两个vmlinux,一个在顶层目录,一个在arch/x86/boot/compressed/目录下;还有两个vmlinux.bin,一个在arch/x86/boot/compressed/,另一个在arch/x86/boot/目录下。而最终,我们bzImage需要的vmlinux.binarch/x86/boot下的那个。

 

vmlinux.binsetup.bin工作完成后,就开始链接bzImage了。arch/x86/boot/tools/build 是用于构建最终bzimage 的实用程序,他的作用就是把setup.binvmlinux.bin连接到一起:setup.bin 按照512字节对齐,同时负责把rootdev,内核crc,以及setupkernel 的大小,patchsetup.bin 开头的arch/x86/boot/head_32.S 中。我们来看看.bzImage.cmd的内容:

cmd_arch/x86/boot/bzImage := arch/x86/boot/tools/build arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin CURRENT > arch/x86/boot/bzImage

 

很简单的拼接成功后,就会在arch/x86/boot/目录下生成bzImage文件。至此,需要告诉你的内核映像生成过程就结束了,我们总结一下:最终bzImage由两部分顺序拼接而成:setup.binvmlinux.bin。其中setup.bin通过setup.ld链接脚本涵盖了arch/x86/boot目录下几乎所有的代码,其重中之重是该目录下的header文件,它是整个内核部分的最先被执行的实模式代码;而vmlinux.bin不仅包含了vmlinux的压缩代码和一个对其解压缩的程序,还包含了arch/x86/boot/compressed目录下的head_32.S代码,该代码最重要的是包含了保护模式的入口函数startup_32

 

而至于make随后的三个命令:make modulesmake modules_installmake install的执行过程,我们就不详细分析了,这里只简单提一下make install,是执行这么一个命令:

sh /usr/src/kernels/linux-2.6.34.1/arch/x86/boot/install.sh 2.6.34.1 arch/x86/boot/bzImage /

              System.map "/boot"

 

我们来看看arch/x86/boot/install.sh脚本的内容:

  20verify () {

  21        if [ ! -f "$1" ]; then

  22                echo ""                                          1>&2

  23                echo " *** Missing file: $1"                         1>&2

  24                echo ' *** You need to run "make" before "make install".' 1>&2

  25                echo ""                                          1>&2

  26                exit 1

  27        fi

  28}

  29

  30# Make sure the files actually exist

  31verify "$2"

  32verify "$3"

  33

  34# User may have a custom install script

  35

  36if [ -x ~/bin/${INSTALLKERNEL} ]; then exec ~/bin/${INSTALLKERNEL} "$@"; fi

  37if [ -x /sbin/${INSTALLKERNEL} ]; then exec /sbin/${INSTALLKERNEL} "$@"; fi

  38

  39# Default install - same as make zlilo

  40

  41if [ -f $4/vmlinuz ]; then

  42        mv $4/vmlinuz $4/vmlinuz.old

  43fi

  44

  45if [ -f $4/System.map ]; then

  46        mv $4/System.map $4/System.old

  47fi

  48

  49cat $2 > $4/vmlinuz

  50cp $3 $4/System.map

  51

  52if [ -x /sbin/lilo ]; then

  53       /sbin/lilo

  54elif [ -x /etc/lilo/install ]; then

  55       /etc/lilo/install

  56else

  57       sync

  58       echo "Cannot find LILO."

  59fi

 

很简单的一个安装脚本,主要做了以下三个工作:

1.  我们把刚才压缩出来的内核映像bzImage,拷入到/boot目录,名字改成 vmlinuz-2.6.34.1

2.  调用mkinitrd程序来创建imitrd-xxx.img文件,其作用我们前边已经讲过了。其中xxx为内核的版本号,是通过查看 /lib/modules来版本来对应的,我们是编译出来的是 2.6.34.1,所以就运行上面的命令创建,创建的出来的是initrd-2.6.34.1.img。不创建这个文件,有时是启动不起来的,比如提示VFS错误等;

3.  拷贝System.map符号映射表到/boot目录下。这个表是把内核所有的全局变量和它对应的虚拟地址全部列出来,感兴趣的同学可以去cat一下。

 

好了,通过对内核映像的编译,我们了解了它是怎么形成的,以及一些重要的编译、链接知识。这些东西对我们后面研究系统如何初始化,乃至整个系统的工作模式都很重要,希望给大家的Linux知识的学习带来帮助。

你可能感兴趣的:(工作,header,脚本,makefile,linux内核,linker)