uboot向kernel的传参机制——bootm与tags
http://blog.csdn.net/skyflying2012/article/details/35787971
最近阅读代码学习了uboot boot kernel的过程以及uboot如何传参给kernel,记录下来,与大家共享:
U-boot版本:2014.4
Kernel版本:3.4.55
一 uboot 如何启动 kernel
1 do_bootm
uboot下使用bootm命令启动内核镜像文件uImage,uImage是在zImage头添加了64字节的镜像信息供uboot解析使用,具体这64字节头的内容,我们在分析bootm命令的时候就会一一说到,那直接来看bootm命令。
在common/cmd_bootm.c中
数组boot_os是bootm最后阶段启动kernel时调用的函数数组,CONFIG_NEEDS_MANUAL_RELOC中的代码含义是将boot_os函数都进行偏移(uboot启动中会将整个code拷贝到靠近sdram顶端的位置执行),
但是boot_os函数在uboot relocate时已经都拷贝了,所以感觉没必要在进行relocate。这个宏因此没有定义,直接走下面。
新版uboot对于boot kernel实现了一个类似状态机的机制,将整个过程分成很多个阶段,uboot将每个阶段称为subcommand,
核心函数是do_bootm_states,需要执行哪个阶段,就在do_bootm_states最后一个参数添加那个宏定义,如: BOOTM_STATE_START
do_bootm_subcommand是按照bootm参数来指定运行某一个阶段,也就是某一个subcommand
对于正常的uImage,bootm加tftp的load地址就可以。
2 do_bootm_states
这样会走到最后函数do_bootm_states,那就来看看核心函数do_bootm_states
参数中需要注意bootm_headers_t *images,这个参数用来存储由image头64字节获取到的的基本信息。由do_bootm传来的该参数是images,是一个全局的静态变量。
首先将states存储在images的state中,因为states中有BOOTM_STATE_START,调用bootm_start.
3 第一阶段:bootm_start
获取verify,bootstage_mark_name标志当前状态为bootm start(bootstage_mark_name可以用于无串口调试,在其中实现LED控制)。
boot_start_lmb暂时还没弄明白,以后再搞清楚。
最后修改images.state为bootm start。
bootm_start主要工作是清空images,标志当前状态为bootm start。
4 第二阶段:bootm_find_os
由bootm_start返回后,do_bootm传了BOOTM_STATE_FINDOS,所以进入函数bootm_find_os
调用boot_get_kernel,函数较长,首先是获取image的load地址,如果bootm有参数,就是img_addr,之后如下:
首先标志当前状态,然后调用genimg_get_image,该函数会检查当前的img_addr是否在sdram中,如果是在flash中,则拷贝到sdram中CONFIG_SYS_LOAD_ADDR处,修改img_addr为该地址。
这里说明我们的image可以在flash中用bootm直接起
map_sysmem为空函数,buf即为img_addr。
首先来说明一下image header的格式,在代码中由image_header_t代表,如下:
genimg_get_format检查img header的头4个字节,代表image的类型,有2种,legacy和FIT,这里使用的legacy,头4个字节为0x27051956。
image_get_kernel则会来计算header的crc是否正确,然后获取image的type,根据type来获取os的len和data起始地址。
最后将hdr的数据拷贝到images的legacy_hdr_os_copy,防止kernel image在解压是覆盖掉hdr数据,保存hdr指针到legacy_hdr_os中,置位legacy_hdr_valid。
从boot_get_kernel中返回到bootm_find_os,继续往下:
根据hdr获取os的type,comp,os,end,load addr。
获取os的start。
到这里bootm_find_os就结束了,主要工作是根据image的hdr来做crc,获取一些基本的os信息到images结构体中。
回到do_bootm_states中接下来调用bootm_find_other,
5 第三阶段:bootm_find_other
该函数大体看一下,对于legacy类型的image,获取查询是否有ramdisk,此处我们没有用单独的ramdisk,ramdisk是直接编译到kernel image中的。
回到do_bootm_states中接下来会调用bootm_load_os。
6 第四阶段:bootm_load_os
static int bootm_load_os(bootm_headers_t *images, unsigned long *load_end, int boot_progress) { image_info_t os = images->os; uint8_t comp = os.comp; ulong load = os.load; ulong blob_start = os.start; ulong blob_end = os.end; ulong image_start = os.image_start; ulong image_len = os.image_len; __maybe_unused uint unc_len = CONFIG_SYS_BOOTM_LEN; int no_overlap = 0; void *load_buf, *image_buf; #if defined(CONFIG_LZMA) || defined(CONFIG_LZO) int ret; #endif /* defined(CONFIG_LZMA) || defined(CONFIG_LZO) */ const char *type_name = genimg_get_type_name(os.type); load_buf = map_sysmem(load, unc_len); image_buf = map_sysmem(image_start, image_len); switch (comp) { case IH_COMP_NONE: if (load == blob_start || load == image_start) { printf(" XIP %s ... ", type_name); no_overlap = 1; } else { printf(" Loading %s ... ", type_name); memmove_wd(load_buf, image_buf, image_len, CHUNKSZ); } *load_end = load + image_len; break; #ifdef CONFIG_GZIP case IH_COMP_GZIP: printf(" Uncompressing %s ... ", type_name); if (gunzip(load_buf, unc_len, image_buf, &image_len) != 0) { puts("GUNZIP: uncompress, out-of-mem or overwrite " "error - must RESET board to recover\n"); if (boot_progress) bootstage_error(BOOTSTAGE_ID_DECOMP_IMAGE); return BOOTM_ERR_RESET; } *load_end = load + image_len; break; #endif /* CONFIG_GZIP */
load_buf是之前find_os是根据hdr获取的load addr,image_buf是find_os获取的image的开始地址(去掉64字节头)。
之后则是根据hdr的comp类型来解压拷贝image到load addr上。
这里就需要注意,kernel选项的压缩格式必须在uboot下打开相应的解压缩支持,或者就不进行压缩
这里还有一点,load addr与image add是否可以重叠,看代码感觉是可以重叠的,还需要实际测试一下。
回到do_bootm_states,接下来根据os从boot_os数组中获取到了相应的os boot func,这里是linux,则是do_bootm_linux。后面代码如下:
这时do_bootm最后的代码,如果正常,boot kernel之后就不应该回来了。states中定义了BOOTM_STATE_OS_PREP(对于mips处理器会使用BOOTM_STATE_OS_CMDLINE),调用do_bootm_linux,如下:
do_bootm_linux实现跟do_bootm类似,也是根据flag分阶段运行subcommand,这里会调到boot_prep_linux。
7 第五阶段:boot_prep_linux
该函数作用是为启动后的kernel准备参数,这个函数我们在第三部分uboot如何传参给kernel再仔细分析一下
boot_prep_linux完成返回到do_bootm_states后接下来就是最后一步了。执行boot_selected_os调用do_bootm_linux,flag为BOOTM_STATE_OS_GO,则调用boot_jump_linux
8 第六阶段:boot_jump_linux
boot_jump_linux主体函数如上
获取gd->bd->bi_arch_number为machid,如果有env则用env的machid,kernel_entry为之前由hdr获取的ep,也就是内核的入口地址。
fake为0,直接调用kernel_entry,参数1为0,参数2为machid,参数3为bi_boot_params。
这之后就进入了kernel的执行流程启动,就不会再回到uboot
这整个boot过程中bootm_images_t一直作为对image信息的全局存储结构。
三 uboot如何传参给kernel
uboot下的传参机制就直接来分析boot_prep_linux函数就可以了,如下:
static void boot_prep_linux(bootm_headers_t *images) { char *commandline = getenv("bootargs"); if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len) { #ifdef CONFIG_OF_LIBFDT debug("using: FDT\n"); if (image_setup_linux(images)) { printf("FDT creation failed! hanging..."); hang(); } #endif } else if (BOOTM_ENABLE_TAGS) { debug("using: ATAGS\n"); setup_start_tag(gd->bd); if (BOOTM_ENABLE_SERIAL_TAG) setup_serial_tag(¶ms); if (BOOTM_ENABLE_CMDLINE_TAG) setup_commandline_tag(gd->bd, commandline); if (BOOTM_ENABLE_REVISION_TAG) setup_revision_tag(¶ms); if (BOOTM_ENABLE_MEMORY_TAGS) setup_memory_tags(gd->bd); if (BOOTM_ENABLE_INITRD_TAG) { if (images->rd_start && images->rd_end) { setup_initrd_tag(gd->bd, images->rd_start,images->rd_end); } } setup_board_tags(¶ms); setup_end_tag(gd->bd); } else { printf("FDT and ATAGS support not compiled in - hanging\n"); hang(); } do_nonsec_virt_switch(); }
首先获取出环境变量bootargs,这就是要传递给kernel的参数。
在配置文件中定义了CONFIG_CMDLINE_TAG以及CONFIG_SETUP_MEMORY_TAGS,根据arch/arm/include/asm/bootm.h,则会定义BOOTM_ENABLE_TAGS,首先调用setup_start_tag,如下:
params是一个全局静态变量用来存储要传给kernel的参数,这里bd->bi_boot_params的值赋给params,因此bi_boot_params需要进行初始化,从而将params放在一个合理的内存区域。
这里params为struct tag的结构,如下:
tag包括hdr和各种类型的tag_*,hdr来标志当前的tag是哪种类型的tag。
setup_start_tag是初始化了第一个tag,是tag_core类型的tag。最后调用tag_next跳到第一个tag末尾,为下一个tag做准备。
回到boot_prep_linux,接下来调用setup_commandline_tag,如下:
该函数设置第二个tag的hdr.tag为ATAG_CMDLINE,然后拷贝cmdline到tags的cmdline结构体中,跳到下一个tag。
回到boot_prep_linux,调用setup_memory_tag,如下:
static void setup_memory_tags(bd_t *bd) { int i; for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) { params->hdr.tag = ATAG_MEM; params->hdr.size = tag_size (tag_mem32); params->u.mem.start = bd->bi_dram[i].start; params->u.mem.size = bd->bi_dram[i].size; params = tag_next (params); } }
过程类似,将第三个tag设为ATAG_MEM,将mem的start,size保存在此处,如果有多片ram(CONFIG_NR_DRAM_BANKS > 1),则将下一个tag保存下一片ram的信息,依次类推。
回到boot_prep_linux中,调用setup_board_tags,这个函数是__weak属性,我们可以在自己的板级文件中去实现来保存跟板子相关的参数,如果没有实现,则是空函数。
最后调用setup_end_tags,如下:
最后将最末尾的tag设置为ATAG_NONE,标志tag结束。
这样整个参数的准备就结束了,最后在调用boot_jump_linux时会将tags的首地址也就是bi_boot_params传给kernel,供kernel来解析这些tag,kernel如何解析看第四部分kenrel如何找到并解析参数
总结一下,uboot将参数以tag数组的形式布局在内存的某一个地址,每个tag代表一种类型的参数,首尾tag标志开始和结束,首地址传给kernel供其解析。
四 kernel如何找到并解析参数
uboot在调用boot_jump_linux时最后kernel_entry(0, machid, r2);
按照二进制规范eabi,machid存在寄存器r1,r2即tag的首地址存在寄存器r2.
查看kernel的入口函数,在arch/arm/kernel/head.S,中可以看到如下一段汇编:
可以看出kernel刚启动会调用__vet_atags来处理uboot传来的参数,如下:
主要是对tag进行了一个简单的校验,查看tag头4个字节(tag_core的size)和第二个4字节(tag_core的type)。
之后对参数的真正分析处理是在start_kernel的setup_arch中,在arch/arm/kernel/setup.c中,如下:
关键函数是setup_machine_tags,如下:
首先回去获取tags的首地址,如果收个tag是ATAG_CORE类型,则会调用save_atags拷贝一份tags,最后调用parse_tags来分析这个tag list,如下:
遍历tags list,找到在tagstable中匹配的处理函数(hdr.tag一致),来处理响应的tag。
这个tagtable的处理函数是在调用__tagtable来注册的,如下:
看这个对cmdline类型的tag的处理,就是将tag中的cmdline拷贝到default_command_line中。还有其他如mem类型的参数也会注册这个处理函数,来匹配处理响应的tag。这里就先以cmdline的tag为例。
这样遍历并处理完tags list之后回到setup_machine_tags,将from(即default_command_line)中的cmdline拷贝到boot_command_line,
最后返回到setup_arch中,
将boot_command_line拷贝到start_kernel给setup_arch的cmdline_p中,这里中间拷贝的boot_command_line是给parse_early_param来做一个早期的参数分析的。
到这里kernel就完全接收并分析完成了uboot传过来的args。
简单的讲,uboot利用函数指针及传参规范,它将
l R0: 0x0
l R1: 机器号
l R2: 参数地址
三个参数传递给内核。
其中,R2寄存器传递的是一个指针,这个指针指向一个TAG区域。
UBOOT和Linux内核之间正是通过这个扩展了的TAG区域来进行复杂参数的传递,如 command line,文件系统信息等等,用户也可以扩展这个TAG来进行更多参数的传递。TAG区域的首地址,正是R2的值。