image.png

uboot不是运行时,它的使命就是加载运行Linux,之后就死亡了。

我们在uboot命令行输入boot,或者到命令行倒计时时不中断,则默认去启动了linux,本文对这个过程进行下分析。

1. boot过程

1.1 boot命令

找一个命令定义,可以cmd目录搜索"boot, ",或者熟悉的去cmd目录下直接找,这里在cmd/bootm.c中

int do_bootd(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
    return run_command(env_get("bootcmd"), flag);
}

U_BOOT_CMD(
    boot,    1,    1,    do_bootd,
    "boot default, i.e., run 'bootcmd'",
    ""
);

这里可见其执行了环境变量'bootcmd',我们在命令行打印如下:

image.png 可见跟main_loop中打印出来是变量s是一样的,也就是说命令行输入boot跟命令行不中断继续执行是一致的,跟我们的猜想相符。

这串命令的最后执行了bootm

U_BOOT_CMD(
    bootm,    CONFIG_SYS_MAXARGS,    1,    do_bootm,
    "boot application image from memory", bootm_help_text
);

bootm的三个参数如下: image.png

1.2 bootm命令

do_bootm--》do_bootm_states(在boot/bootm.c中定义)主要流程包括根据镜像头获取镜像信息,解压镜像,以及启动操作系统。以下为其主要执行流程:

image.png

入参state的定义:

    states = BOOTM_STATE_START | BOOTM_STATE_FINDOS | BOOTM_STATE_PRE_LOAD |
        BOOTM_STATE_FINDOTHER | BOOTM_STATE_LOADOS |
        BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
        BOOTM_STATE_OS_GO;

根据state的定义执行相关的处理函数。

int do_bootm_states(struct cmd_tbl *cmdtp, int flag, int argc,
            char *const argv[], int states, struct bootm_headers *images,
            int boot_progress)
{
    if (states & BOOTM_STATE_START)
        ret = bootm_start(cmdtp, flag, argc, argv);

    if (!ret && (states & BOOTM_STATE_PRE_LOAD))
        ret = bootm_pre_load(cmdtp, flag, argc, argv);

    if (!ret && (states & BOOTM_STATE_FINDOS))
        ret = bootm_find_os(cmdtp, flag, argc, argv);

    if (!ret && (states & BOOTM_STATE_FINDOTHER))
        ret = bootm_find_other(cmdtp, flag, argc, argv);

    /* Load the OS */
    if (!ret && (states & BOOTM_STATE_LOADOS)) {
        iflag = bootm_disable_interrupts();//关闭中断
        ret = bootm_load_os(images, 0);
        if (ret && ret != BOOTM_ERR_OVERLAP)
            goto err;
        else if (ret == BOOTM_ERR_OVERLAP)
            ret = 0;
    }

    /* Relocate the ramdisk */
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
    if (!ret && (states & BOOTM_STATE_RAMDISK)) {
        ulong rd_len = images->rd_end - images->rd_start;

        ret = boot_ramdisk_high(&images->lmb, images->rd_start,
            rd_len, &images->initrd_start, &images->initrd_end);
        if (!ret) {
            env_set_hex("initrd_start", images->initrd_start);
            env_set_hex("initrd_end", images->initrd_end);
        }
    }
#endif
#if CONFIG_IS_ENABLED(OF_LIBFDT) && defined(CONFIG_LMB)
    if (!ret && (states & BOOTM_STATE_FDT)) {
        boot_fdt_add_mem_rsv_regions(&images->lmb, images->ft_addr);
        ret = boot_relocate_fdt(&images->lmb, &images->ft_addr,
                    &images->ft_len);
    }
#endif

    /* From now on, we need the OS boot function */
    if (ret)
        return ret;
    boot_fn = bootm_os_get_boot_func(images->os.os); //,参数 images->os.os 就是系统类型,根据这个系统类型来选择对应的启动函数,Linux 系统启动函数为 do_bootm_linux,boot_fn=do_bootm_linux,后面执行 boot_fn函数的地方实际上是执行的 do_bootm_linux 函数

    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 (boot_fn == NULL && need_boot_fn) {
        if (iflag)
            enable_interrupts();
        printf("ERROR: booting os '%s' (%d) is not supported\n",
               genimg_get_os_name(images->os.os), images->os.os);
        bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS);
        return 1;
    }


    /* Call various other states that are not generally used */
    if (!ret && (states & BOOTM_STATE_OS_CMDLINE))
        ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images);
    if (!ret && (states & BOOTM_STATE_OS_BD_T))
        ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images);
    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);
    }

