u-boot kernel启动
目前uboot已经可以在开发板上成功使用了,下一步,就是最关键的一步了——加载Linux内核到DDR上并启动内核。
一、加载内核并启动
uboot加载内核的方法一般分为两种。
1.SD卡加载
如果kernel在SD卡中,那么需要使用movi read kernel 30008000
将内核加载到DDR上0x30008000
的位置。
uboot如何知道kernel在SD卡的那个扇区呢?根据iNand分区表得到的。
2.tftp加载
确保网络联通后,可以在虚拟机下构建tftp客户端实现烧录,加载时采用命令tftp 0x30008000 zImage-qt
就可以完成内核到DDR的加载。
3.启动
可以使用bootm 0x30008000
启动内核。
二、uImage与zImage
比较u-boot
以及u-boot.bin
:
可以看到直接连接编译得到的
u-boot
文件为ELF格式,是Linux下的可执行文件。值得注意的是该文件属于not stripped
状态。该状态说明这个程序没有剔除符号表信息。用objcopy
剔除之后生成文件u-boot.bin
,可以烧录。剔除符号表信息前,文件有956KB,而剔除后,仅剩下384K。很明显,小体积的文件更适合烧录。
Linux内核经过编译也会生成elf可执行程序,一般叫做
vmlinux
或vmlinuz
。但是这个内核文件太大了,经过strip
之后,就产生了Image文件。其实这个Image文件已经可以被烧录执行了。但是为了进一步压缩成本,所以Linux参与者再对这个Image进行了压缩,并且在压缩后的文件前端,加入了一部分解压缩代码。构成了压缩格式的zImage。uboot为了启动内核,就又发明了一种内核格式——uImage。加工的过程其实就是用uboot中的mkimage工具再zImage前加上64Bytes的头信息即可。
在kernel目录下,执行make x210ii_qt_defconfig
即可完成内核编译第一步配置,接下来使用make menuconfig
进行内核模块化删改,最后make,即可得到zImage。
三、do_bootm
分析
do_bootm
是执行bootm
的主要函数。
关键结构体image_header_t
与bootm_headers_t
image_header_t
/*
* Legacy format image header,
* all data in network byte order (aka natural aka bigendian).
*/
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;
bootm_headers_t
typedef struct bootm_headers {
/*
* Legacy os image header, if it is a multi component image
* then boot_get_ramdisk() and get_fdt() will attempt to get
* data from second and third component accordingly.
*/
image_header_t *legacy_hdr_os; /* image header pointer */
image_header_t legacy_hdr_os_copy; /* header copy */
ulong legacy_hdr_valid;
int verify; /* getenv("verify")[0] != 'n' */
struct lmb *lmb; /* for memory mgmt */
} bootm_headers_t;
流程图
从这个流程图中可以看出,do_bootm
函数至少经历了三次大改。第一次是初始的情况下,使用的是uImage
。后来因为内核的更新,用条件编译在判断uImage
原本的函数里加入设备树的相关配置。最后一次,则是直接独立出来判断zImage
,用goto语句跳转。
所以目前uboot支持三种镜像加载方式,分别为zImage
,uImage
,设备树
。
zImage
if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) {
printf("Boot with zImage\n");
addr = virt_to_phys(addr);
hdr = (image_header_t *)addr;
hdr->ih_os = IH_OS_LINUX;
hdr->ih_ep = ntohl(addr);
memmove (&images.legacy_hdr_os_copy, hdr, sizeof(image_header_t));
/* save pointer to image header */
images.legacy_hdr_os = hdr;
images.legacy_hdr_valid = 1;
uImage
-
boot_get_kernel
检查输入参数,然后获取image的format type,并打印信息。经过image_check_magic
,检测37到40字节的数是否是0x27051956
,打印出检测到的Image格式,1
代表uImage。
因为是uImage格式,所以输出一些信息。
接着使用image_get_type
获得OS相关信息。
image_get_kernel
,先打印image的信息,然后确认内核的crc校验。并输出。
设置bootm_headers
结构体中的legacy_hdr_os
与legacy_hdr_valid
。
-
genimg_get_format
判断image的格式,然后获取OS相关信息。例如image的类型,压缩方式,OS名称,image_start,image_end,image_size,load_start,load_end。
-
判断压缩格式
判断未使用压缩,所以做相应输出。
-
检查加载地址与Image地址
if ((load_start < image_end) && (load_end > image_start)) {
debug ("image_start = 0x%lX, image_end = 0x%lx\n", image_start, image_end);
debug ("load_start = 0x%lx, load_end = 0x%lx\n", load_start, load_end);
if (images.legacy_hdr_valid) {
if (image_get_type (&images.legacy_hdr_os_copy) == IH_TYPE_MULTI)
puts ("WARNING: legacy format multi component "
"image overwritten\n");
} else {
puts ("ERROR: new format image overwritten - "
"must RESET the board to recover\n");
show_boot_progress (-113);
do_reset (cmdtp, flag, argc, argv);
}
}
四、do_bootm_linux
分析
do_bootm
函数最终执行do_bootm_linux
来完成内核加载的最后一步。
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;
struct tag_mtdpart mtdpart_info;
} u;
};
tag_header中有这个tag的size和类型编码,kernel拿到一个tag后先分析tag_header得到tag的类型和大小,然后将tag中剩余部分当作一个tag_xxx来处理。
内核传参是依靠上述结构体实现的。传参时,实际为一个结构体序列,以tag_start
开始,以tag_end
结束
传参宏
#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);
#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
#ifdef CONFIG_MTDPARTITION
setup_mtdpartition_tag();
#endif
setup_end_tag (bd);
#endif
CONFIG_SETUP_MEMORY_TAGS,tag_mem,传参内容是内存配置信息。
CONFIG_CMDLINE_TAG,tag_cmdline,传参内容是启动命令行参数,也就是uboot环境变量的bootargs.
( CONFIG_INITRD_TAG
CONFIG_MTDPARTITION,传参内容是iNand/SD卡的分区表。
起始tag是ATAG_CORE、结束tag是ATAG_NONE,其他的ATAG_XXX都是有效信息tag。
如何传参
先打印Starting kernel ...
,然后开始准备传参。
// 函数指针
void (*theKernel)(int zero, int arch, uint params);
// entry_point
theKernel = (void (*)(int, int, uint))ep;
// 开始执行内核,u-boot结束
theKernel (0, machid, bd->bi_boot_params);
/* does not return */
return;
uboot最终是调用theKernel函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了3个参数。这3个参数就是uboot直接传递给linux内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。