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 时所写入的地址。有了它,根目录就可以正常地被挂载,系统即可以正常操作。

    流程分析结束。

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

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

  • 相关阅读:
    未能导入activex控件,请确保它正确注册
    【OpenCV入门教程之一】 安装OpenCV:OpenCV 3.0、OpenCV 2.4.8、OpenCV 2.4.9 +VS 开发环境配置
    回调函数
    struct--------构造函数对结构体初始化的影响
    调用约定
    HDU 4923 Room and Moor
    Codeforces 260 C. Boredom
    Codeforces 260 B. Fedya and Maths
    Codeforces 260 A
    HNU 12888 Encryption(map容器)
  • 原文地址:https://www.cnblogs.com/GyForever1004/p/11962995.html
Copyright © 2011-2022 走看看