#ifdef CONFIG_TRACE
    /* Pretend to run the OS, then run a user command */
    if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {
        char *cmd_list = env_get("fakegocmd");

        ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,
                images, boot_fn);
        if (!ret && cmd_list)
            ret = run_command_list(cmd_list, -1, flag);
    }
#endif

    /* Check for unsupported subcommand. */
    if (ret) {
        printf("subcommand failed (err=%d)\n", ret);
        return ret;
    }

    /* Now run the OS! We hope this doesn't return */
    if (!ret && (states & BOOTM_STATE_OS_GO))
        ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
                images, boot_fn); //启动linux内核

    /* Deal with any fallout */
err:
    if (iflag)
        enable_interrupts();

    if (ret == BOOTM_ERR_UNIMPLEMENTED)
        bootstage_error(BOOTSTAGE_ID_DECOMP_UNIMPL);
    else if (ret == BOOTM_ERR_RESET)
        do_reset(cmdtp, flag, argc, argv);

    return ret;
}

bootm_os_get_boot_func来查找对应系统的启动函数boot_fn 在boot/bootm_os.c中:

bootm_os_get_boot_func--》
    return boot_os[os];

static boot_os_fn *boot_os[] = {
    [IH_OS_U_BOOT] = do_bootm_standalone,
#ifdef CONFIG_BOOTM_LINUX
    [IH_OS_LINUX] = do_bootm_linux,
#endif
#ifdef CONFIG_BOOTM_NETBSD
    [IH_OS_NETBSD] = do_bootm_netbsd,
#endif
#ifdef CONFIG_BOOTM_RTEMS
    [IH_OS_RTEMS] = do_bootm_rtems,
#endif
#if defined(CONFIG_BOOTM_OSE)
    [IH_OS_OSE] = do_bootm_ose,
#endif
#if defined(CONFIG_BOOTM_PLAN9)
    [IH_OS_PLAN9] = do_bootm_plan9,
#endif
#if defined(CONFIG_BOOTM_VXWORKS) && \
    (defined(CONFIG_PPC) || defined(CONFIG_ARM) || defined(CONFIG_RISCV))
    [IH_OS_VXWORKS] = do_bootm_vxworks,
#endif
#if defined(CONFIG_CMD_ELF)
    [IH_OS_QNX] = do_bootm_qnxelf,
#endif
#ifdef CONFIG_INTEGRITY
    [IH_OS_INTEGRITY] = do_bootm_integrity,
#endif
#ifdef CONFIG_BOOTM_OPENRTOS
    [IH_OS_OPENRTOS] = do_bootm_openrtos,
#endif
#ifdef CONFIG_BOOTM_OPTEE
    [IH_OS_TEE] = do_bootm_tee,
#endif
#ifdef CONFIG_BOOTM_EFI
    [IH_OS_EFI] = do_bootm_efi,
#endif
};

boot_selected_os()在boot/bootm_os.c中定义:

int boot_selected_os(int argc, char *const argv[], int state,
             struct bootm_headers *images, boot_os_fn *boot_fn)
{
    arch_preboot_os();
    board_preboot_os();
    boot_fn(state, argc, argv, images); //启动内核

    return BOOTM_ERR_RESET;
}

1.3 do_bootm_linux

image.png

boot_fn=do_bootm_linux就是是最终启动 Linux 内核的函数。其调用 boot_prep_linux 来完成具体的处理过程。boot_prep_linux 主要用于处理环境变量bootargs,bootargs 保存着传递给 Linux kernel 的参数。

