開發環境: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\n"); hang(); }
switch (spl_image.os) {
case IH_OS_U_BOOT:
debug("Jumping to U-Boot\n");
break;
#ifdef CONFIG_SPL_OS_BOOT
case IH_OS_LINUX:
debug("Jumping to Linux\n");
spl_fixup_fdt();
spl_board_prepare_for_linux();
jump_to_image_linux(&spl_image);
#endif
default:
debug("Unsupported OS image.. Jumping nevertheless..\n");
}
...
if (CONFIG_IS_ENABLED(ATF_SUPPORT)) {
debug("loaded - jumping to U-Boot via ATF BL31.\n");
bl31_entry();
}
debug("loaded - jumping to U-Boot...\n");
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.\n"); bl31_entry(); } debug("loaded - jumping to U-Boot...\n"); 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 "\0"
#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\n"); 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 時所寫入的地址。有了它,根目錄就可以正常地被掛載,系統即可以正常操作。
流程分析結束。
之前也分析過類似的流程,但還是第一次將完整的過程記錄下來。
僅以此文記錄那些年分析過的啟動流程。^_^