【精读Uboot】Uboot跳转内核

在进入ATF后,ATF与OP-TEE共同协作,转而跳转到处于非安全上下文的U-Boot中(ATF->Uboot)。在Uboot阶段会重新从汇编开始执行,不一样的是里面的board_init_f函数不再是SPL阶段使用的,而是common/board_f.c中定义的。SPL阶段的board_init_f主要目的是使能芯片的核心器件,而common/board_f.c中定义的board_init_f函数是为了初始化Uboot的驱动框架。

1、汇编部分的差异

在汇编部分的board_init_f执行完之后,接下来就是清除bss然后跳转至board_init_r

clear_loop:
	str	xzr, [x0], #8
	cmp	x0, x1
	b.lo	clear_loop

	/* call board_init_r(gd_t *id, ulong dest_addr) */
	mov	x0, x18				/* gd_t */
	ldr	x1, [x18, #GD_RELOCADDR]	/* dest_addr */
	b	board_init_r			/* PC relative jump */

Uboot阶段的board_init_f函数初始化Uboot环境,如建立malloc框架、初始化串口、初始化定时器等基础框架,而board_init_r则是建立Uboot中的驱动模型,然后完成所有设备驱动的初始化,最后跳转至内核。

2、bootX调用过程

我们关心的是如何实现内核跳转,因此不再详细介绍Uboot中的驱动框架。在Uboot中,内核的跳转和bootX(bootibootmbootz)系列函数有关。bootibootm(FIT类型的内核镜像)用于启动uimagebootz用于启动zimage

board_init_r函数最后,uboot会进入main_loop函数,其中会从环境变量中的bootcmd里面的提取启动命令。

main_loop
	->bootdelay_process
    	->s = env_get("bootdelay")
    	->s = env_get("bootcmd")
    ->autoboot_command(s)
        ->run_command_list(s, -1, 0)
    		->run bootcmd

厂商自定义的启动命令使用CONFIG_BOOTCOMMAND宏来定义,对于i.MX8MP来说,bootcmd如下所示:

CONFIG_BOOTCOMMAND="run sr_ir_v2_cmd;run distro_bootcmd;run bsp_bootcmd"

其中bsp_bootcmd是以下内容,在启动内核之前。loadimage=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${image}从指定的mmc设备中将Image文件加载到指定的DDR地址${loadaddr}。然后在mmcboot中先设置bootargs启动参数,再使用bootm ${loadaddr}启动内核。

	"prepare_mcore=setenv mcore_clk clk-imx8mp.mcore_booted;\0" \
	"scriptaddr=0x43500000\0" \
	"kernel_addr_r=" __stringify(CONFIG_SYS_LOAD_ADDR) "\0" \
	"bsp_script=boot.scr\0" \
	"image=Image\0" \
	"splashimage=0x50000000\0" \
	"console=ttymxc1,115200\0" \
	"fdt_addr_r=0x43000000\0"			\
	"fdt_addr=0x43000000\0"			\
	"boot_fdt=try\0" \
	"fdt_high=0xffffffffffffffff\0"		\
	"boot_fit=no\0" \
	"fdtfile=" CONFIG_DEFAULT_FDT_FILE "\0" \
	"bootm_size=0x10000000\0" \
	"mmcdev="__stringify(CONFIG_SYS_MMC_ENV_DEV)"\0" \
	"mmcpart=1\0" \
	"mmcroot=/dev/mmcblk1p2 rootwait rw\0" \
	"mmcautodetect=yes\0" \
	"mmcargs=setenv bootargs ${jh_clk} ${mcore_clk} console=${console} root=${mmcroot}\0 " \
	"loadbootscript=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${bsp_script};\0" \
	"bootscript=echo Running bootscript from mmc ...; " \
		"source\0" \
	"loadimage=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${image}\0" \
	"loadfdt=fatload mmc ${mmcdev}:${mmcpart} ${fdt_addr_r} ${fdtfile}\0" \
	"mmcboot=echo Booting from mmc ...; " \
		"run mmcargs; " \
		"if test ${boot_fit} = yes || test ${boot_fit} = try; then " \
			"bootm ${loadaddr}; " \
		"else " \
			"if run loadfdt; then " \
				"booti ${loadaddr} - ${fdt_addr_r}; " \
			"else " \
				"echo WARN: Cannot load the DT; " \
			"fi; " \
		"fi;\0" \

	"bsp_bootcmd=echo Running BSP bootcmd ...; " \
		"mmc dev ${mmcdev}; if mmc rescan; then " \
		   "if run loadbootscript; then " \
			   "run bootscript; " \
		   "else " \
			   "if run loadimage; then " \
				   "run mmcboot; " \
			   "else run netboot; " \
			   "fi; " \
		   "fi; " \
	   "fi;"

3、booti

bootmbooti最终的跳转过程都差不多,区别在于bootm需要解包内核的FIT image(和Uboot中的FIT类似),得到内核和设备树的加载信息。因此我们这里就只看booti函数。

在上一节中,我们最终使用的启动命令为booti ${loadaddr} - ${fdt_addr_r},后面的内核地址和设备树地址为一个参数。

booti_start会做启动之前的准备工作,例如获取内核的ddr运行地址、设备树的地址,HAB签名验证等。将运行地址填充到bootm_headers中,然后再回到do_booti中,填充启动类型和架构信息。

int do_booti(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
    int ret;

    argc--; argv++;

    if (booti_start(cmdtp, flag, argc, argv, &images))
        return 1;

    //关闭中断
    bootm_disable_interrupts();
    //设置引导的os类型
    images.os.os = IH_OS_LINUX;
    //设置引导os的cpu架构
    images.os.arch = IH_ARCH_ARM64;

    ret = do_bootm_states(cmdtp, flag, argc, argv,
                  BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
                  BOOTM_STATE_OS_GO,
                  &images, 1);

    return ret;
}

最后通过 do_bootm_states跳转至内核。

4、do_bootm_states

在上一步我们分别传入了bootargs、Command flags、argc、argv、BOOTM_STATE_XXX、Image header信息、

首先根据os的类型获取对应的启动函数boot_fn,对于linux,这里的启动函数是do_bootm_linux。然后bootm_process_cmdline_env处理传入的bootargs,再通过do_bootm_linux->boot_prep_linux预处理跳转。最后由于传入的标志位BOOTM_STATE_OS_GOdo_bootm_linux->boot_jump_linux才真正地进行跳转。

boot_fn = bootm_os_get_boot_func(images->os.os);
need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE |
			BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP |
			BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO);