int do_bootm_linux(int flag, int argc, char *const argv[],
           struct bootm_headers *images)
{
    /* No need for those on ARM */
    if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
        return -1;

    if (flag & BOOTM_STATE_OS_PREP) {
        boot_prep_linux(images);
        return 0;
    }

    if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
        boot_jump_linux(images, flag);
        return 0;
    }
    boot_prep_linux(images);
    boot_jump_linux(images, flag);
    return 0;
}

boot_jump_linux

static void boot_jump_linux(struct bootm_headers *images, int flag)
{
#ifdef CONFIG_ARM64
    void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,
            void *res2);
    int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

    kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,
                void *res2))images->ep; //此函数是进入 Linux 内核的

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

    announce_and_cleanup(fake); //打印一些信息并做一些清理工作,例如打印了Starting Kernel ...

    if (CONFIG_IS_ENABLED(OF_LIBFDT) && images->ft_len)
        r2 = (unsigned long)images->ft_addr; //如果使用设备树的话,r2 应该是设备树的起始地址,而设备树地址保存在 images的 ftd_addr 成员变量中
    else
        r2 = gd->bd->bi_boot_params; //如果不使用设备树的话,r2 应该是 uboot 传递给 Linux 的参数起始地址,也就是环境变量 bootargs 的值,

        kernel_entry(0, machid, r2); //uboot真正进入linux内核了

kernel_entry函数有三个参数:zero,arch,params,第一个参数 zero 同样为 0;第二个参数为机器 ID;第三个参数 ATAGS 或者设备树(DTB)首地址,ATAGS 是传统的方法,用于传递一些命令行信息啥的,如果使用设备树的话就要传递设备树(DTB)。

函数 kernel_entry 并不是 uboot 定义的,而是 Linux 内核定义的,Linux 内核镜像文件的第一行代码就是函数 kernel_entry,而 images->ep 保存着 Linux内核镜像的起始地址,起始地址保存的正是 Linux 内核第一行代码!

2. 镜像基础介绍

uboot主要用于启动操作系统,以armv8架构下的linux为例,其启动时需要包含kernel、dtb和rootfs三部分。

2.1 内核镜像

linux内核编译出来的原始内核文件vmlinux,在optee/linux目录中

image.png

vmlinux经过bojcopy去掉elf头文件生成的纯二进制文件

OBJCOPYFLAGS_Image := -O binary -R .note -R .note.gnu.build-id -R .comment –S   (1)
targets := Image Image.bz2 Image.gz Image.lz4 Image.lzma Image.lzo
$(obj)/Image: vmlinux FORCE                                                     
        $(call if_changed,objcopy)                                               (2)

$(obj)/Image.bz2: $(obj)/Image FORCE
        $(call if_changed,bzip2)                                                 (3)

$(obj)/Image.gz: $(obj)/Image FORCE
        $(call if_changed,gzip)                                                  (4)

$(obj)/Image.lz4: $(obj)/Image FORCE
        $(call if_changed,lz4)                                                   (5)

$(obj)/Image.lzma: $(obj)/Image FORCE
        $(call if_changed,lzma)                                                  (6)

$(obj)/Image.lzo: $(obj)/Image FORCE
        $(call if_changed,lzo)                                                   (7)

(1)objcopy命令使用的flag定义
(2)以vmlinux为原始文件,通过objcopy命令制作Image镜像。其命令可扩展如下:

aarch64-linux-gnu-objcopy -O binary -R .note -R .note.gnu.build-id -R .comment -S vmlinux Image

该命令会执行以下操作:
 a –O binary:将输出二进制镜像,即会去掉elf头
 b –R .note:-R选项表示去掉镜像中指定的section,如这里会去掉.note、.note.gnu.build-id和.comment段
 c –S:去掉符号表和重定位信息,它与-R选项的功能类似,都是为了减小镜像的size
  因此,执行该命令后生成的Image镜像是去掉elf头,去掉.note等无用的section,以及strip过的二进制镜像。它可以被uboot的booti命令直接启动。但若要使用bootm启动,则还需要将其进一步封装为后面介绍的uimage或bootimg镜像

