回到顶层Makefile的849行,随着vmlinux的链接工作结束,在主目录下生成了vmlinux文件,vmlinux目标也就结束了。那么就会来到arch/x86/Makefile的155行bzImage目标。看到这个目标,就知道后面的工作就算打包压缩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-bit或64-bit保护模式,设置好必须的参数后才能开启分页模式转到32-bit或64-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 header,Program header table或Section header table。这些ELF header 和一些section 的作用是告诉内核ELF Loader 如何载入ELF 可执行文件。但是, Linux内核作为一种特殊的ELF 文件,没有ELF Loader的帮助,需要特殊辅助程序去装载它。它的装载地址是固定的,前面讲了的,0xC0100000。
这时,为了保证通用性而存在的ELF header 和一些section对内核的装载就没有意义了。为了使内核尽可能小,可以把这些信息去掉。所以通常采用objcopy命令来去掉原来ELF文件中的header和section,同时转化为raw binary格式的文件。即便如此,通过objcopy 处理过的vmlinux 一般需要压缩以后再重新链接,当然必须把解压缩的程序也同时链接进来。
这个压缩的过程着实奇怪:压缩后,然后再解压缩,岂不是浪费启动时间?这还是因为当x86系列处理器启动初期,处于实模式状态,可以寻得的地址空间十分有限,仅仅是1MB,如果内核过大,就无法加载。更新的内核(从2.6.30开始)甚至提供了更高压缩率的格式:bzip或LZMA。
由上面的分析可以看出, 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/Makefile中bzImage的规则如下:
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.bin、arch/x86/boot/vmlinux.bin、arch/x86/boot/tools/build。tools/build 已加到hostprogs-y中去了,会生成相应的主机程序。tools/build主机程序的主要工作是将arch/x86/boot/setup.bin和arch/x86/boot/vmlinux.bin 拼接在一起,后面会看到。下面我们将分别讲述setup.bin 和vmlinux.bin的生成过程。
先来梳理一下前面讲的系统启动流程。BIOS从启动设备(如Hard Disk 的MBR)中装载grub:stage1是简单的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.S的startup_32函数是被链接进了vmlinux,随后,vmlinux将会被压缩,所以,这一个startup_32就不存在了。
看到arch/x86/boot/compressed/head_32.S的入口函数startup_32,startup_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/Makefile,setup.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
最后59到62行有三个很重要的ASSERT关键字,对连接后的setup.bin这么一个c程序进行检查,如果_end 地址大于0x8000、hdr不等于0x1f1或者__end_init大于5*512,就会发出一些警告信息。注意这个hdr,叫做安装头(setup head),是由bootloader设置的一些系统主要安装信息。这个东西很重要,初始化很大一段程序会围绕着它工作。
下面再来看看vmlinux.bin,vmlinux.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/Makefile中vmlinux规则为:
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命令从顶层目录拷贝刚刚生成的vmlinux到arch/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.o,misc.o,piggy.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.bin是arch/x86/boot下的那个。
vmlinux.bin和setup.bin工作完成后,就开始链接bzImage了。arch/x86/boot/tools/build 是用于构建最终bzimage 的实用程序,他的作用就是把setup.bin和vmlinux.bin连接到一起:setup.bin 按照512字节对齐,同时负责把rootdev,内核crc,以及setup和kernel 的大小,patch到setup.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.bin和vmlinux.bin。其中setup.bin通过setup.ld链接脚本涵盖了arch/x86/boot目录下几乎所有的代码,其重中之重是该目录下的header文件,它是整个内核部分的最先被执行的实模式代码;而vmlinux.bin不仅包含了vmlinux的压缩代码和一个对其解压缩的程序,还包含了arch/x86/boot/compressed目录下的head_32.S代码,该代码最重要的是包含了保护模式的入口函数startup_32。
而至于make随后的三个命令:make modules、make modules_install和make 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知识的学习带来帮助。