linux zImage啟動流程分析

时间:2021-11-05 15:13:48

环境:

         硬件平台:ARM9 S3C2440 TQ2440开发板。

         软件环境:VM7.1虚拟机;Fedora10arm-linux-gcc 4.3.3Linux2.6.35u-boot2010.06(天嵌原版本)

一、   zImageuImagevmLinux相关概念

当正确配置完内核后,采用make zImage make bzImage 、make uImage等命令编译内核镜像时,都会生成vmLinuxLs –l arch/arm/boot/compressed/ 可以看到:

-rwxr-xr-x 1 root root 2084135 09-26 14:18 vmlinux

VmlinuxELF格式的可引导的、压缩内核。其中的VM”virtual memory”的缩写。其中make zImagemake bzImage的区别在这里就不多说了,需要注意的是,make bzImage常常会让人误解成内核是bzip2格式进行压缩的,其实则不然,bz实际上是”big zImage”的意思。在Linux2.6.35内核中,可以选用三种压缩方式,分别为:GZIPLZMALZO。其中最常用的是GZIP。在zImage内核的内部,开头处内嵌有解压缩的代码,当bootloader引导结束后,将控制权将给内核的时候,内核可以实现自解压,有兴趣有读者可以参考内核代码:arch/arm/boot/compressed/head.S和该目录下的misc.c文件。

打开arch/arm/boot/Makefile文件:

56 $(obj)/zImage:  $(obj)/compressed/vmlinux FORCE

 57         $(call if_changed,objcopy)

 58         @echo '  Kernel: $@ is ready'

可以看出,zImagevmlinux通过objcopy后生成的;打开arch/arm/boot/compressed/Makefie文件:

66 suffix_$(CONFIG_KERNEL_GZIP) = gzip

可以看出,当配置内核为GZIP方式时,zImage的压缩方式使用gzip。现在可以清楚的明白zImagevmlinux的关系了,一句话总结一下:zImage就是vmlinux通过objcopygzip压缩后,得到的内核,其头部是由head.S misc.c组成的自解压代码。

u-boot所一直追捧的uImage格式,只是在zImage的前面加上了40Byte的内核头部信息,由于本文主要讲解zImage,此处暂不对uImage进行过多的分析。

二、   启动过程综述

zImage内核有着解压速度快、体积小、编译简单等优点,很适合应用在嵌入式领域中,但U-BOOT一直以来都追捧uImage格式,对zImage的格式是不支持的。因此,要想完全理解zImage的启动过程,我们还要从U-BOOT说起。

我们这里使用的U-BOOT版本是天嵌科技提供的2010.06月版,支持的内核大小为3M。天嵌科技的U-BOOT(以下简称u-boot),支持zImage镜像,其主要实现的代码在 ./lib-arm/boot-zImage.c文件中实现。

当启动zImage内核时,u-boot调用boot_zImage函数(前面部分略过,从boot_zImage函数讲起),该函数完成以下工作:

1.       设置内核由nand flash复制到sdram中的地址:0x30008000

2.       调用copy_kernel_img 函数复制内核到sdram;

3.       设置Image magic number;

4.       设置传递给内核的参数地址为0x30001000;

5.       设置机器码为168

6.       最后调用call_linux函数,将控制权彻底交给内核。

当完成了上述工作后,内核开始启动,zImage内核的入口程序为:arch/arm/boot/compressedhead.S 。它完的工作主要是:开启MMUCACHE,设置解压地址、缓存地址、入口地址等(具体的内容见第三部分代码详解),调用decompress_kernel函数(arch/arm/boot/compressedmisc.c)解压内核,调用call_kernel (注意:call_kernel arch/arm/boot/compressedhead.S的一个标号)调用解压后的内核。

三、重点代码分析