(3 – 7)以Image为源文件,调用不同的压缩算法,对镜像进行压缩。若调用gzip命令,则可将其压缩为我们熟悉的zImage镜像。与Image一样,压缩后的镜像也是可以被booti直接启动,且经过封装以后可以被bootm启动的

2.2 dtb文件

设备树是设备树dts源文件经过编译后生成的,其目标文件为二进制格式的dtb文件。其示例编译命令如下:

dtc -I dts -O dtb -o example.dtb example.dts

(1)–I:指定输入文件格式
(2)–O:指定输出文件格式
(3)–o:指定输出文件名

设备树还支持dtb overlay机制,即可以向设备提供一个基础dtb和多个dtbo镜像,并在启动前将它们merge为最终的dtb。

2.3 根文件系统

linux可以支持多种形式的根文件系统,如initrd、initramfs、基于磁盘的根文件系统等。站在启动镜像的角度看其实它们都是制作好的文件系统镜像,内核可以从特定的位置获取并挂载它们。以下是它们在启动时的基本特性:

(1)initrd
它是一种内存文件系统,需要由bootloader预先加载到内存中,并将其内存地址传递给内核。如uboot将initrd加载到地址$initrd_addr处,则bootm参数如下:

  bootm  $kernel_addr  $initrd_addr  $fdt_addr

(2)initramfs
 initramfs也是一种内存文件系统,但与initrd不同,它是与内核打包在一起的。因此不需要通过额外的参数

(3)磁盘rootfs
磁盘根文件系统会被刷写到flash、mmc或disk的分区中,在内核启动时可在bootargs添加下面格式的参数,以指定根文件系统的位置

   root=/dev/xxx

因此,以上这些rootfs只有initrd是需要uboot独立加载的,故只有当rootfs为initrd时,uboot镜像打包流程才需要在镜像打包时为其单独考虑

关于uboot支持的镜像格式,可以参考:https://zhuanlan.zhihu.com/p/520575102

这里说下Fit uimage格式,这个跟secure boot相关。 Fit uimage是使用devicetree语法来定义uimage镜像描述信息以及启动时的各种属性,这些信息被写入一个后缀名为its的源文件中。

以下是一个its文件的示例

/dts-v1/;

