一、概述
linux内核镜像常见到的有两种形式,zImage和uImage。这两种文件的格式稍有差别,所以启动这两种格式的内核镜像也会有所不同。目前,uboot只支持启动uImage类型的镜像,对zImage还不支持(但是可以移植,TQ2440就是这样做的)。
二、uImage和zImage
1、zImage
zImage是用命令“#make zImage”生成的,我截取了生成信息最后部分的内容如下:
OBJCOPY arch/arm/boot/Image Kernel: arch/arm/boot/Image is ready GZIP arch/arm/boot/compressed/piggy.gz AS arch/arm/boot/compressed/piggy.o LD arch/arm/boot/compressed/vmlinux OBJCOPY arch/arm/boot/zImage Kernel: arch/arm/boot/zImage is ready
从中可以看到,zImage是经过gzip压缩过的,所以在内核启动过程(不属于u-boot控制范围,在内核镜像的头部嵌有解压函数)中必然会对应一个解压过程。
2、uImage
(1) 生成方法
uImage是u-boot专用的内核镜像,可用命令“#make uImage”生成。生成信息最后部分的内容如下:
Kernel: arch/arm/boot/Image is ready Kernel: arch/arm/boot/zImage is ready UIMAGE arch/arm/boot/uImage Image Name: Linux-2.6.30.4-EmbedSky Created: Thu Mar 20 19:53:32 2014 Image Type: ARM Linux Kernel Image (uncompressed) Data Size: 2314736 Bytes = 2260.48 kB = 2.21 MB Load Address: 0x30008000 Entry Point: 0x30008000 Image arch/arm/boot/uImage is ready
事实上,uImage是调用mkimage(uboot制作的工具)这个工具生成的。
root@daneiqi:/opt/EmbedSky# mkimage -n 'linux-2.6.30' -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008000 -d zImage uImage Image Name: linux-2.6.30 Created: Thu Mar 20 19:59:36 2014 Image Type: ARM Linux Kernel Image (uncompressed) Data Size: 2314736 Bytes = 2260.48 kB = 2.21 MB Load Address: 0x30008000 Entry Point: 0x30008000
(2)特点
在原来的可执行映象文件zImage的前面加上一个0x40字节的头, 记录参数所指定的信息,这样uboot才能识别这个映象是针对哪个CPU体系结构的,哪个OS的, 哪种类型,加载内存中的哪个位置,入口点在内存的那个位置以及映象名是什么。
(3)image_header
头部的结构是在include/image.h中定义的,如下所示:
typedef struct image_header { uint32_t ih_magic; /* Image Header Magic Number */ uint32_t ih_hcrc; /* Image Header CRC Checksum */ uint32_t ih_time; /* Image Creation Timestamp */ uint32_t ih_size; /* Image Data Size */ uint32_t ih_load; /* Data Load Address */ uint32_t ih_ep; /* Entry Point Address */ uint32_t ih_dcrc; /* Image Data CRC Checksum */ uint8_t ih_os; /* Operating System */ uint8_t ih_arch; /* CPU architecture */ uint8_t ih_type; /* Image Type */ uint8_t ih_comp; /* Compression Type */ uint8_t ih_name[IH_NMLEN]; /* Image Name */ } image_header_t;
打开上边生成的uImage文件,可以看到对应的数据。
(1)ih_magic 0x27051956 magic值,我觉得是uImage的头部开始值,根据这个值,判断是否是uImage
(2)ih_crc 0x19dbf9c6 头部校验
(3)ih_time 0x74295319 创建时间
(4)ih_size 0x002351f0 镜像大小为2260.48KB
(5)ih_load 0x30008000 内核加载地址
(6)ih_ep 0x30008000 内核运行地址,“theKernel”指向该地址,说明这里藏着进入第一个函数--解压
(7)ih_dcrc 0x38fc654e 内核校验
(8)ih_os 0x05 #define IH_OS_LINUX 5 /* Linux */
(9)ih_arch 0x02 #define IH_CPU_ARM 2 /* ARM */
(10)ih_type 0x02 #define IH_TYPE_KERNEL 2 /* OS Kernel Image */
(11)ih_comp 0x00 #define IH_COMP_NONE 0 /* No Compression Used */
(12)ih_name Linux_2.6.30.4-EmbedSky
三、u-boot内核启动流程概述
前文已经说明u-boot只支持uImage,步骤三、四都是针对uImage的。
另外声明一点,步骤三四的测试uboot代码是韦东山视频提供的。
1、从NandFlash中读取内核到RAM中
2、在RAM中,给内核进行重定位
3、给内核传递参数
4、启动内核
四、u-boot启动内核细节分析
1、启动命令
从环境变量中查看启动命令:
2、从NandFlash中读取内核到RAM中
nand read.jffs2 0x30007FC0 kernel
此命令会激活(common/cmd_nand.c)中的do_nand函数,从而将nandflash上的kernel分区加载到0x30007fc0位置处。
OpenJTAG> mtd device nand0 <nandflash0>, # parts = 4 #: name size offset mask_flags 0: bootloader 0x00040000 0x00000000 0 1: params 0x00020000 0x00040000 0 2: kernel 0x00200000 0x00060000 0 3: root 0x0fda0000 0x00260000 0 active partition: nand0,0 - (bootloader) 0x00040000 @ 0x00000000 defaults: mtdids : nand0=nandflash0 mtdparts: mtdparts=nandflash0:256k@0(bootloader),128k(params),2m(kernel),-(root)
从分区表中,可以看出kernel分区的起始地址是0x60000,大小是0x200000(2M),这条命令实际上等效于
nand read.jffs2 0x30007FC0 0x60000 0x200000
也可以使用命令
nand read 0x30007FC0 0x60000 0x200000
nand read.jffs2可以自动页对齐,所以大小可以是非页整的;如果使用nand read的大小必须是页对齐的。
3、读取uImage头部
bootm 0x30007fc0
此命令会激活(common/cmd_bootm.c)中的do_bootm函数,从而开始执行
2、在RAM中,给内核进行重定位 3、给内核传递参数 4、启动内核
image_header_t header; 定义一个全局变量header,是读取头部的缓冲区
addr = simple_strtoul(argv[1], NULL, 16); 定位头部地址,将字符串“0x30007fc0”转化为整型
printf ("## Booting image at %08lx ... ", addr); 显示从哪儿启动
memmove (&header, (char *)addr, sizeof(image_header_t)); 读取头部到header变量中
4、判断当前的内存区是否是uImage的开始位置
if (ntohl(hdr->ih_magic) != IH_MAGIC) { { puts ("Bad Magic Number "); SHOW_BOOT_PROGRESS (-1); return 1; } }
注意到:
#define IH_MAGIC 0x27051956 /* Image Magic Number */(include/image.h)
5、校验头部
data = (ulong)&header; len = sizeof(image_header_t); checksum = ntohl(hdr->ih_hcrc); hdr->ih_hcrc = 0; if (crc32 (0, (uchar *)data, len) != checksum) { puts ("Bad Header Checksum "); SHOW_BOOT_PROGRESS (-2); return 1; }
6、打印头部信息
/* for multi-file images we need the data part, too */ print_image_hdr ((image_header_t *)addr);
7、核查内核数据
data = addr + sizeof(image_header_t); len = ntohl(hdr->ih_size); if (verify) { puts (" Verifying Checksum ... "); if (crc32 (0, (uchar *)data, len) != ntohl(hdr->ih_dcrc)) { printf ("Bad Data CRC "); SHOW_BOOT_PROGRESS (-3); return 1; } puts ("OK "); } SHOW_BOOT_PROGRESS (4);
注意到data已经跳过了uImage的头部,指向了真正的内核首部,也即0x30008000。
8、核查架构、内核类型、压缩类型等信息,其中会涉及到重定位
len_ptr = (ulong *)data; #if defined(__PPC__) if (hdr->ih_arch != IH_CPU_PPC) #elif defined(__ARM__) if (hdr->ih_arch != IH_CPU_ARM) #elif defined(__I386__) if (hdr->ih_arch != IH_CPU_I386) #elif defined(__mips__) if (hdr->ih_arch != IH_CPU_MIPS) #elif defined(__nios__) if (hdr->ih_arch != IH_CPU_NIOS) #elif defined(__M68K__) if (hdr->ih_arch != IH_CPU_M68K) #elif defined(__microblaze__) if (hdr->ih_arch != IH_CPU_MICROBLAZE) #elif defined(__nios2__) if (hdr->ih_arch != IH_CPU_NIOS2) #elif defined(__blackfin__) if (hdr->ih_arch != IH_CPU_BLACKFIN) #elif defined(__avr32__) if (hdr->ih_arch != IH_CPU_AVR32) #else # error Unknown CPU type #endif { printf ("Unsupported Architecture 0x%x ", hdr->ih_arch); SHOW_BOOT_PROGRESS (-4); return 1; } SHOW_BOOT_PROGRESS (5); switch (hdr->ih_type) { case IH_TYPE_STANDALONE: name = "Standalone Application"; /* A second argument overwrites the load address */ if (argc > 2) { hdr->ih_load = htonl(simple_strtoul(argv[2], NULL, 16)); } break; case IH_TYPE_KERNEL: name = "Kernel Image"; break; case IH_TYPE_MULTI: name = "Multi-File Image"; len = ntohl(len_ptr[0]); /* OS kernel is always the first image */ data += 8; /* kernel_len + terminator */ for (i=1; len_ptr[i]; ++i) data += 4; break; default: printf ("Wrong Image Type for %s command ", cmdtp->name); SHOW_BOOT_PROGRESS (-5); return 1; } SHOW_BOOT_PROGRESS (6); /* * We have reached the point of no return: we are going to * overwrite all exception vector code, so we cannot easily * recover from any failures any more... */ iflag = disable_interrupts(); #ifdef CONFIG_AMIGAONEG3SE /* * We've possible left the caches enabled during * bios emulation, so turn them off again */ icache_disable(); invalidate_l1_instruction_cache(); flush_data_cache(); dcache_disable(); #endif switch (hdr->ih_comp) { case IH_COMP_NONE: if(ntohl(hdr->ih_load) == data) { printf (" XIP %s ... ", name); } else { #if defined(CONFIG_HW_WATCHDOG) || defined(CONFIG_WATCHDOG) size_t l = len; void *to = (void *)ntohl(hdr->ih_load); void *from = (void *)data; printf (" Loading %s ... ", name); while (l > 0) { size_t tail = (l > CHUNKSZ) ? CHUNKSZ : l; WATCHDOG_RESET(); memmove (to, from, tail); to += tail; from += tail; l -= tail; } #else /* !(CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG) */ memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len); #endif /* CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG */ } break; case IH_COMP_GZIP: printf (" Uncompressing %s ... ", name); if (gunzip ((void *)ntohl(hdr->ih_load), unc_len, (uchar *)data, &len) != 0) { puts ("GUNZIP ERROR - must RESET board to recover "); SHOW_BOOT_PROGRESS (-6); do_reset (cmdtp, flag, argc, argv); } break; #ifdef CONFIG_BZIP2 case IH_COMP_BZIP2: printf (" Uncompressing %s ... ", name); /* * If we've got less than 4 MB of malloc() space, * use slower decompression algorithm which requires * at most 2300 KB of memory. */ i = BZ2_bzBuffToBuffDecompress ((char*)ntohl(hdr->ih_load), &unc_len, (char *)data, len, CFG_MALLOC_LEN < (4096 * 1024), 0); if (i != BZ_OK) { printf ("BUNZIP2 ERROR %d - must RESET board to recover ", i); SHOW_BOOT_PROGRESS (-6); udelay(100000); do_reset (cmdtp, flag, argc, argv); } break; #endif /* CONFIG_BZIP2 */ default: if (iflag) enable_interrupts(); printf ("Unimplemented compression type %d ", hdr->ih_comp); SHOW_BOOT_PROGRESS (-7); return 1; } puts ("OK "); SHOW_BOOT_PROGRESS (7); switch (hdr->ih_type) { case IH_TYPE_STANDALONE: if (iflag) enable_interrupts(); /* load (and uncompress), but don't start if "autostart" * is set to "no" */ if (((s = getenv("autostart")) != NULL) && (strcmp(s,"no") == 0)) { char buf[32]; sprintf(buf, "%lX", len); setenv("filesize", buf); return 0; } appl = (int (*)(int, char *[]))ntohl(hdr->ih_ep); (*appl)(argc-1, &argv[1]); return 0; case IH_TYPE_KERNEL: case IH_TYPE_MULTI: /* handled below */ break; default: if (iflag) enable_interrupts(); printf ("Can't boot image type %d ", hdr->ih_type); SHOW_BOOT_PROGRESS (-8); return 1; } SHOW_BOOT_PROGRESS (8);
在这部分代码中,有这么一部分关于压缩类型的:
switch (hdr->ih_comp) { case IH_COMP_NONE: if(ntohl(hdr->ih_load) == data) { printf (" XIP %s ... ", name); } else { #if defined(CONFIG_HW_WATCHDOG) || defined(CONFIG_WATCHDOG) size_t l = len; void *to = (void *)ntohl(hdr->ih_load); void *from = (void *)data; printf (" Loading %s ... ", name); while (l > 0) { size_t tail = (l > CHUNKSZ) ? CHUNKSZ : l; WATCHDOG_RESET(); memmove (to, from, tail); to += tail; from += tail; l -= tail; } #else /* !(CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG) */ memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len); #endif /* CONFIG_HW_WATCHDOG || CONFIG_WATCHDOG */ } break;
可以看到,u-boot会判断当前去除uImage头部内核代码所处的位置(7步骤已经说明地址是data)是否与编译时安排的重定位位置(hdr->ih_load)一致。
如果一致,就打印一句话。
如果不一致,则需要调用 memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);进行内核的重定位,要知道它有2M多的大小,会花费一些时间。尽量使读取内核的时候,就读取到hdr->ih_load-64的位置上,这样就不必再搬运一次。
9、根据操作系统类型,启动对应的操作系统
switch (hdr->ih_os) { default: /* handled by (original) Linux case */ case IH_OS_LINUX: #ifdef CONFIG_SILENT_CONSOLE fixup_silent_linux(); #endif do_bootm_linux (cmdtp, flag, argc, argv, addr, len_ptr, verify); break; case IH_OS_NETBSD:
10、执行do_bootm_linux,继续启动linux系统
此函数在lib_arm/armlinux.c中
void (*theKernel)(int zero, int arch, uint params); image_header_t *hdr = &header; bd_t *bd = gd->bd; #ifdef CONFIG_CMDLINE_TAG char *commandline = getenv ("bootargs"); #endif theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
可见,已经将内核运行的首地址赋给了theKernel函数指针变量,将来可以利用这个变量调用进入内核的函数。
另外,在进入内核之前,要给内核传递参数。方法是将参数以一定的结构放在内存指定的位置上,将来内核从该地址读取数据即可。
命令行的启动参数存储在以bootargs命名的对象里,值为
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
告诉内核,启动后的根文件系统位于mtd的哪个区,初始进程,以及控制台
11、判断是否是一个ramdisk或者multi镜像
/* * Check if there is an initrd image */ if (argc >= 3) { SHOW_BOOT_PROGRESS (9); addr = simple_strtoul (argv[2], NULL, 16); printf ("## Loading Ramdisk Image at %08lx ... ", addr); /* Copy header so we can blank CRC field for re-calculation */ #ifdef CONFIG_HAS_DATAFLASH if (addr_dataflash (addr)) { read_dataflash (addr, sizeof (image_header_t), (char *) &header); } else #endif memcpy (&header, (char *) addr, sizeof (image_header_t)); if (ntohl (hdr->ih_magic) != IH_MAGIC) { printf ("Bad Magic Number "); SHOW_BOOT_PROGRESS (-10); do_reset (cmdtp, flag, argc, argv); } data = (ulong) & header; len = sizeof (image_header_t); checksum = ntohl (hdr->ih_hcrc); hdr->ih_hcrc = 0; if (crc32 (0, (unsigned char *) data, len) != checksum) { printf ("Bad Header Checksum "); SHOW_BOOT_PROGRESS (-11); do_reset (cmdtp, flag, argc, argv); } SHOW_BOOT_PROGRESS (10); print_image_hdr (hdr); data = addr + sizeof (image_header_t); len = ntohl (hdr->ih_size); #ifdef CONFIG_HAS_DATAFLASH if (addr_dataflash (addr)) { read_dataflash (data, len, (char *) CFG_LOAD_ADDR); data = CFG_LOAD_ADDR; } #endif if (verify) { ulong csum = 0; printf (" Verifying Checksum ... "); csum = crc32 (0, (unsigned char *) data, len); if (csum != ntohl (hdr->ih_dcrc)) { printf ("Bad Data CRC "); SHOW_BOOT_PROGRESS (-12); do_reset (cmdtp, flag, argc, argv); } printf ("OK "); } SHOW_BOOT_PROGRESS (11); if ((hdr->ih_os != IH_OS_LINUX) || (hdr->ih_arch != IH_CPU_ARM) || (hdr->ih_type != IH_TYPE_RAMDISK)) { printf ("No Linux ARM Ramdisk Image "); SHOW_BOOT_PROGRESS (-13); do_reset (cmdtp, flag, argc, argv); } #if defined(CONFIG_B2) || defined(CONFIG_EVB4510) || defined(CONFIG_ARMADILLO) /* *we need to copy the ramdisk to SRAM to let Linux boot */ memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len); data = ntohl(hdr->ih_load); #endif /* CONFIG_B2 || CONFIG_EVB4510 */ /* * Now check if we have a multifile image */ } else if ((hdr->ih_type == IH_TYPE_MULTI) && (len_ptr[1])) { ulong tail = ntohl (len_ptr[0]) % 4; int i; SHOW_BOOT_PROGRESS (13); /* skip kernel length and terminator */ data = (ulong) (&len_ptr[2]); /* skip any additional image length fields */ for (i = 1; len_ptr[i]; ++i) data += 4; /* add kernel length, and align */ data += ntohl (len_ptr[0]); if (tail) { data += 4 - tail; } len = ntohl (len_ptr[1]); } else { /* * no initrd image */ SHOW_BOOT_PROGRESS (14); len = data = 0; } #ifdef DEBUG if (!data) { printf ("No initrd "); } #endif
12、给内核传递参数
#if defined (CONFIG_SETUP_MEMORY_TAGS) || defined (CONFIG_CMDLINE_TAG) || defined (CONFIG_INITRD_TAG) || defined (CONFIG_SERIAL_TAG) || defined (CONFIG_REVISION_TAG) || defined (CONFIG_LCD) || defined (CONFIG_VFD) setup_start_tag (bd); #ifdef CONFIG_SERIAL_TAG setup_serial_tag (¶ms); #endif #ifdef CONFIG_REVISION_TAG setup_revision_tag (¶ms); #endif #ifdef CONFIG_SETUP_MEMORY_TAGS setup_memory_tags (bd); #endif #ifdef CONFIG_CMDLINE_TAG setup_commandline_tag (bd, commandline); #endif #ifdef CONFIG_INITRD_TAG if (initrd_start && initrd_end) setup_initrd_tag (bd, initrd_start, initrd_end); #endif #if defined (CONFIG_VFD) || defined (CONFIG_LCD) setup_videolfb_tag ((gd_t *) gd); #endif setup_end_tag (bd); #endif
比较重要的函数有:
setup_start_tag (bd);
setup_memory_tags (bd);
setup_commandline_tag (bd, commandline);
setup_end_tag (bd);
其中 bd->bi_boot_params(参考uboot全局变量),bi_boot_params=>>0x30000100,启动参数存放的位置。
13、启动内核
printf (" Starting kernel ... "); theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
把机器码以及启动参数存放的位置都告诉给内核。
五、启动过程展示
1、不需要重定位启动
2、重定位启动
下例中读取到的位置,不是合适的位置,内核的入口不是0x30008000,所以还要对内核进行重定位,也就是将内核搬移到指定的位置。
六、u-boot启动zImage
1、直接启动zImage
既然,zImage是uImage去除头部的部分,那么可以从0x30008000直接启动zImage,我们用go命令去执行。
可见,内核的第一个函数果然是解压函数。但是程序卡到图片最后的位置,不能继续执行。
原因是由于没有给内核传递启动参数,也就是说在执行函数theKernel之前,没有做好准备
void (*theKernel)(int zero, int arch, uint params);
2、移植u-boot支持启动zImage
具体代码可看TQ2440开发板的u-boot代码。
再来看一下启动大纲:
1、从NandFlash中读取内核到RAM中 2、在RAM中,给内核进行重定位 3、给内核传递参数 4、启动内核
可以直接从nandflash中将内核zImage读取到内存0x30008000位置处,然后在0x30000100位置处传递参数
也就是调用函数
setup_start_tag (bd);
setup_memory_tags (bd);
setup_commandline_tag (bd, commandline);
setup_end_tag (bd);
最后,调用theKernel函数启动内核。
参考资料:韦东山u-boot启动内核视频