1. arch/arm/boot/compressedhead.S分析

 进入内核代码后,首先保存u-boot传进来的机器码到r7中,保存参数指针到r8中,代码如下:

 1:          mov         r7, r1                           @ save architecture ID

                      mov     r8, r2                           @ save atags pointer

   接着,对处理器的启动模式进行判断,进入SVC模式,关中断等。重点是要理解这段代码:

                   adr   r0, LC0

                 ARM(                 ldmia        r0, {r1, r2, r3, r4, r5, r6, r11, ip, sp})

       subs        r0, r0, r1           @ calculate the delta offsetmj

                                     @ if delta is zero, we are

                   beq  not_relocated           @ running at the address we

                                                        @ were linked at.

                   /*

                    * We're running at a different address.  We need to fix

                    * up various pointers:

                    *   r5 - zImage base address (_start)

                    *   r6 - size of decompressed image

                    *   r11 - GOT start

                    *   ip - GOT end

                    */

                   add  r5, r5, r0

                   add  r11, r11, r0

                   add  ip, ip, r0

                   ````````````````````````

                   LC0:          .word        LC0                     @ r1

                                      .word        __bss_start               @ r2

                                      .word        _end                            @ r3

                                      .word        zreladdr            @ r4

                                      .word        _start                          @ r5

                                      .word        _image_size              @ r6

                                      .word        _got_start                 @ r11

                                      .word        _got_end          @ ip

                                      .word        user_stack+4096              @ sp

         这段代码的作用的是将LC0标号后面的地址,传到各个寄存器中,经过这段代码后,r1保存的是链接zImageLC0标号处的地址,r2中保存的是bss段的起始地址,r4中保存的是内核加载的物理地址,r5保存的是镜像的起始地址,r6保存的是镜像的大小,如果还是不能理解,请用source insight的查找功能(Ctrl+/),对其进行查找,便于理解。

         其中r0保存的是运行到LC0标号时的PC值。要注意理解adr指令的功能:将基于PC值的偏移地址存入寄存器中。后面的subs r0,r0,r1,是判断链接地址和运行地址的差,如果是0,说明无偏移,则跳过,无需重定位;否则,则要重定位。

接着,如果没有定义CONFIG_ZBOOT_ROM,还要重定位bss段和GOT

最后,打开CACHE,设置指针,设置缓存(64K),准备解压内核,在解压内核之前,还要检测是否overwrite,代码如下:

/*

 * Check to see if we will overwrite ourselves.

 *   r4 = final kernel address

 *   r5 = start of this image

 *   r6 = size of decompressed image

 *   r2 = end of malloc space (and therefore this image)

 * We basically want:

 *   r4 >= r2 -> OK  /*解压后的内核Image地址是否在分配的空间之后*/

 *   r4 + image length <= r5 -> OK/*Image是否在zImage之前*/

 */

                   cmp r4, r2

                   bhs   wont_overwrite

                   add  r0, r4, r6

                   cmp r0, r5

                   bls    wont_overwrite

 

                   mov r5, r2                           @ decompress after malloc space

                   mov r0, r5

                   mov r3, r7

                   bl      decompress_kernel

 

经过判断后,如果没有出现overwrite现象,就直接解压内核,否则的话,还要进行代码的搬运。搬运的代码在此就不列出了。

调用decompress_kernel函数时需要传递四个参数,根据规则,使用r0—r3四个寄存器,分别传递:r0传递Image的首地址;r1传递分配缓存空间的首地址;r2缓存空间的末地址;r3传递的是一开始保存的机器码,这也正与

decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,unsigned long free_mem_ptr_end_p,    int arch_id)

函数的四个参数相对应。

为了能更形象的表示内存中各段的位置,请看下面的内存位置表。u-boot会将zImage镜像copysdram0x30008000位置处。此时为初始状态,这里称为状态1

状态1

.text

         0x30008000 (zImage)

.got

.data

.bss

.stack       (4K)

         当调用解压函数decompress_kernel后,内存中的各段位置如下,状态2

状态2

.text

         0x30008000 (zImage)

.got

.data

.bss

.stack (4K)

.64K缓存区 解压内核所需

.Image               解压后的内核        

         当如果head.S中有代码搬运工作时,即出现overwrite时,内存中的各段位置如,状态3

状态3

.text

         0x30008000 (zImage)

.got

.data

.bss

.stack (4K)

.64K缓存区 解压内核所需

.Image               解压后的内核        