/ {
    description = "Various kernels, ramdisks and FDT blobs";
    #address-cells = <1>;

    images {
        kernel-1 {
            description = "vanilla-2.6.23";              (1)
            data = /incbin/("./vmlinux.bin.gz");         (2)
            type = "kernel";                             (3)
            arch = "ppc";                                (4)
            os = "linux";                                (5)
            compression = "gzip";                        (6)
            load = <00000000>;                           (7)
            entry = <00000000>;                          (8)
            hash-1 {
                algo = "md5";                        (9)
            };
            hash-2 {
                algo = "sha1";
            };
        };

        kernel-2 {
            description = "2.6.23-denx";
            data = /incbin/("./2.6.23-denx.bin.gz");
            type = "kernel";
            arch = "ppc";
            os = "linux";
            compression = "gzip";
            load = <00000000>;
            entry = <00000000>;
            hash-1 {
                algo = "sha1";
            };
        };

        kernel-3 {
            description = "2.4.25-denx";
            data = /incbin/("./2.4.25-denx.bin.gz");
            type = "kernel";
            arch = "ppc";
            os = "linux";
            compression = "gzip";
            load = <00000000>;
            entry = <00000000>;
            hash-1 {
                algo = "md5";
            };
        };

        ramdisk-1 {
            description = "eldk-4.2-ramdisk";
            data = /incbin/("./eldk-4.2-ramdisk");
            type = "ramdisk";
            arch = "ppc";
            os = "linux";
            compression = "gzip";
            load = <00000000>;
            entry = <00000000>;
            hash-1 {
                algo = "sha1";
            };
        };

        ramdisk-2 {
            description = "eldk-3.1-ramdisk";
            data = /incbin/("./eldk-3.1-ramdisk");
            type = "ramdisk";
            arch = "ppc";
            os = "linux";
            compression = "gzip";
            load = <00000000>;
            entry = <00000000>;
            hash-1 {
                algo = "crc32";
            };
        };

        fdt-1 {
            description = "tqm5200-fdt";
            data = /incbin/("./tqm5200.dtb");
            type = "flat_dt";
            arch = "ppc";
            compression = "none";
            hash-1 {
                algo = "crc32";
            };
        };

        fdt-2 {
            description = "tqm5200s-fdt";
            data = /incbin/("./tqm5200s.dtb");
            type = "flat_dt";
            arch = "ppc";
            compression = "none";
            load = <00700000>;
            hash-1 {
                algo = "sha1";
            };
        };

    };

    configurations {
        default = "config-1";

        config-1 {
            description = "tqm5200 vanilla-2.6.23 configuration";
            kernel = "kernel-1";
            ramdisk = "ramdisk-1";
            fdt = "fdt-1";
        };

        config-2 {
            description = "tqm5200s denx-2.6.23 configuration";
            kernel = "kernel-2";
            ramdisk = "ramdisk-1";
            fdt = "fdt-2";
        };

        config-3 {
            description = "tqm5200s denx-2.4.25 configuration";
            kernel = "kernel-3";
            ramdisk = "ramdisk-2";
        };
    };
};

它包含images和configurations两个顶级节点,images指定该its文件会包含哪些镜像,以及这些镜像的属性信息。configurations用于定义一系列镜像组合信息,如在本例中包含了config-1、config-2和config-3三种镜像组合方式。Its使用default属性指定启动时默认采用的配置信息,若启动时不希望使用默认配置,则可通过在启动参数中动态指定配置序号。下面我们通过kernel-1节点看下image属性的含义:

(1)镜像的描述信息
(2)镜像文件的路径
(3)镜像类型,如kernel、ramdisk或fdt
(4)支持的架构
(5)支持的操作系统
(6)其使用的压缩算法
(7)加载地址
(8)运行地址
(9)完整性校验使用的hash算法

  configurations的属性比较简单,就是指定某个配置下使用哪一个kernel、dtb和ramdisk镜像。Fit image除了支持完整性校验外,还可支持hash算法 + 非对称算法的secure boot方案,如以下例子:

kernel {
            data = /incbin/("test-kernel.bin");
            type = "kernel_noload";
            arch = "sandbox";
            os = "linux";
            compression = "none";
            load = <0x4>;
            entry = <0x8>;
            kernel-version = <1>;
            signature {
                algo = "sha1,rsa2048";        (1)
                key-name-hint = "dev";       (2)
            };
        };

(1)指定sha1为secure boot签名使用的hash算法,rsa2048为其使用的签名算法
(2)可能使用的验签密钥名

后记:

uboot写到这里启动完毕,但是感觉越写越复杂,那linux不得更复杂的吓人。看来掌握这些知识不是一蹴而就的,嵌入式工程师也是需要时间去沉淀。

读书和思考: 最近看《认知觉醒》,分享给大家一些(2):

  1. 利用潜意识去抓取内心的想法和做出一些选择,审视自己的第一反应。学习的时候可以不用面面俱到,但是之后让自己想下那些东西有用让自己印象深刻,然后针对这点去思考拓展实践,读万卷书不如行千里路。
  2. 无法把自己在注意力投入到重要的事情上,像刷手机视频、打游戏、刷剧和对有兴趣可以即时满足的小事投入过多的时间等,就是元认知出问题了。你能意识到自己在想什么,进而意识到这些想法是否明智,再进一步纠正那些不明智的想法,最终做出更好的选择。一句话:多反思。

results matching ""

    No results matching ""