本文从代码角度,分析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开源的分区方案。
本文还有更多的细节可以深入,越深入越感到有趣。