在进入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的驱动框架。
在汇编部分的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中的驱动模型,然后完成所有设备驱动的初始化,最后跳转至内核。
我们关心的是如何实现内核跳转,因此不再详细介绍Uboot中的驱动框架。在Uboot中,内核的跳转和bootX(booti
、bootm
、bootz
)系列函数有关。booti
和bootm
(FIT类型的内核镜像)用于启动uimage
,bootz
用于启动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;"
bootm
和booti
最终的跳转过程都差不多,区别在于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
跳转至内核。
在上一步我们分别传入了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_GO
,do_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中。
这里的代码分析删除了和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)