uboot启动内核过程
1. uboot与内核的关系
uboot其实全名叫Universal BootLoader(通用引导装载程序),是专门用来启动内核的引导代码。每个要启动内核的CPU必须先执行uboot代码初始化运行环境,然后将内核代码从启动介质(比如iNand)搬移到内核链接指定地址处,最后跳转执行内核完成启动内核的工作。uboot与内核起初都被烧录到启动介质指定扇区处,而烧录的文件(镜像)分别为u-boot.bin与zImage,在电源打开时,CPU会自动取走uboot到SRAM中,然后按照上述的步骤直到完成自己的使命。除了上面的两个镜像外,一般还有个rootfs。
2. 内核的各个版本
u-boot.bin镜像的前身其实是可执行程序u-boot(elf格式),u-boot通过arm-linux-objcopy工具修改为u-boot.bin。同样的,zImage最原先是vmlinux或vmlinuz文件,格式也是elf,这个文件78M,而且它不能被CPU直接执行,必须通过arm-linux-objcopy工具修改为Image,这个文件7.5M,但当时启动介质一般是软盘,大小为1.2M和1.44MB两种,为了能够节省软盘,开源社区将Image镜像压缩后,并在镜像前加了一段解压缩代码,构成了zImage镜像。然后,uboot开源社区为了启动内核,还发明了另外一种镜像uImage (zImage前面加上64字节的uImage的头信息) ,这个镜像是由zImage通过make uImage
命令获得的,其中用到了一个文件mkimage ,这个文件可以在uboot的BSP中找到。此外究竟是使用zImage还是uImage(这两个都可以被uboot启动,但有些uboot不支持zImage,相反,所有uboot都支持uImage),可以通过x210_sd.h 中是否定义了LINUX_ZIMAGE_MAGIC这个宏来决定。
3. do_bootm函数(…/uboot/common/Cmd_bootm.c)
在uboot启动内核时会调用一个命令,这个命令就是bootm,它是由do_bootm函数实现的(一般所有命令的实现函数名都是do_xx)。这个函数包含了两种启动方式,一种是无头校验的zImage(实际上zImage不是官方的本意,大部分uboot还是uImage方式启动,只不过有的公司为了启动方便,在这个函数中强行增加了无头校验的代码,并使用goto语句跳过了头校验部分),另一种是有头校验的uImage,此外,在内核3.x版本又新增了设备树启动方式,这个可以通过定义宏CONFIG_FIT 来实现。
3.1 zImage的一些启动细节
要使用zImage方式,必须定义CONFIG_ZIMAGE_BOOT这个宏,在X210_sd.h 中可以定义。do_bootm函数的第197到224行是zImage的特殊启动部分,在最开始先定义了一个魔数#define LINUX_ZIMAGE_MAGIC 0x016f2818
,这个数用来与zImage镜像的第37-40个字节进行对比,若是就执行zImage方式启动,否则执行uImage方式启动。而且,zImage支持无参数启动,内核地址选用默认地址MEMORY_BASE_ADDRESS(0x30000000)。另外 images 这个全局变量保存了zImage启动的头信息,以及镜像起始地址,检验成功标志位。在这部分代码中,会修改头信息比如os(操作系统类型),ep(运行地址),并会设置检验标志位为1,但这种方式坏处是在移植uboot的时候需要查看是否有必要修改这部分代码。在这部分中会打印Boot with zImage的启动信息。
3.2 uImage的一些启动细节
区别于zImage启动方式,uImage启动显然要更加繁琐些,这是因为uboot需要真正的校验了uImage头,除了魔数#define IH_MAGIC 0x27051956
的校验,还有镜像头和数据循环冗余码校验,以及校验是否是支持的架构,这些都在image_get_kernel 这个函数中完成。上面的函数是在boot_get_kernel 函数中调用,这个函数除了校验外,还对启动方式进行了甄别,获取了镜像地址,镜像头,长度和镜像实际地址。此外,uImage还提供了几种解压缩算法用于解压缩镜像。而且,在3.x版本uImage增加了FIT设备树的内容。
3.3 do_bootm_linux函数
在执行完zImage或uImage的独立代码后,do_bootm 会对系统类别进行甄别选择一种符合镜像的启动方式启动,比如linux内核会执行do_bootm_linux 函数。do_bootm_linux 函数会通过找到 images 变量找到内核实际运行开始地址ep,然后将ep强制类型转换为函数指针theKernel theKernel = (void (*)(int, int, uint))ep;
。而且这个函数会获取机器码的环境变量值,打印出来并最后传参给内核。在检查好磁盘内容后,清除cache内容,执行theKernel (0, machid, bd->bi_boot_params);
代码,启动内核,并打印uboot的最后一句话“\nStarting kernel …\n\n”。
3.4 theKernel函数的传参
在介绍 theKernel 函数的传参的传参之前,先引入一个特殊的数据结构tag:
struct tag_header {
u32 size;
u32 tag;
};
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
* Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
struct tag_mtdpart mtdpart_info;//iNand/SD卡的分区表
} u;
};
正如代码所示,uboot传的每一个参数都是这样一个数据结构,他有一个头,里面放着当前这个参数的类型以及下一个参数的位置(当前的地址加当前数据结构的大小就是下一个参数的位置)。它还有一个专属的参数数据结构,比如有CPU的,内存的,还有命令行(用于自启动,也就是uboot环境变量的bootargs)的等等,这些必须定义各自的宏才能添加到参数列表中,代码如下:
#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) || \
defined (CONFIG_MTDPARTITION)
setup_start_tag(bd); //core
#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
#ifdef CONFIG_MTDPARTITION
setup_mtdpartition_tag();
#endif
setup_end_tag(bd);
#endif
在这,我们用一个表格更形象的表示一下具体的参数结构:
position | content |
---|---|
start tag | ATAG_CORE |
size (tag_core) | |
core.flags | |
core.pagesize | |
core.rootdev | |
memory1 tag | ATAG_MEM |
size (tag_mem32) | |
bd->bi_dram[i].start | |
bd->bi_dram[i].size | |
memory2 tag | ATAG_MEM |
size (tag_mem32) | |
bd->bi_dram[i].start | |
bd->bi_dram[i].size | |
… … … … | … … … … |
end tag | ATAG_NONE |
0 |
因此只要bd->bi_boot_params指针指向最开始tag的地址,然后传给内核,内核就能一个个的把参数读出来,另外说明一点,这儿的传参是通过寄存器r0,r1,r2传递的。
4. uboot 启动内核方法
- 使用movi命令将内核搬移到启动位置,使用bootm命令启动内核
- 设置bootargs环境变量值,开机自启动
- 使用tftp下载内核到指定位置,使用bootm命令启动