uboot启动kernel使用命令过程分析

本文从代码角度,分析uboot如何使用环境变量以及加载命令,启动kernel的。

本文以armV8 CPU为例子(例如RK1808或者7ev),其他架构类型基本一样。

1. uboot启动流程简介

        为了让读者知道本文介绍的知识点在uboot所在位置,在介绍之前,先简单回顾下uboot的启动流程。

        从uboot链接脚本(u-boot\arch\arm\cpu\armv8\u-boot.lds)可知,uboot的启动入口符号 _start:

.......
ENTRY(_start)
SECTIONS
{
#ifdef CONFIG_ARMV8_SECURE_BASE
	/DISCARD/ : { *(.rela._secure*) }
#endif
	. = 0x00000000;

	. = ALIGN(8);
	.text :
	{
		*(.__image_copy_start)
		CPUDIR/start.o (.text*)
		*(.text*)
	}
........

而_start符号定义在 u-boot\arch\arm\cpu\armv8\start.S 文件里:


.globl	_start
_start:
#ifdef CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK
/*
 * Various SoCs need something special and SoC-specific up front in
 * order to boot, allow them to set that in their boot0.h file and then
 * use it here.
 */
#include <asm/arch/boot0.h>
#else
	b	reset
#endif

start.S主要对CPU环境初始化,最后跳转到_main符号地址,_main定义在u-boot\arch\arm\lib\crt0_64.S:

ENTRY(_main)

分析该文件,做了一些列初始化,包括为C环境做的准备等,主要跳转了两个函数:board_init_f 和 board_init_r。

其中board_init_f定义在uboot/common/board_f.c里,该函数主要执行了init_sequence_f定义的函数结构体,至于具体做了什么,留着专题分析,本文重点不在这里。

void board_init_f(ulong boot_flags)
{
	gd->flags = boot_flags;
	gd->have_console = 0;

	if (initcall_run_list(init_sequence_f))
		hang();

#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
		!defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64)
	/* NOTREACHED - jump_to_copy() does not return */
	hang();
#endif
}

crt0_64.S最后跳转至board_init_r函数,该函数定义在u-boot\common\board_r.c,该函数也是执行了init_sequence_r定义的函数结构体,而init_sequence_r(u-boot\common\board_r.c里定义)定义的函数集里,最后执行的函数是run_main_loop,查看run_main_loop(u-boot\common\board_r.c里定义)函数定义知道最后跳转到main_loop。


void board_init_r(gd_t *new_gd, ulong dest_addr)
{
	/*
	 * Set up the new global data pointer. So far only x86 does this
	 * here.
	 * TODO(sjg@chromium.org): Consider doing this for all archs, or
	 * dropping the new_gd parameter.
	 */
.............

	if (initcall_run_list(init_sequence_r))
		hang();

	/* NOTREACHED - run_main_loop() does not return */
	hang();
}

static int run_main_loop(void)
{
.....
	for (;;)
		main_loop();
	return 0;
}

main_loop定义在u-boot\common\main.c,main_loop是uboot最后进入的一个函数,主要执行如下动作:初始化跳转kernel前环境,响应看客户按键,读取环境变量并执行命令跳转至kernel,跳转失败后续处理等。

本文主要分析uboot如何利用环境变量执行命令跳转至kernel,主要在main_loop函数里。

2. uboot使用环境变量跳转kernel

        在main_loop里,可以看到uboot在执行了bootdelay_process(u-boot\common\autoboot.c)函数,读取包括bootdelay、bootcmd等环境变量值,其中bootcmd是启动kernel的命令:

const char *bootdelay_process(void)
{
	char *s;
	int bootdelay;
........
	s = env_get("bootdelay");
	bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;
.......
	debug("### main_loop entered: bootdelay=%d\n\n", bootdelay);  
.......
	s = env_get("bootcmd");

	process_fdt_options(gd->fdt_blob);
	stored_bootdelay = bootdelay;

	return s;
}

