zoukankan      html  css  js  c++  java
  • [精] UBOOT2017+FIT 启动流程详尽分析

    开发环境:Nanopi-neo-plus2

    软件版本:uboot-2017

    软件版本:linux-4.14 


    买这个板子有一段时间了,并没有全身心的投入在上面,有时间了的话就搞一搞,

    这篇随笔算是对这个版本的 uboot 启动流程做个大概的梳理和记录,体系结构相关的内容不作分析。

    我这里会从 SPL(Secondary programloader) 阶段开始入手,按流程整理出 SPL 是怎样一步一步启动的 uboot,
    而 uboot 又是怎样加载并启动的 kernel。

    废话不多说,以内容为重点来打通整体脉络。

    一、从SPL入口点开始:

    阅读uboot源码时需注意:源码中存在众多 CONFIG_SPL_BUILD 宏的区分,使用了该宏的代码段,只有在 SPL 阶段时才会被编译进程序。

    [ start.S armV8 ]

    _start:
        b reset
    reset:
      ...
      bl lowlevel_init
      ...
      bl _main

    [ lowlevel_init.S armV8 ]

    ENTRY(lowlevel_init)

      ldr w0, =CONFIG_SPL_STACK

      bic sp, x0, #0xf

      stp x29, x30, [sp, #-16]!

      bl s_init

    ENDPROC(lowlevel_init)

    [ crt0_64.S arm/lib ]

    ENTRY(_main)
      ldr x0,
    =(CONFIG_SPL_STACK)   bic sp, x0, #0xf
      ...

      mov x18, x0
      bl board_init_f_init_reserve

      mov x0, #0

      bl board_init_f

      ...

      mov x0, x18             /* gd_t */
      ldr x1, [x18, #GD_RELOCADDR]  /* dest_addr */
      b board_init_r          /* PC relative jump */

    ENDPROC(_main)

     [ Board.c mach-sunxi ]

    void board_init_f(ulong dummy)
    {
        spl_init();
        preloader_console_init();
    #ifdef CONFIG_SPL_I2C_SUPPORT
    /* Needed early by sunxi_board_init if PMU is enabled */ i2c_init(CONFIG_SYS_I2C_SPEED, CONFIG_SYS_I2C_SLAVE); #endif sunxi_board_init();
    #ifdef CONFIG_SPL_BUILD spl_mem_test();
    #endif }

    [ spl.c spl ]

    void board_init_r(gd_t *dummy1, ulong dummy2)
    {
        ...
        board_boot_order(spl_boot_list);
    
        if (boot_from_devices(&spl_image, spl_boot_list,
            ARRAY_SIZE(spl_boot_list))) {
            puts("SPL: failed to boot from all boot devices
    ");
            hang();
        }    

        switch (spl_image.os) {
        case IH_OS_U_BOOT:
          debug("Jumping to U-Boot ");
          break;
    #ifdef CONFIG_SPL_OS_BOOT
        case IH_OS_LINUX:
          debug("Jumping to Linux ");
          spl_fixup_fdt();
          spl_board_prepare_for_linux();
          jump_to_image_linux(&spl_image);
    #endif
        default:
          debug("Unsupported OS image.. Jumping nevertheless.. ");
        }

       ...

        if (CONFIG_IS_ENABLED(ATF_SUPPORT)) {
          debug("loaded - jumping to U-Boot via ATF BL31. ");
          bl31_entry();
        }

        debug("loaded - jumping to U-Boot... ");
        spl_board_prepare_for_boot();
        jump_to_image_no_args(&spl_image);

    } 

     [ spl.c spl ]

    board_boot_order(spl_boot_list);
    ==>    
        boot_source = readb(SPL_ADDR + 0x28);
        switch (boot_source) {
        case SUNXI_BOOTED_FROM_MMC0:
            return BOOT_DEVICE_MMC1;
        case SUNXI_BOOTED_FROM_NAND:
            return BOOT_DEVICE_NAND;
        case SUNXI_BOOTED_FROM_MMC2:
            return BOOT_DEVICE_MMC2;
        case SUNXI_BOOTED_FROM_SPI:
            return BOOT_DEVICE_SPI;
        };

     [ spl.c spl ]

    static int boot_from_devices(struct spl_image_info *spl_image,
                     u32 spl_boot_list[], int count)
    {
        loader = spl_ll_find_loader(spl_boot_list[i]);
        ==>
         struct spl_image_loader *drv =   ll_entry_start(struct spl_image_loader, spl_image_loader);   const int n_ents =   ll_entry_count(struct spl_image_loader, spl_image_loader);   struct spl_image_loader *entry;   for (entry = drv; entry != drv + n_ents; entry++) {   if (boot_device == entry->boot_device)   return entry;   }
      ...
      if (loader && !spl_load_image(spl_image, loader))
        return 0;
    }

    代码中使用了 ll_entry_start 宏,就可以联想到  ll_entry_declare 声明,随后通过搜索可找到

    #define SPL_LOAD_IMAGE(__name)                    
        ll_entry_declare(struct spl_image_loader, __name, spl_image_loader)

    宏定义,进一步找到

    #define SPL_LOAD_IMAGE_METHOD(_name, _priority, _boot_device, _method) 
        SPL_LOAD_IMAGE(_method ## _priority ## _boot_device) = { 
            .name = _name, 
            .boot_device = _boot_device, 
            .load_image = _method, 
    }

    我们假设设备以SD卡的方式启动,SD对应着  BOOT_DEVICE_MMC1,那么通过搜索 SPL_LOAD_IMAGE_METHOD 筛选 BOOT_DEVICE_MMC1 就可以定位到驱动的本源,即

    [ spl_mmc.c spl ]

    SPL_LOAD_IMAGE_METHOD("MMC1", 0, BOOT_DEVICE_MMC1, spl_mmc_load_image);

     继续分析启动流程

    spl_load_image(spl_image, loader)
        ==>int spl_mmc_load_image(struct spl_image_info *spl_image,
                   struct spl_boot_device *bootdev)
            {
                ...
                spl_boot_mode(bootdev->boot_device);
                    return MMCSD_MODE_RAW;
                ...
                /* 通过宏 CONFIG_SPL_OS_BOOT 选择,spl直接启动OS还是启动uboot,这里返回1,启动uboot */
                spl_start_uboot()
                    return 1;
                ...
                mmc_load_image_raw_sector(spl_image, mmc,
                    CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR); 
                    ...
                    mmc_load_legacy(spl_image, mmc, sector, header);
                        spl_parse_image_header(spl_image, header);
                            spl_set_header_raw_uboot(spl_image);
                              spl_image->size = CONFIG_SYS_MONITOR_LEN;
                              spl_image->entry_point = CONFIG_SYS_UBOOT_START;
                              spl_image->load_addr = CONFIG_SYS_TEXT_BASE;
                              spl_image->os = IH_OS_U_BOOT;
                              spl_image->name = "U-Boot";
           ...
         }

     回到 board_init_r 继续分析,spl_image->os 赋值为  IH_OS_U_BOOT ,所以 break 直接跳出;接下来有执行ATF的bl31部分(这部分暂不做分析),最后执行 jump_to_image_no_args 跳转到 spl_image->entry_point ,也就是正式的uboot阶段。

    if (CONFIG_IS_ENABLED(ATF_SUPPORT)) {
       debug("loaded - jumping to U-Boot via ATF BL31.
    ");
       bl31_entry();
    }
    
    debug("loaded - jumping to U-Boot...
    ");
    spl_board_prepare_for_boot();
    jump_to_image_no_args(&spl_image);

    至此,SPL 的生命阶段正式结束。

    二、继续分析 UBOOT:

    Makefile 通过宏的区分编译出两个执行程序 spl、uboot.

    SPL 已经在上述分析完毕,BOOT 在启动初期与 SPL 大同小异,这里只是分析比较大的变动。
    直接定位到 _main,由于不再有 CONFIG_SPL_BUILD 宏的限制,这里的程序代码就发生了变化。

    [ crt0_64.S ]

    ENTRY(_main)
      ldr x0, =(CONFIG_SPL_STACK)
      bic sp, x0, #0xf
      ...
      mov x18, x0
      bl    board_init_f_init_reserve
    
      mov x0, #0
    
      bl board_init_f
      ...
       /* Add in link-vs-relocation offset */
       ldr    x9, [x18, #GD_RELOC_OFF]    /* x9 <- gd->reloc_off */
       add    lr, lr, x9                  /* new return address after relocation */
       ldr    x0, [x18, #GD_RELOCADDR]    /* x0 <- gd->relocaddr */
       b    relocate_code
       ...
    
      mov x0, x18               /* gd_t */
      ldr    x1, [x18, #GD_RELOCADDR]  /* dest_addr */
      b    board_init_r             /* PC relative jump */
    
    ENDPROC(_main)

    [ board_f.c common ]

    void board_init_f(ulong boot_flags)
    {
        ...        ///< 初始化CPU、Timer、Serial、板级信息及内存分配、布局等
    }

    [ relocate.S arm/lib ]

    ENTRY(relocate_code)
        ...    ///< 这部分代码比较关键,个人觉得和之前总结的动态编译较为类似,这里不再分析
            ///< 有兴趣可以参考下这篇博文:blog.csdn.net/ooonebook/article/details/53047992
    ENDPROC(relocate_code)

    [ board_r.c common ]

    void board_init_r(gd_t *new_gd, ulong dest_addr)
    {
        ...    ///< 初始化化各种软硬件资源
        
        run_main_loop
        ==>
        for (;;)   main_loop(); }

    [ main.c common ]

    void main_loop(void)
    {
        const char *s;
        ...
        s = bootdelay_process();
        ==>
            s = env_get("bootcmd");  ///< 取得 bootcmd 环境变量信息,
            ==>
                "bootcmd="    CONFIG_BOOTCOMMAND        ""
                #define CONFIG_BOOTCOMMAND "run distro_bootcmd"
                ...            ///<    自动启动脚本
                
        if (cli_process_fdt(&s))    ///< fdt中的 bootcmd 可覆盖上步中赋值的 s
            cli_secure_boot_cmd(s);
    
        autoboot_command(s);    ///< 在 bootdelay 时间内没有任何操作的话,则自动执行上述 s,脚本最终会执行到用户的 boot.scr 里面的内容
                                ///< 我在这里使用的是( bootm FIT )的启动方式,而 boot.scr 中默认为( booti image initramfs fdt ),所以将该内容注释掉
        cli_loop();        ///< 进入到控制台,可手动执行boot中自带的指令
        ...
    }

    执行到这里,在默认的情况下会执行 boot.scr 脚本中的代码,最终会使用 booti 指令来启动 kernel.

    三、关于 FIT 方式启动

    [ FIT 制作过程 ]

    1.initramfs 制作,制作过程可见链接
    2.在 kernel 源码下建立FIT文件夹,加入imgae、fdt、initramfs
    3.构建 its 文件,内容例如下

     [ image.its ]

    /dts-v1/;
     
    / {
        description = "U-Boot fitImage for plnx_aarch64 kernel";
        #address-cells = <1>;
     
        images {
            kernel@0 {
                description = "Linux Kernel";
                data = /incbin/("./FIT/Image");
                type = "kernel";
                arch = "arm64";
                os = "linux";
                compression = "none";
                load = <0x46080000>;
                entry = <0x46080000>;
                hash@1 {
                    algo = "sha1";
                };
            };
            fdt@0 {
                description = "Flattened Device Tree blob";
                data = /incbin/("./FIT/sun50i-h5-nanopi-neo-plus2.dtb");
                type = "flat_dt";
                arch = "arm64";
                compression = "none";
                hash@1 {
                    algo = "sha1";
                };
            };
            ramdisk@0 {
                description = "ramdisk";
                data = /incbin/("./FIT/rootfs.cpio.gz");
                type = "ramdisk";
                arch = "arm64";
                os = "linux";
                compression = "none";
                hash@1 {
                    algo = "sha1";
                };
            };
        };
        configurations {
            default = "config@0";
            config@0 {
                description = "Boot Linux kernel with FDT blob + ramdisk";
                kernel = "kernel@0";
                fdt = "fdt@0";
                ramdisk = "ramdisk@0";
                hash@1 {
                    algo = "sha1";
                };
            };
        };
    };

    使用此 its 打包成 image 文件

    mkimage -f image.its kernel.img

     kernel.img 打包完成后可以放到 SD 卡的 boot 分区中,它是 fat32 文件系统。

     然后,在 boot.scr 脚本中可去除关于 加载 fdt、ramdist、booti 等相关指令,我们已做好集成,而无需一一地进行加载。最后在脚本中加入以下两条指令,即可完成系统的启动。

    fatload mmc 0 0x47000000 kernel.img
    bootm 0x47000000

     在这里提醒一下,image 中的 load、entry 地址需要注意。起初实验时,我将这两个值都配置为和 booti 一样的地址:0x46080000,但是始终无法进入 kernel,没有任何打印输出。

     最后无奈,开始从源码对比 bootm 与 booti 的不同点在哪里,其中一点在 booti_setup 源码中的注释吸引了我的关注,代码如下:

    static int booti_setup(bootm_headers_t *images)
    {
        ...
        /*
         * Prior to Linux commit a2c1d73b94ed, the text_offset field
         * is of unknown endianness.  In these cases, the image_size
         * field is zero, and we can assume a fixed value of 0x80000.
         */
        if (ih->image_size == 0) {
          puts("Image lacks image_size field, assuming 16MiB
    ");
          image_size = 16 << 20;
          text_offset = 0x80000;
        } else {
          image_size = le64_to_cpu(ih->image_size);
          text_offset = le64_to_cpu(ih->text_offset);
        }
        ...
    }

    这里面如果 image_size 为 0 的话,那么就会将 kernel 的 load 点加上 0x80000 的 offset,我尝试在 its 的 load、entry 点加上这个 offset,重新打包测试,kernel 就可以正常的启动了。

    果然这个地址乱填还真的不行。

    四、bootm 启动 FIT 流程简要分析

     [ bootm.c ]

    int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
    {
        ...    ///< 子命令处理
        
        return do_bootm_states(cmdtp, flag, argc, argv, BOOTM_STATE_START |
            BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER |
            BOOTM_STATE_LOADOS |
            BOOTM_STATE_RAMDISK |
            BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
            BOOTM_STATE_OS_GO, &images, 1);
    }

     [ bootm.c ]

    int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
                int states, bootm_headers_t *images, int boot_progress)
    {
        bootm_start(cmdtp, flag, argc, argv);
        bootm_find_os(cmdtp, flag, argc, argv);                  ///< 这里 case IMAGE_FORMAT_FIT, 获取 FIT 中的 kernel 资源信息
        bootm_find_other(cmdtp, flag, argc, argv);               ///< 提取其它资源信息 ramdisk、fdt 等
        bootm_load_os(images, &load_end, 0);                     ///< 加载 kernel 资源,如果采用了压缩格式,会涉及到 bootm_decomp_image
        boot_ramdisk_high(&images->lmb, images->rd_start,        ///< 重定位 ramdisk 至高地址区 
             rd_len, &images->initrd_start, &images->initrd_end);
        boot_relocate_fdt(&images->lmb, &images->ft_addr,        ///< 重定位 fdt
             &images->ft_len);
        boot_fn = bootm_os_get_boot_func(images->os.os);         ///< 这里返回 do_bootm_linux 地址
        boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);        ///< 调用 boot_prep_linux
        boot_selected_os(argc, argv, BOOTM_STATE_OS_GO, images, boot_fn);    ///< 调用 boot_jump_linux
    }

     [ bootm.c ]

    int do_bootm_linux(int flag, int argc, char * const argv[],
               bootm_headers_t *images)
    {
        boot_prep_linux(images);
        ==>
            image_setup_linux(images)
                boot_relocate_fdt(lmb, of_flat_tree, &of_size);
                image_setup_libfdt(images, *of_flat_tree, of_size, lmb);
                    fdt_initrd(blob, *initrd_start, *initrd_end);
                        fdt_setprop_uxx(fdt, nodeoffset, "linux,initrd-start", (uint64_t)initrd_start, is_u64);    ///< 在 fdt 中加入 initrd-start 信息,以便于 kernel 初始化时可以找到 initramfs.
                        fdt_setprop_uxx(fdt, nodeoffset, "linux,initrd-end", (uint64_t)initrd_end, is_u64);
        boot_jump_linux(images, flag);
        ==>
            armv8_switch_to_el2((u64)images->ft_addr,   ///< fdt 入口点
                                0, 0, 0,
                            images->ep,                 ///< kernel 入口点
                            ES_TO_AARCH64);
    }

    [ transition.S armV8 ]

    ENTRY(armv8_switch_to_el2)
        switch_el x6, 1f, 0f, 0f
    0:
        cmp x5, #ES_TO_AARCH64
        b.eq 2f
        /*
         * When loading 32-bit kernel, it will jump
         * to secure firmware again, and never return.
         */
        bl armv8_el2_to_aarch32
    2:
        /*
         * x4 is kernel entry point or switch_to_el1
         * if CONFIG_ARMV8_SWITCH_TO_EL1 is defined.
         * When running in EL2 now, jump to the
         * address saved in x4.
         */
        br x4        ///< 我这里 CONFIG_ARMV8_SWITCH_TO_EL1 未定义,所以直接跳转至内核入口
    1:    
        armv8_switch_to_el2_m x4, x5, x6
    ENDPROC(armv8_switch_to_el2)

    到这里,如果执行正常的话,那么 uboot 就会将手上的军统大权完完全全地交给了 kernel,它也是完成了自己最重要的任务 --- 引导内核。

    五、kernel 解析 initrd 地址

     uboot 将 initrd 的地址写入了 fdt,在 kernel 里又是怎样解析的呢?我们继续分析,可以从 start_kernel 一点点梳理到 early_init_dt_check_for_initrd,奥秘就在这里。

     [ fdt.c ]

    static void __init early_init_dt_check_for_initrd(unsigned long node)
    {
        ...
        prop = of_get_flat_dt_prop(node, "linux,initrd-start", &len);
        start = of_read_number(prop, len/4);
        prop = of_get_flat_dt_prop(node, "linux,initrd-end", &len);
        end = of_read_number(prop, len/4);
        __early_init_dt_declare_initrd(start, end);
        ==>
            initrd_start = start;
            initrd_end = end;
    }

     [ initramfs.c ]

    static int __init populate_rootfs(void)
    {
        ...
        unpack_to_rootfs((char *)initrd_start,
                initrd_end - initrd_start);
        ...
    }

    这里 unpack_to_rootfs 所用到的参数,即为 uboot 在配置 fdt 的 bootargs 时所写入的地址。有了它,根目录就可以正常地被挂载,系统即可以正常操作。

    流程分析结束。

    之前也分析过类似的流程,但还是第一次将完整的过程记录下来。

    仅以此文记录那些年分析过的启动流程。^_^

  • 相关阅读:
    2016"百度之星"
    codeforces 55 div2 C.Title 模拟
    codeforces 98 div2 C.History 水题
    codeforces 97 div2 C.Replacement 水题
    codeforces 200 div2 C. Rational Resistance 思路题
    bzoj 2226 LCMSum 欧拉函数
    hdu 1163 九余数定理
    51nod 1225 余数的和 数学
    bzoj 2818 gcd 线性欧拉函数
    Codeforces Round #332 (Div. 2)D. Spongebob and Squares 数学
  • 原文地址:https://www.cnblogs.com/GyForever1004/p/11962995.html
Copyright © 2011-2022 走看看