.reloc_start      重定位的代码

.reloc_end

         以上3个表,是我个人浅见,并不代表其它人的观点,如果错误,还请不吝赐教。

2、部分重点地址的说明

         看了上面一堆地址,现在肯定是有点晕了,要想真正弄懂其本质,还要多看代码,下面列出部分重要地址在代码中的出处,方便读者。

(1).arch/arm/Makefile中的

111          textofs-y       := 0x00008000

212          # The byte offset of the kernel image in RAM from the start of RAM.

213          TEXT_OFFSET := $(textofs-y)

 

         此处,定义了Image的偏移地址。注意看原版注释。

(2).arch/arm/boot/Makefile中的

20 # Note: the following conditions must always be true:

 21 #   ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)

 22 #   PARAMS_PHYS must be within 4MB of ZRELADDR

 23 #   INITRD_PHYS must be in RAM

 24 ZRELADDR    := $(zreladdr-y)

 25 PARAMS_PHYS := $(params_phys-y)

 26 INITRD_PHYS := $(initrd_phys-y)

 

         此处定义了,内核的物理地址和u-boot传递参数的地址,也就是加载地址。此处要和u-boot一致。

(3).arch/arm/mach-s3c2410/Makefile.boot中的

  1 ifeq ($(CONFIG_PM_H1940),y)

  2         zreladdr-y              := 0x30108000

  3         params_phys-y   := 0x30100100

  4 else

  5         zreladdr-y              := 0x30008000

  6         params_phys-y   := 0x30000100

  7 endif

 

         此处定了物理地址和参数地址的具体址。

(4).include/asm-generic/Page.h
#ifdef CONFIG_KERNEL_RAM_BASE_ADDRESS

#define PAGE_OFFSET               (CONFIG_KERNEL_RAM_BASE_ADDRESS)

#else

#define PAGE_OFFSET               (0)

#endif

 

         此处定义了PAGE_OFFSET

3、解压代码分析

经过head.S的初始化后,调用了arch/arm/boot/compressed/misc.c文件中的decompress_kernel函数,现在来看一下这个函数,为方便浏览,现将函数的代码贴到下面,并在后面注释。

unsigned long

decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,

                  unsigned long free_mem_ptr_end_p,

                  int arch_id)

{

         unsigned char *tmp;

         output_data              = (unsigned char *)output_start;

         free_mem_ptr                   = free_mem_ptr_p;

         free_mem_end_ptr          = free_mem_ptr_end_p;

         __machine_arch_type     = arch_id;

         arch_decomp_setup();

         tmp = (unsigned char *) (((unsigned long)input_data_end) - 4);

         output_ptr = get_unaligned_le32(tmp);

         putstr("Uncompressing Linux...");

         do_decompress(input_data, input_data_end - input_data,

                            output_data, error);

         putstr(" done, booting the kernel.\n");

         return output_ptr;

}

此函数在取出head.S传递过来的参数后,调用arch_decomp_setup函数进行初始化,实现的功能是检测CPU型号、使能看门狗(如果在配置内核时配置的前提下)和串口。之后调用do_decompress函数进行解压,do_decompress函数是在arch/arm/boot/compressed/Decompress.c文件中的。其代码如下:

#ifdef CONFIG_KERNEL_GZIP

#include "../../../../lib/decompress_inflate.c"

#endif

#ifdef CONFIG_KERNEL_LZO

#include "../../../../lib/decompress_unlzo.c"

#endif

#ifdef CONFIG_KERNEL_LZMA

#include "../../../../lib/decompress_unlzma.c"

#endif

void do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))

{

         decompress(input, len, NULL, NULL, output, NULL, error);

}

当配置内核时选中GZIP方式,就会#include “../../../../lib/decompress_inflate.c”。文件最后一行为:

#define decompress gunzip

即:最后调用 gunzip函数对zImage进行解压。

当完成所有解压任务后,又将跳转回head.S文件中,执行call_kernel,将启动真正的Image.

 

以上只是这几天对 2.6.35内核学习时的一些笔记,并不一定正确,希望大家指正批评,谢谢。