if (!ret && (states & BOOTM_STATE_OS_PREP)) {
		ret = bootm_process_cmdline_env(images->os.os == IH_OS_LINUX);
		if (ret) {
			printf("Cmdline setup failed (err=%d)\n", ret);
			ret = CMD_RET_FAILURE;
			goto err;
		}
		ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
	}

if (!ret && (states & BOOTM_STATE_OS_GO))
		ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
				images, boot_fn);

boot_prep_linux中,通过struct lmb(logical memory blocks)向内核传递参数。其中的传递机制为,根据lmb对象的物理地址和参数大小CONFIG_SYS_BARGSIZE,将需要传递的参数写入到指定位置,然后image_setup_linux把LMB信息填入到fdt中。

5、内核跳转

这里的代码分析删除了和ARMv7相关的代码。通过armv8_switch_to_el2来跳转至内核。

static void boot_jump_linux(bootm_headers_t *images, int flag)
{
	int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

	debug("## Transferring control to Linux (at address %lx)...\n",
		(ulong) kernel_entry);
	bootstage_mark(BOOTSTAGE_ID_RUN_OS);

	announce_and_cleanup(fake);

	if (!fake) {
		do_nonsec_virt_switch();
		//spin table 多核启动
		update_os_arch_secondary_cores(images->os.arch);

		if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
		    (images->os.arch == IH_ARCH_ARM))
			armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,
					    (u64)images->ft_addr, 0,
					    (u64)images->ep,
					    ES_TO_AARCH32);
		else
			armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
					    images->ep,
					    ES_TO_AARCH64);
	}
}

通过armv8_switch_to_el2切换到EL2跳转内核,或者直接跳转内核。switch_el读取当前的异常等级,el3对于label1,其余异常对应label0。64位架构下,label0会跳转到label2,直接跳转到x4寄存器中的地址。传入参数分别是x4=内核地址(images->ep),x5=架构(ES_TO_AARCH64),x0=设备树地址

ENTRY(armv8_switch_to_el2)
    switch_el x6, 1f, 0f, 0f
0:
    cmp x5, #ES_TO_AARCH64
    b.eq 2f
    bl armv8_el2_to_aarch32
2:
    br x4
1:  armv8_switch_to_el2_m x4, x5, x6  //x6为临时寄存器。
ENDPROC(armv8_switch_to_el2)

你可能感兴趣的:(深入理解uboot源代码,uboot,驱动开发,linux)