环境:
硬件平台:ARM9 S3C2440 TQ2440开发板。
软件环境:VM7.1虚拟机;Fedora10;arm-linux-gcc 4.3.3;Linux2.6.35;u-boot2010.06(天嵌原版本)
一、 zImage、uImage和vmLinux相关概念
当正确配置完内核后,采用make zImage 、make bzImage 、make uImage等命令编译内核镜像时,都会生成vmLinux。Ls –l arch/arm/boot/compressed/ 可以看到:
-rwxr-xr-x 1 root root 2084135 09-26 14:18 vmlinux
Vmlinux是ELF格式的可引导的、压缩内核。其中的VM是”virtual memory”的缩写。其中make zImage和make bzImage的区别在这里就不多说了,需要注意的是,make bzImage常常会让人误解成内核是bzip2格式进行压缩的,其实则不然,bz实际上是”big zImage”的意思。在Linux2.6.35内核中,可以选用三种压缩方式,分别为:GZIP、LZMA和LZO。其中最常用的是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'
可以看出,zImage是vmlinux通过objcopy后生成的;打开arch/arm/boot/compressed/Makefie文件:
66 suffix_$(CONFIG_KERNEL_GZIP) = gzip
可以看出,当配置内核为GZIP方式时,zImage的压缩方式使用gzip。现在可以清楚的明白zImage和vmlinux的关系了,一句话总结一下:zImage就是vmlinux通过objcopy、gzip压缩后,得到的内核,其头部是由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 。它完的工作主要是:开启MMU和CACHE,设置解压地址、缓存地址、入口地址等(具体的内容见第三部分代码详解),调用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保存的是链接zImage时LC0标号处的地址,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镜像copy到sdram的0x30008000位置处。此时为初始状态,这里称为状态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内核学习时的一些笔记,并不一定正确,希望大家指正批评,谢谢。