在读取到bootcmd命令内容后,main_loop又执行了autoboot_command(u-boot\common\autoboot.c)函数,来执行bootcmd的命令内容。所以接下来看下bootcmd内容从来哪里来,内容是什么,以及过程中执行了哪些命令才能加载kernel并跳转的。

3. bootcmd的命令内容分析

        bootcmd命令定义在u-boot\include\env_default.h文件,放到default_environment这个大的结构体,该结构体会在uboot启动的时候,根据内容被加载成可识别环境变量内容。从中可可看到bootcmd内容来自CONFIG_BOOTCOMMAND,说明用户可自定义bootcmd的内容。

env_t environment __UBOOT_ENV_SECTION__ = {
.......
	{
#elif defined(DEFAULT_ENV_INSTANCE_STATIC)
static char default_environment[] = {
#else
const uchar default_environment[] = {
#endif
......
#ifdef	CONFIG_USE_BOOTARGS
	"bootargs="	CONFIG_BOOTARGS			"\0"
#endif
#ifdef	CONFIG_BOOTCOMMAND
	"bootcmd="	CONFIG_BOOTCOMMAND		"\0"
#endif
........
	}
#endif
};

前面说CONFIG_BOOTCOMMAND是用户自定义的,接下来看下RK1808是如何使用的,搜索发现CONFIG_BOOTCOMMAND宏的内容来自RKIMG_BOOTCOMMAND,CONFIG_RKIMG_BOOTCMD(u-boot\include\configs\rockchip-common.h)定义如下:


#define RKIMG_BOOTCOMMAND			\
	"boot_android ${devtype} ${devnum};"	\
	"bootrkp;"				\
	"run distro_bootcmd;"

前面boot_android和boootrkp是rk自定义的命令,暂时不分析,主要分析run distro_bootcmd。

4. distro_bootcmd 命令内容

        distro_bootcmd定义在u-boot\include\config_distro_bootcmd.h里(这节如果没有说明源码路径,则都是在该路径)

#define BOOTENV \
	.........
	"distro_bootcmd=" BOOTENV_SET_SCSI_NEED_INIT                      \
		"for target in ${boot_targets}; do "                      \
			"run bootcmd_${target}; "                         \
		"done\0"

从定义来看, distro_bootcmd是循环读取boot_targets内容和bootcmd_组成命令。boot_targets定义如下:

#define BOOTENV_DEV_NAME(devtypeu, devtypel, instance) \
	BOOTENV_DEV_NAME_##devtypeu(devtypeu, devtypel, instance)
#define BOOTENV_BOOT_TARGETS \
	"boot_targets=" BOOT_TARGET_DEVICES(BOOTENV_DEV_NAME) "\0"

先来看BOOT_TARGET_DEVICES(u-boot\include\configs\rockchip-common.h)在板级的定义:

/* First try to boot from SD (index 1), then eMMC (index 0) */
#if CONFIG_IS_ENABLED(CMD_MMC)
	#define BOOT_TARGET_MMC(func) \
		func(MMC, mmc, 1) \
		func(MMC, mmc, 0)
#else
	#define BOOT_TARGET_MMC(func)
#endif

........

#define BOOT_TARGET_DEVICES(func) \
	BOOT_TARGET_MMC(func) \
	BOOT_TARGET_RKNAND(func) \
	BOOT_TARGET_USB(func) \
	BOOT_TARGET_PXE(func) \
	BOOT_TARGET_DHCP(func)

这里只分析从MMC启动,将BOOT_TARGET_MMC宏代入BOOT_TARGET_DEVICES可得如下:

#define BOOT_TARGET_DEVICES(func) \
    func(MMC, mmc, 1) \
    func(MMC, mmc, 0)  \
    func(RKNAND, rknand, 0)
    ...

代入BOOTENV_BOOT_TARGETS得到BOOTENV_BOOT_TARGETS的定义如下:

#define BOOTENV_BOOT_TARGETS \
	"boot_targets=BOOTENV_DEV_NAME(MMC, mmc, 1) \
        BOOTENV_DEV_NAME(MMC, mmc, 0)  \
        BOOTENV_DEV_NAME(RKNAND, rknand, 0)" "\0"

将BOOTENV_DEV_NAME换掉后:

#define BOOTENV_BOOT_TARGETS \
	"boot_targets=BOOTENV_DEV_NAME_MMC(MMC, mmc, 1) \
        BOOTENV_DEV_NAME_MMC(MMC, mmc, 0)  \
        BOOTENV_DEV_NAME_RKNAND(RKNAND, rknand, 0)" "\0"

BOOTENV_DEV_NAME_MMC以及后续宏定义如下:

#define BOOTENV_DEV_NAME_BLKDEV(devtypeu, devtypel, instance) \
	#devtypel #instance " "

#define BOOTENV_DEV_NAME_MMC	BOOTENV_DEV_NAME_BLKDEV

替换后可得最后的定义:

#define BOOTENV_BOOT_TARGETS \
	"boot_targets=" mmc1 mmc0 rknand... "\0"

回顾distro_bootcmd的定义,即distro_bootcmd实际上是bootcmd_mmc1、bootcmd_mmc0等命令

#define BOOTENV \
	.........
	"distro_bootcmd=" BOOTENV_SET_SCSI_NEED_INIT                      \
		"for target in ${boot_targets}; do "                      \
			"run bootcmd_${target}; "                         \
		"done\0"

4. 分析mmc启动的bootcmd_mmc0命令

        以前面分析为基础,接下来只分析mmc的启动,其他启动方式过程一样。

        搜索并没有发现先bootcmd_mmc0的定义 ,其实该定义是在distro_bootcmd前一个环境变量定义了,整体看下BOOTENV的定义如下:

#define BOOTENV \
	BOOTENV_SHARED_HOST \
	BOOTENV_SHARED_MMC \
	BOOTENV_SHARED_PCI \
	BOOTENV_SHARED_USB \
	BOOTENV_SHARED_SATA \
	BOOTENV_SHARED_SCSI \
	BOOTENV_SHARED_IDE \
	BOOTENV_SHARED_UBIFS \
	BOOTENV_SHARED_EFI \
	"boot_prefixes=/ /boot/\0" \
	"boot_scripts=boot.scr.uimg boot.scr\0" \
	"boot_script_dhcp=boot.scr.uimg\0" \
	BOOTENV_BOOT_TARGETS \
	\
	"boot_extlinux="                                                  \
		"sysboot ${devtype} ${devnum}:${distro_bootpart} any "    \
			"${scriptaddr} ${prefix}extlinux/extlinux.conf\0" \
	\
	"scan_dev_for_extlinux="                                          \
		"if test -e ${devtype} "                                  \
				"${devnum}:${distro_bootpart} "           \
				"${prefix}extlinux/extlinux.conf; then "  \
			"echo Found ${prefix}extlinux/extlinux.conf; "    \
			"run boot_extlinux; "                             \
			"echo SCRIPT FAILED: continuing...; "             \
		"fi\0"                                                    \
	\
	"boot_a_script="                                                  \
		"load ${devtype} ${devnum}:${distro_bootpart} "           \
			"${scriptaddr} ${prefix}${script}; "              \
		"source ${scriptaddr}\0"                                  \
	\
	"scan_dev_for_scripts="                                           \
		"for script in ${boot_scripts}; do "                      \
			"if test -e ${devtype} "                          \
					"${devnum}:${distro_bootpart} "   \
					"${prefix}${script}; then "       \
				"echo Found U-Boot script "               \
					"${prefix}${script}; "            \
				"run boot_a_script; "                     \
				"echo SCRIPT FAILED: continuing...; "     \
			"fi; "                                            \
		"done\0"                                                  \
	\
	"scan_dev_for_boot="                                              \
		"echo Scanning ${devtype} "                               \
				"${devnum}:${distro_bootpart}...; "       \
		"for prefix in ${boot_prefixes}; do "                     \
			"run scan_dev_for_extlinux; "                     \
			"run scan_dev_for_scripts; "                      \
		"done;"                                                   \
		SCAN_DEV_FOR_EFI                                          \
		"\0"                                                      \
	\
	"scan_dev_for_boot_part="                                         \
		"part list ${devtype} ${devnum} -bootable devplist; "     \
		"env exists devplist || setenv devplist 1; "              \
		"for distro_bootpart in ${devplist}; do "                 \
			"if fstype ${devtype} "                           \
					"${devnum}:${distro_bootpart} "   \
					"bootfstype; then "               \
				"run scan_dev_for_boot; "                 \
			"fi; "                                            \
		"done\0"                                                  \
	\
	BOOT_TARGET_DEVICES(BOOTENV_DEV)                                  \
	\
	"distro_bootcmd=" BOOTENV_SET_SCSI_NEED_INIT                      \
		"for target in ${boot_targets}; do "                      \
			"run bootcmd_${target}; "                         \
		"done\0"

为了知道bootcmd_mmc0命令的内容,先来分析其中BOOT_TARGET_DEVICES(BOOTENV_DEV)的定义。BOOT_TARGET_DEVICES前面已经分析,代入可知这里是BOOTENV_DEV(MMC, mmc, 0),对于BOOTENV_DEV以及相关设计到的定义都在u-boot\include\config_distro_bootcmd.h。

#define BOOTENV_DEV_BLKDEV(devtypeu, devtypel, instance) \
	"bootcmd_" #devtypel #instance "=" \
		"setenv devnum " #instance "; " \
		"run " #devtypel "_boot\0"

#define BOOTENV_DEV_MMC		BOOTENV_DEV_BLKDEV

#define BOOTENV_DEV(devtypeu, devtypel, instance) \
    BOOTENV_DEV_##devtypeu(devtypeu, devtypel, instance)

全部一条替换,最后得出bootcmd_mmc0的定义:

	bootcmd_mmc0= setenv devnum 0; run  mmc_boot;

再来看下命令mmc_boot的定义,该定义在BOOTENV里的宏BOOTENV_SHARED_MMC,该宏的定义如下:

#define BOOTENV_SHARED_BLKDEV_BODY(devtypel) \
		"if " #devtypel " dev ${devnum}; then " \
			"devtype=" #devtypel "; " \
			"run scan_dev_for_boot_part; " \
		"fi\0"

#define BOOTENV_SHARED_BLKDEV(devtypel) \
	#devtypel "_boot=" \
	BOOTENV_SHARED_BLKDEV_BODY(devtypel)


#define BOOTENV_SHARED_MMC	BOOTENV_SHARED_BLKDEV(mmc)

在这里将BOOTENV_SHARED_BLKDEV替换如下:

#define BOOTENV_SHARED_MMC
    "mmc_boot=" BOOTENV_SHARED_BLKDEV_BODY(mmc)

再将BOOTENV_SHARED_BLKDEV_BODY替换,得到最终的定义如下:

#define BOOTENV_SHARED_MMC "mmc_boot=if mmc dev ${devnum};" "\
                "then devtype=mmc; run scan_dev_for_boot_part" "\
                "fi\0"

这里除了scan_dev_for_boot_part,其他都比较清楚,再来看下scan_dev_for_boot_part的定义,该定义是在宏BOOTENV里直接定义,比较清晰:

"scan_dev_for_boot_part="                                         \
		"part list ${devtype} ${devnum} -bootable devplist; "     \
		"env exists devplist || setenv devplist 1; "              \
		"for distro_bootpart in ${devplist}; do "                 \
			"if fstype ${devtype} "                           \
					"${devnum}:${distro_bootpart} "   \
					"bootfstype; then "               \
				"run scan_dev_for_boot; "                 \
			"fi; "                                            \
		"done; "                                                  \
		"setenv devplist\0"					  \
	\

这里主要意思是找出分区里的启动分区序号,如果没有可启动分区,默认配为1(分区1为启动分区,这里以分区1为启动分区为例子),如果该分区为可启动文件系统类型合法,则调用scan_dev_for_boot。

注:拓充知识点:使用gpt分区时,可为分区添加bootable标识,表示该分区为当前的启动分区,例如下,尝试写一个gpt分区:

=> setenv mbr_parts 'name=boot,start=4M,size=128M,id=0x0e;
name=rootfs1,size=256M,bootable,id=0x83;
name=rootfs2,size=256M,id=0x83;
name=data,size=-,id=0x83'
=> mbr write mmc 0

接下来继续往下定位,看下scan_dev_for_boot(BOOTENV里)定义:

"scan_dev_for_boot="                                              \
		"echo Scanning ${devtype} "                               \
				"${devnum}:${distro_bootpart}...; "       \
		"for prefix in ${boot_prefixes}; do "                     \
			"run scan_dev_for_extlinux; "                     \
			"run scan_dev_for_scripts; "                      \
		"done;"                                                   \
		SCAN_DEV_FOR_EFI                                          \
		"\0"                                                      \
	\

从boot_prefixes(/和 /boot/)读取值到prefix,再运行scan_dev_for_scripts和scan_dev_for_extlinux。

5. scan_dev_for_scripts:boot.scr脚本启动内核

"boot_a_script="                                                  \
		"load ${devtype} ${devnum}:${distro_bootpart} "           \
			"${scriptaddr} ${prefix}${script}; "              \
		"source ${scriptaddr}\0"                                  \
	\
	"scan_dev_for_scripts="                                           \
		"for script in ${boot_scripts}; do "                      \
			"if test -e ${devtype} "                          \
					"${devnum}:${distro_bootpart} "   \
					"${prefix}${script}; then "       \
				"echo Found U-Boot script "               \
					"${prefix}${script}; "            \
				"run boot_a_script; "                     \
				"echo SCRIPT FAILED: continuing...; "     \
			"fi; "                                            \
		"done\0"                                                  \
	\

scan_dev_for_scripts主要从当前的分区里,读取到boot_scripts(boot.scr.uimg boot.scr)定义的文件,当该文件存在时,执行boot_a_script,这里以boot.src文件为例子。boot_a_script展开如下:

boot_a_script = load mmc 0:1 ${scriptaddr}  /boot.scr; source ${scriptaddr}

意思是从mmc 0:1分区信息里读出/boot.src的文件信息到scriptaddr这个地址上,再执行scriptaddr这个地址的内容。其中scriptaddr(include/configs/rk1808_common.h)定义如下:

#define ENV_MEM_LAYOUT_SETTINGS \
	"scriptaddr=0x00500000\0" \
	"pxefile_addr_r=0x00600000\0" \
	"fdt_addr_r=0x01f00000\0" \
	"kernel_addr_no_low_bl32_r=0x00280000\0" \
	"kernel_addr_r=0x00680000\0" \
	"kernel_addr_c=0x04080000\0" \
	"ramdisk_addr_r=0x0a200000\0"

关于boot.scr的介绍及作用可看下 Home · linux-sunxi/u-boot-sunxi Wiki · GitHub

boot.scr的内容以及生成示例:

boot.scr内容:
setenv bootargs console=ttyS0 root=/dev/mmcblk0p1 rootwait panic=10 ${extra}
ext2load mmc 0 0x43000000 boot/script.bin
ext2load mmc 0 0x48000000 boot/uImage
bootm 0x48000000

生成方式:
mkimage -C none -A arm -T script -d boot.cmd boot.scr

以上分析其实是在帮助读者理解平时说到的一句话:u-boot阶段会执行boot.scr来加载后续的kernel和rootfs。

实际项目中,对一些集成化源码原厂,直接修改需要打补丁大量修改uboot不现实,例如赛灵思,则通过修改启动方式可通过这种方式来启动,将所有启动镜像(zImag、rootfs、dtb和boot.scr)烧写到启动分区(写明该启动分区或者放在1分区),uboot启动将自动读取boot.scr,根据boot.scr里的内容执行。以下是实际项目中配置使用的boot.scr:

setenv bootargs "earlycon console=ttyPS0,115200 clk_ignore_unused root=/dev/mmcblk0p3 rootfstype=ext4 rw rootwait fsck.mode=force mtdparts=spi0.0:28m(boot),-(user)"
	load mmc 0:1 0xE00000 Image
	load mmc 0:1 0x2600000 system.dtb
	booti 0xE00000 - 0x2600000

注意这里也传递了uboot向内核传递的启动参数。

6. scan_dev_for_extlinux:extlinux脚本启动内核

scan_dev_for_extlinux的定义如下:

	"boot_extlinux="                                                  \
		"sysboot ${devtype} ${devnum}:${distro_bootpart} any "    \
			"${scriptaddr} ${prefix}extlinux/extlinux.conf\0" \
	\
	"scan_dev_for_extlinux="                                          \
		"if test -e ${devtype} "                                  \
				"${devnum}:${distro_bootpart} "           \
				"${prefix}extlinux/extlinux.conf; then "  \
			"echo Found ${prefix}extlinux/extlinux.conf; "    \
			"run boot_extlinux; "                             \
			"echo SCRIPT FAILED: continuing...; "             \
		"fi\0"  

从/和/boot路径读取extlinux/下的extlinux.conf,如果存在该文件,则执行boot_extlinux,boot_extlinux使用sysboot命令读取了该文件然后执行文件内容。

extlinux.conf的内容示例如下,这点可以参考rockchip(Rockchip Kernel - Rockchip open source Document)是如何使用打包该文件的,rockchip平台看了下目前都是使用这种脚本来启动内核的。

label buildroot-sysupdate
kernel /boot/zImage
devicetree /boot/at91-sama5d3_xplained.dtb
append root=/dev/mmcblk0p${bootpart} rootwait console=ttyS0

以上分析的是extlinux.conf脚本启动内核,即uboot也提供了这种方式来启动内核。

顺便拓展下sysboot(cmd/pxe.c)命令,该命令最终还是调用了booti和bootm来启动内核,在uboot的源码中有体现这点:

static int label_boot(cmd_tbl_t *cmdtp, struct pxe_label *label)
{
....................

	kernel_addr = genimg_get_kernel_addr(bootm_argv[1]);
	buf = map_sysmem(kernel_addr, 0);
	/* Try bootm for legacy and FIT format image */
	if (genimg_get_format(buf) != IMAGE_FORMAT_INVALID)
		do_bootm(cmdtp, 0, bootm_argc, bootm_argv);
#ifdef CONFIG_CMD_BOOTI
	/* Try booting an AArch64 Linux kernel image */
	else
		do_booti(cmdtp, 0, bootm_argc, bootm_argv);
#elif defined(CONFIG_CMD_BOOTZ)
	/* Try booting a Image */
	else
		do_bootz(cmdtp, 0, bootm_argc, bootm_argv);
#endif
	unmap_sysmem(buf);
	return 1;
}

7. 后记

        在uboot开发中,启动内核可以灵活使用boot.scr和extlinux.conf两种脚本配置来启动内核,同时环境变量的时候也可以比较另外的对分区进行操作,利用gpt或者mbr两种分区命令,可实现分区表建立,甚至达到定制化的目的(例如rockchip使用字节parameter.txt文件来定制分区),实际最终使用的还是uboot开源的分区方案。

        本文还有更多的细节可以深入,越深入越感到有趣。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值