[精] 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\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", , 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 ;
...
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, # 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 制作过程 ]
.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 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 == ) {
puts("Image lacks image_size field, assuming 16MiB\n");
image_size = << ;
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, );
}
[ 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
:
cmp x5, #ES_TO_AARCH64
b.eq 2f
/*
* When loading -bit kernel, it will jump
* to secure firmware again, and never return.
*/
bl armv8_el2_to_aarch32
:
/*
* 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 未定义,所以直接跳转至内核入口
:
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/);
prop = of_get_flat_dt_prop(node, "linux,initrd-end", &len);
end = of_read_number(prop, len/);
__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 时所写入的地址。有了它,根目录就可以正常地被挂载,系统即可以正常操作。
流程分析结束。
之前也分析过类似的流程,但还是第一次将完整的过程记录下来。
仅以此文记录那些年分析过的启动流程。^_^
[精] UBOOT2017+FIT 启动流程详尽分析的更多相关文章
- Flink 源码解析 —— Standalone Session Cluster 启动流程深度分析之 Job Manager 启动
Job Manager 启动 https://t.zsxq.com/AurR3rN 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink 从0到1学习 -- Mac ...
- Flink 源码解析 —— Standalone Session Cluster 启动流程深度分析之 Task Manager 启动
Task Manager 启动 https://t.zsxq.com/qjEUFau 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink 从0到1学习 -- Ma ...
- 海思uboot启动流程详细分析(转)
海思uboot启动流程详细分析(一) 海思uboot启动流程详细分析(二) 海思uboot启动流程详细分析(三)
- lk启动流程详细分析
转载请注明来源:cuixiaolei的技术博客 这篇文章是lk启动流程分析(以高通为例),将会详细介绍下面的内容: 1).正常开机引导流程 2).recovery引导流程 3).fastboot引导流 ...
- 海思uboot启动流程详细分析(三)【转】
1. 前言 书接上文(u-boot启动流程分析(二)_平台相关部分),本文介绍u-boot启动流程中和具体版型(board)有关的部分,也即board_init_f/board_init_r所代表的. ...
- 海思uboot启动流程详细分析(二)
1. 第二个start.S 从start_armboot开始,在startup.c中有包含#include <config.h> 在config.h中: /* Automatically ...
- 【内核】linux内核启动流程详细分析
Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件,包括内核入口ENTRY(stext)到start_kernel间的初始化代码, 主要作用 ...
- 【内核】linux内核启动流程详细分析【转】
转自:http://www.cnblogs.com/lcw/p/3337937.html Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件 ...
- Flink on Yarn模式启动流程源代码分析
此文已由作者岳猛授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Flink on yarn的启动流程可以参见前面的文章 Flink on Yarn启动流程,下面主要是从源码角 ...
随机推荐
- HelloPlatform
#include <stdio.h> int main() { #if _PLATFORM_ == _PLATFORM_TRU64 printf("Hello _PLATFORM ...
- ECMAScript规范解读this
在<JavaScript深入之执行上下文栈>中讲到,当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution conte ...
- what-is-the-difference-between-type-and-class
Inspired by Wikipedia... In type theory terms; A type is an abstract interface. Types generally repr ...
- LeetCode 702. Search in a Sorted Array of Unknown Size
原题链接在这里:https://leetcode.com/problems/search-in-a-sorted-array-of-unknown-size/ 题目: Given an integer ...
- SpringBoot:认认真真梳理一遍自动装配原理
前言 Spring翻译为中文是“春天”,的确,在某段时间内,它给Java开发人员带来过春天,但是随着我们项目规模的扩大,Spring需要配置的地方就越来越多,夸张点说,“配置两小时,Coding五分钟 ...
- 单片机模块化程序: 单片机AT指令配置模块程序模板(非阻塞版)
拷贝这两个文件到自己的工程 测试1://单片机发送AT+RST\r\n 如果单片机串口接收到OK 或者ready 执行下一条 测试视频: https://qqqqqbucket.oss-cn-bei ...
- Tomcat8.x的安装与启动
Tomcat是企业网站的服务器,大多都用于中.小型网站开发和学习开发JSP应用程序中.笔者也是开始学习,下面介绍Tomcat8.x的安装步骤. 进入Tomcat官网,点击左边的download目录下的 ...
- request登录案例
一.案例需求 1.编写login.html登录页面 username & password 两个输入框 2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表 3.使 ...
- NOI2019 Day2游记
开场T1是个最短路优化建图,边向二维矩形内所有点连,本来可以写树套树的,但是卡空间(128MB),后来发现其实是不用把边都建出来的,只需要用数据结构模拟dijkstra的过程,支持二维区间对一个值取m ...
- TCP/IP协议族基本知识
常见的网络拓扑 两台主机通信的过程:应用进程产生消息,经由主机的 TCP/IP 协议栈发送到局域网(LAN),最后经过广域网(目前最大的广域网的因特网)中的网络设备(路由器)传给目的主机所在的局域网( ...