Linux系统开发——U-Boot启动流程分析
本文最后更新于11 天前,其中的信息可能已经过时,如有错误请发送邮件到527388734@qq.com

一、引言

在上一篇章中,我们从 Makefile 入手,详细分析了 U-Boot 在配置阶段和编译阶段的工作机制。本篇将带领大家进入 U-Boot 的源码世界,深入探索 U-Boot 是如何完成初始化配置,以及最终如何引导 Linux 内核的。

二、U-Boot启动流程详细分析

2.1 _start:一切从这里开始

在顶层 Makefile 中,u-boot 目标的生成依赖于 u-boot.lds 链接脚本——它将大量 .o 文件链接成一个完整的 ELF 可执行文件。因此,从链接脚本入手,可以让我们从宏观上把握 U-Boot 的内存布局,与此同时也可以明确U-Boot的一个入口地址。

打开该脚本,在开头处就可以发现这么一句话:

ENTRY(_start)

这里指明了 U-Boot 的入口地址:_start。我们可以在源码中查找 _start 的定义位置,如下图所示,它在 ./arch/arm/lib/vector.S下声明:

接下来,我们重点分析 _start 中的内容,从而搞清楚 U-Boot 在上电初期到底做了什么:

_start:
        b	reset
	ldr	pc, _undefined_instruction
	ldr	pc, _software_interrupt
	ldr	pc, _prefetch_abort
	ldr	pc, _data_abort
	ldr	pc, _not_used
	ldr	pc, _irq
	ldr	pc, _fiq
  • 第一条指令:b reset

b 是 ARM 汇编中的跳转指令(Branch),它的作用是让 CPU 无条件跳转到 reset 这个标号处执行。也就是说,CPU 上电后的第一条指令就是跳转到复位处理函数,开始真正的初始化工作。

  • 其余指令:ldr pc, _xxx

ldr 的本质是从内存中读取一个值存入寄存器。ldr pc, _xxx 的意思就是:把 _xxx 这个地址里存储的中断处理函数地址,加载到 pc 寄存器中。由于 pc 指向哪里,CPU 就执行哪里,所以这条指令相当于提前把中断处理函数的地址告诉 CPU。当对应中断发生时,CPU 就可以直接跳转过去执行。

明白了入口_start主要做的两件事情之后我们接下来该刨开reset,看看这个里面都做了哪些事情。

2.2 reset

通过查找我们可以看到reset里面就做了一个跳转的动作:

reset:
	/* Allow the board to save important registers */
	b	save_boot_params

而save_boot_params里面有只是做了一个跳转:

ENTRY(save_boot_params)
	b	save_boot_params_ret		@ back to my caller

直到save_boot_params_ret才真正开始做事情了:

save_boot_params_ret:
	mrs	r0, cpsr
	and	r1, r0, #0x1f		@ mask mode bits
	teq	r1, #0x1a		@ test for HYP mode
	bicne	r0, r0, #0x1f		@ clear all mode bits
	orrne	r0, r0, #0x13		@ set SVC mode
	orr	r0, r0, #0xc0		@ disable FIQ and IRQ
	msr	cpsr,r0

	mrc	p15, 0, r0, c1, c0, 0	@ Read CP15 SCTLR Register
	bic	r0, #CR_V		@ V = 0
	mcr	p15, 0, r0, c1, c0, 0	@ Write CP15 SCTLR Register
	ldr	r0, =_start
	mcr	p15, 0, r0, c12, c0, 0	@Set VBAR

	bl	cpu_init_cp15
	bl	cpu_init_crit

	bl	_main

reset的一个跳转链路如下图所示:

所以分析 reset,本质上就是分析 save_boot_params_ret。看到这么多汇编指令,大家会不会一脸懵?不必着急,接下来我逐条解释。

2.2.1 CPU 模式切换与中断屏蔽

  • mrs r0, cpsr

mrs 是 ARM 汇编中的状态寄存器读取指令(Move from System Register),作用是将系统寄存器(如 cpsr)的值读取到通用寄存器中。

为什么要读cpsr寄存器?

cpsr(Current Program Status Register,当前程序状态寄存器)记录了 CPU 的当前状态。包括当前的运行模式、中断使能状态以及条件标志位等等。接下来的各种操作都是对于cpsr里的值进行解析和修改。

  • and r1, r0, #0x1f

这句话的核心就是按位与运算操作。把他等效成C语言的形式为:

	r1 = r0 & 0x1f

相当于就是取cpsr里的低五位的值存储到r1寄存器当中。

在ARM架构当中cpsr 的低 5 位编码了 CPU 模式,从ARM官方可以看到以下说明

0b10000 (0x10)	User 模式
0b10001 (0x11)	FIQ 模式
0b10010 (0x12)	IRQ 模式
0b10011 (0x13)	Supervisor 模式	
0b10111 (0x17)	Abort 模式
0b11010 (0x1a)	 HYP 模式
0b11011 (0x1b)	Undefined 模式
0b11111 (0x1f)	System 模式
  • teq r1, #0x1a

这条指令判断当前 CPU 是否处于 HYP 模式。teq 会影响状态标志位(Z 标志)。后续指令中的 ne 后缀就是根据这个 Z 标志来判断是否执行。

  • bicne r0, r0, #0x1f

bicne 相当于是一个组合指令:bic相当于是位清零;ne:条件后缀:不相等时才执行;所以这句话等价于:

if(Z != 1){
  r0 = r0 & (~0x1f)
}
  • orrne r0, r0, #0x13

这个orrne同样是一个组合指令:orr按位或运算;ne同样为条件后缀,不相等才执行。等等效成C语言:

if(Z != 1){
   r0 = r0 | 0x13  //设置成SVC 模式
}
  • orr r0, r0, #0xc0
将 r0 的 bit6 和 bit7 置 1,即禁止 FIQ 和 IRQ 中断。
  • msr cpsr, r0

将修改后的值写回 cpsr,使配置生效。执行到这一步此时CPU会:切换到 SVC 模式(如果不是 HYP)并禁止IRQ和FIQ中断。

第一部分总结:上述代码的核心作用就是让 CPU 进入 SVC 特权模式,并暂时屏蔽所有中断,为后续的硬件初始化打好基础。

2.2.2 中断向量表重定位

接下来分析下面这部分:

	mrc	p15, 0, r0, c1, c0, 0	@ Read CP15 SCTLR Register
	bic	r0, #CR_V		@ V = 0
	mcr	p15, 0, r0, c1, c0, 0	@ Write CP15 SCTLR Register

	ldr	r0, =_start
	mcr	p15, 0, r0, c12, c0, 0	@Set VBAR

这段代码的作用是:

  1. 读取 CP15 协处理器的 SCTLR(系统控制寄存器)
  2. 清除 SCTLR 的 V 位(bit13),设为 0 表示使用低地址向量表
  3. 将 _start 的地址写入 VBAR(向量基址寄存器)

通过清除 V 位并设置 VBAR,我们可以将中断向量表重定位到任意地址(这里是 0x87800000)。这样当异常发生时,CPU 就会跳转到 0x87800000 + 偏移 去执行。

2.2.3 底层硬件初始化

再接下来跳转两个底层硬件初始化函数:

    bl  cpu_init_cp15
    bl  cpu_init_crit
  1. cpu_init_cp15:初始化 CP15 协处理器(Cache、MMU 等)
  2. cpu_init_crit:初始化关键硬件(如 L2 Cache、SDRAM 控制器等)
  • _main 后续分析

2.3 cpu_init_crit

他内部的实现为如下所示:

ENTRY(cpu_init_crit)
	b	lowlevel_init		@ go setup pll,mux,memory
ENDPROC(cpu_init_crit)

可以看到他的内部也只是实现了一个lowlevel_init跳转,可以在arch\arm\cpu\armv7\lowlevel_init.S下找到相对应的实现。

ENTRY(lowlevel_init)
	ldr	sp, =CONFIG_SYS_INIT_SP_ADDR
	bic	sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_DM
	mov	r9, #0
#else
#ifdef CONFIG_SPL_BUILD
	ldr	r9, =gdata
#else
	sub	sp, sp, #GD_SIZE
	bic	sp, sp, #7
	mov	r9, sp
#endif
#endif
	push	{ip, lr}
	bl	s_init
	pop	{ip, pc}
ENDPROC(lowlevel_init)

在分析这个函数之前需要给大家看一张图片:

可以看到在圈红圈地址范围内对应的是OCRAM(也就是内部RAM)理解了这一点之后我们在来看lowlevel_init的操作。

首先我们可以看到在函数开始部分先去执行了一个sp指针的复制操作。CONFIG_SYS_INIT_SP_ADDR(他的内部起始又嵌套了各种偏移运算,这里讲解起来很麻烦)这个宏的指向的地址其实就是指向片内SRAM中做临时栈的起始地址0x0091FF00。

用一张图可以看到当前sp的一个位置:

想必大家会有这些疑问:为什么偏偏要给sp寄存器赋值?又为什么这个栈是临时的?

  • sp(Stack Pointer)栈指针,指向当前栈顶位置,管理函数调用、返回地址、局部变量、寄存器保护,如果初始sp无效,那么程序在执行跳转等操作时会立刻崩溃。
  • 起始执行到这一步DDR 内存可能还未初始化,不能使用。所以栈只能放在已经可用的片内 SRAM 中。这个临时栈只用于极早期初始化,后续会切换到 DDR 中的正式栈。

我们在回到lowlevel_init的分析,可以看到接下来的各种操作sub、bic、mov等都是对于sp指针位置做更为具体的计算,其实他不管怎么设置,只需要知道他都会落到OCRAM的一个范围内。

接下来比较重要的操作:

	push	{ip, lr}
	bl	s_init
	pop	{ip, pc}

这是一个典型的保存现场恢复现场的一个流程。在执行s_init跳转前先将lr寄存器进行压栈push操作(lr寄存器即保存了lowlevel_init 中 bl 的下一条指令,如果不进行保存,那么跳转s_init会将lr寄存器的值进行覆盖,所以就没有办法回到调用之前的位置)。

而s_init对于其他平台来说是初始化PLL、时钟等底层硬件,但是对于IM6U平台来说,这些外设在BootROM阶段就已经完成初始化,所以他只是一个空函数,那么在此我们就不做解释了。

所以总结来说,lowlevel_init 中只是设置了sp指针的一个位置,为后面的初始化操作做准备。

看到这里我们我们先来回看一下他的一个调用流程,如下图所示:

2.4 _main:从汇编到 C 的跨越

_main 是 U-Boot 从汇编世界进入 C 世界的正式入口。在这之前,我们一直在汇编代码中打转(reset → cpu_init_cp15 → cpu_init_crit → lowlevel_init → s_init),现在终于要进入 C 语言的世界了。

_main的函数有点长,在这里就不一次性把代码做展示。我会对于_main的功能进行分块操作,逐步展开对于_main的讲解。

2.4.1 全局变量gd设置


	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)  /* sp 指向 0x0091FF00(OCRAM 顶部)*/
	bic	sp, sp, #7	/* 做8字节对齐 */
	mov	r0, sp          /*将sp寄存器的值赋值给r0,为后续board_init_f_alloc_reserve的传参做准备*/
	bl	board_init_f_alloc_reserve  /*内部其实就是预留早期的 malloc 内存区域和 gd 内存区域*/
	mov	sp, r0
	mov	r9, r0                     /*将gd保存到r9,便于后续直接访问*/
	bl	board_init_f_init_reserve  /*初始化gd,内部其实就是清零操作*/

上面的代码展示了 U-Boot 中全局数据结构 gd 的初始化过程,主要包括预留空间和清零初始化两步。

然而gd是什么?

gd(global_data)是 U-Boot 中一个非常重要的全局数据结构,用于在系统启动的不同阶段(重定位前、重定位后、SPL、主 U-Boot)之间传递系统状态信息。

typedef struct global_data {
    unsigned long   flags;          /* 状态标志 */
    unsigned long   baudrate;       /* 串口波特率 */
    unsigned long   cpu_clk;        /* CPU 时钟频率 */
    unsigned long   bus_clk;        /* 总线时钟频率 */
    unsigned long   ram_size;       /* RAM 大小 */
    void            *fdt_blob;      /* 设备树指针 */
    unsigned long   relocaddr;      /* 重定位后的地址 */
    unsigned long   reloc_off;      /* 重定位偏移 */
    unsigned long   start_addr_sp;  /* 初始栈指针 */
    unsigned long   malloc_base;    /* malloc 堆基址 */
    unsigned long   env_addr;       /* 环境变量地址 */
    unsigned long   env_valid;      /* 环境变量是否有效 */
    struct bd_info  *bd;            /* 板级信息 */
    // ... 还有很多字段
} gd_t;

又为什么要把gd保存到r9当中呢?

  • 一方面是考虑这个寄存器的稳定性,r9 是 callee-saved 寄存器,函数调用时不会被破坏
  • 另一方面是从性能方面考虑,直接访问寄存器要比直接访问内存要快的多

一句话总结:gd 是 U-Boot 的全局数据中心,存放在栈顶预留的空间中,r9 寄存器作为它的专用指针,保证了所有函数都能快速、稳定地访问系统状态信息。

2.4.2 执行 board_init_f

board_init_f 是 U-Boot 进入 C 世界后的第一个初始化函数,在重定位之前执行。

这个接口内部主要是做了下面几件事情:

  • 初始化一系列外设,比如串口、定时器等。
  • 初始化gd的各个成员变量。
  • 为代码重定位做准备(是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 U-Boot)

board_init_f在common/board_f.c文件当中实现。关键部分如下:

void board_init_f(ulong boot_flags)
{
    gd->flags = boot_flags;
    gd->have_console = 0;
    if (initcall_run_list(init_sequence_f))
	hang();
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
		!defined(CONFIG_EFI_APP)
	/* NOTREACHED - jump_to_copy() does not return */
	hang();
#endif
	imx6_light_up_led1();
}

可以看到这部分先是初始化了gd当中的flags以及have_console变量。

  • flags:启动标志
  • have_console:控制台状态

接下来最关键的部分是去运行初始化序列init_sequence_f。

init_sequence_f是一个函数指针数组,里面包含了需要执行的所有初始化函数。

而这些初始化函数实质上也是对于gd成员的各种操作。来去设置了设置 U-Boot 代码长度、malloc 堆空间范围、初始化控制台(串口)、设置重定位相关信息…等等。

执行流程为下图所示:

理解 board_init_f 执行后的内存布局,是掌握 U-Boot 重定位机制的关键。

在执行 board_init_f 之前,U-Boot 已经被加载到 DDR 中,但还没有进行重定位。此时的内存布局如下:

  • U-Boot 运行在 0x87800000(链接地址)
  • 栈还在 OCRAM 中(0x0091FF00 附近)
  • 整个 DDR 除了 U-Boot 代码占用的空间,其余都是空闲的

执行board_init_f 中的 init_sequence_f 会执行一系列 reserve_* 函数,从 DDR 顶部向下依次预留空间。

预留后的内存布局大概如下:

可以看到重定位前后的一个对比情况:

这么做的核心目的是为后续的Linux内核让路,避免内核对于U-Boot的一个踩踏。

最后再来总结一下,board_init_f 的核心工作是“搬家的图纸设计”——它从 DDR 顶部开始,依次为 MMU 页表、U-Boot 代码、malloc 堆、gd、设备树、栈等预留空间,确定好每个人的“新家地址”,为后续的 relocate_code 搬家行动做好全部准备。

2.4.3 重定位sp和gd至DDR中

这部分的关键指令为:

ldr	sp, [r9, #GD_START_ADDR_SP]	/* sp = gd->start_addr_sp */

我们在上面提到过r9是保存着gd全局变量的一个地址,而#GD_START_ADDR_SP这个宏的偏移是指向的是gd当中的start_addr_sp变量,而这个变量在board_init_f中已经规划好了位置。所以这条指令等价 C 语言为:

sp = gd->start_addr_sp

从这里开始U-Boot就可以不需要用到内部RAM(OCRAM)。

2.4.4 重定位gd

	ldr	r9, [r9, #GD_BD]		/* r9 = gd->bd */
	sub	r9, r9, #GD_SIZE		/* new GD is below bd */

操作r9寄存器即操作gd全局结构体。

2.4.5 重定位U-Boot

ldr r0, [r9, #GD_RELOCADDR]     /* r0 = gd->relocaddr */
b   relocate_code
  • ldr r0, [r9, #GD_RELOCADDR]:计算重定位目标地址,并将结果保存至r0。
  • b relocate_code:内部是执行重定位的过程,其传参是 r0,也就是目标地址。

对于relocate_code内部的实现,我们可以简单理解他做了一个memcpy的动作,从而实现了代码的一个重定位。

2.4.6 重定位中断向量表

bl	relocate_vectors

在 U-Boot 完成代码重定位之后,必须解决一个问题:中断向量表也跟着搬走了,但 CPU 还不知道它的新地址。而relocate_vectors 就是用来解决这个问题的。

其实内部和前面重定位向量表的方式差不多,都是去操作VBAR寄存器。如下所示:

	ldr	r0, [r9, #GD_RELOCADDR]	/* r0 = gd->relocaddr */
	mcr     p15, 0, r0, c12, c0, 0  /* Set VBAR */

2.4.7 执行board_init_r

board_init_r 是 U-Boot 重定位完成后执行的后期初始化函数,它与 board_init_f 分工明确:

  • board_init_f使用重定位前临时栈,主要任务是完成早期初始化、规划内存布局、为重定位做准备等工作。
  • board_init_r使用重定位后的最终栈(DDR顶部),主要任务是后期初始化、初始化外设、进入命令行。

但相同点是都会对 gd 全局变量进行各种赋值。

void board_init_r(gd_t *new_gd, ulong dest_addr)
{
//....

	if (initcall_run_list(init_sequence_r))
		hang();
	/* NOTREACHED - run_main_loop() does not return */
	hang();
}

可以看到,他跟board_init_f同样是运行了一段初始化列表。只不过列表变成了init_sequence_r。

而init_sequence_r同样有大量的初始化接口:

我总结了一下在init_sequence_r主要进行的初始化工作:

  • 核心初始化:
    • initr_caches:使能 MMU 和 Cache,提升 CPU 访问内存性能。
    • initr_malloc:重定位 malloc 堆,使用 DDR 中的新区域。
    • initr_console:重新初始化控制台(串口),确保重定位后能正常输出。
    • initr_env:从存储介质加载环境变量,决定 U-Boot 启动行为。
  • 外设初始化:
    • initr_mmc:初始化 MMC/SD 卡驱动,用于加载内核和文件系统
    • initr_net:初始化网络驱动,支持 tftp/nfs 等网络功能。
    • initr_usb:初始化 USB 控制器,支持 USB 键盘、U 盘等。
    • initr_i2c:初始化 I2C 总线,访问 I2C 设备(如 RTC、传感器)。
    • initr_spi:初始化 SPI 总线,访问 SPI Flash 等设备
  • 板级与调试:
    • initr_board:板级后期初始化(GPIO、LED、外设等)。
    • initr_trace:初始化跟踪调试功能。
    • initr_cmdline:初始化命令行,准备接收用户输入。
  • 进入主循环:
    • run_main_loop

细心的读者会发现:initr_mmc 在这里才初始化 eMMC 驱动,但 U-Boot 本身却是从 eMMC 中加载进来的。这岂不是矛盾?——没有驱动,怎么读取 U-Boot?没有 U-Boot,又哪里来的驱动?

简单来说,芯片内部固化的 BootROM 程序,会通过一个最简单的硬件接口,直接从 eMMC 的固定位置读取一小段代码(如 SPL),这段代码中包含完整的 MMC 驱动,从而能够去加载位于 eMMC 中的 U-Boot。

2.4.8 run_main_loop

一般U-Boot在启动时会进入3秒的倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内核,这个功能就是由 run_main_loop 函数来完成的。

static int run_main_loop(void)
{
#ifdef CONFIG_SANDBOX
	sandbox_main_loop_init();
#endif
	/* main_loop() can return to retry autoboot, if so just run it again */
	for (;;)
		main_loop();
	return 0;
}

可以看到这里边的主要功能函数就为main_loop();

main_loop 是 U-Boot 启动流程的最后一站,也是用户与 U-Boot 交互的核心入口。它负责处理启动延迟、自动启动内核、以及进入命令行交互模式。关键接口如下所示:

void main_loop(void)
{
	const char *s;
	cli_init(); /* 初始化命令行 */
	run_preboot_environment_command();/* 执行预启动命令(如按键检测)*/
	s = bootdelay_process();/* 获取延迟时间并倒计时 */
	if (cli_process_fdt(&s))
		cli_secure_boot_cmd(s);
	autoboot_command(s);/* 倒计时结束:自动启动内核 */
	cli_loop();         /* 检测到按键进入命令行模式 */
}

其流程图如下所示:

2.4.9 bootz启动Linux内核

2.4.9.1 核心数据结构:bootm_headers_t

接下来讲解的是:当 autoboot_command 中没有检测到按键按下时,U-Boot 自动启动 Linux 内核的过程。

在深入分析之前,我们先来认识一个关键的数据结构——bootm_headers_t。它记录了 U-Boot 启动内核所需的全部信息,主要包括三大部分:

  • 镜像信息(主要记录镜像在哪里、多大、从哪运行):
    • legacy_hdr_os/legacy_hdr_os_copy:传统 uImage 头部指针及副本
    • legacy_hdr_valid:头部有效性标志
    • os:内核镜像信息(加载地址、入口地址、大小等)
    • ep:操作系统入口点
  • 启动参数(记录要传递给内核的启动参数和设备树):
    • ft_addr / ft_len:设备树地址和长度
    • cmdline_start / cmdline_end:内核命令行参数位置
  • 状态机标志位:
    • BOOTM_STATE_START :开始阶段
    • BOOTM_STATE_FINDOS:查找OS镜像
    • ……其他的状态

可以说,bootm_headers_t 是 U-Boot 启动内核的 “数据总控中心”。整个启动过程,就是按照这个结构体中的信息描述来执行的。

bootm_headers_t images; //全局变量,贯穿整个启动流程
2.4.9.2 启动关键指令:bootz

那么,这个结构体是如何被填充,又是如何驱动内核启动的呢?

答案就在 bootz 命令的实现中。当 autoboot_command 检测到倒计时结束且没有按键按下时,会执行 bootcmd 环境变量中的命令,,通常是:

bootz ${loadaddr} - ${fdtaddr} 
# 例如:bootz 0x80800000  0x80800000

bootz 是 U-Boot 中的一个系统命令,它在源码中对应的实现接口是 do_bootz()。因此,要搞清楚 U-Boot 是如何启动 Linux 内核的,我们就需要从 do_bootz() 入手,看看 U-Boot 是如何一步步填充 bootm_headers_t,并通过状态机最终跳转到 Linux 内核的。

我们可以在cmd/bootm.c中找到如下定义:

int do_bootz(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
	int ret;
	argc--; argv++;
	if (bootz_start(cmdtp, flag, argc, argv, &images))
		return 1;
	bootm_disable_interrupts();
	images.os.os = IH_OS_LINUX;
	ret = do_bootm_states(cmdtp, flag, argc, argv,
			      BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
			      BOOTM_STATE_OS_GO,
			      &images, 1);
	return ret;
}

在理解do_bootz之前我们先了解一下其调用关系图如下所示:

注意do_bootz 调用了两次 do_bootm_states,但两次调用的阶段完全不同

  • 第一次:只做基础检查(BOOTM_STATE_START
  • 第二次:执行完整的启动流程(BOOTM_STATE_OS_PREP → BOOTM_STATE_OS_GO
2.4.9.3 启动总调度器:do_bootm_states 

do_bootm_states 是整个启动流程中最重要的接口,它根据状态机标志位,按顺序执行各个启动阶段。

可以看到这个接口里面会根据U-Boot的不同阶段去执行不同的代码。

我们可以用一张类似于时间轴的图去理解:

全局变量images同样是在这个流程中被填充完整的。

在 do_bootm_states 的执行过程中,会出现多个 boot_fn 调用,初次阅读时可能会感到困惑。实际上,这是 U-Boot 实现多操作系统支持的核心设计。相关源码如下:

// 1. 根据操作系统类型获取对应的启动函数
	boot_fn = bootm_os_get_boot_func(images->os.os);
	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);
// 2. 检查是否需要 boot_fn
	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;
	}
// 3. 按状态标志依次调用 boot_fn
	/* 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 = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);

在这里面boot_fn会有一个赋值的操作:boo_fn = bootm_os_get_boot_func(images->os.os);

这里的核心是 bootm_os_get_boot_func(images->os.os)——它根据操作系统类型返回对应的启动函数指针,这是 U-Boot 支持多种操作系统(Linux、VxWorks、Windows CE 等)的通用性设计。

boot_fn 是一个函数指针,其类型定义如下:

typedef int boot_os_fn(int flag, int argc, char * const argv[],
                       bootm_headers_t *images);
boot_os_fn *boot_fn;

U-Boot 定义了一个函数指针数组 boot_os[],记录了不同操作系统的启动接口:

static boot_os_fn *boot_os[] = {
    [IH_OS_U_BOOT] = do_bootm_standalone,
#ifdef CONFIG_BOOTM_LINUX
    [IH_OS_LINUX] = do_bootm_linux,        // Linux 启动函数
#endif
#ifdef CONFIG_BOOTM_VXWORKS
    [IH_OS_VXWORKS] = do_bootm_vxworks,    // VxWorks 启动函数
#endif
    // ... 其他操作系统
};

bootm_os_get_boot_func 的实现对于IM6U平台来说非常简单——直接返回数组中的对应项:

boot_os_fn *bootm_os_get_boot_func(int os)
{
    return boot_os[os];
}

那么如何确定当前启动的是 Linux?

在之前的启动流程中,images.os.os 已经被设置为 IH_OS_LINUX。例如如下操作:


images.os.os = IH_OS_LINUX;            // 设置为 Linux

因此,bootm_os_get_boot_func(IH_OS_LINUX) 返回的就是 do_bootm_linux。其实现如下:

int do_bootm_linux(int flag, int argc, char *argv[], bootm_headers_t *images)
{
	/* No need for those on ARC */
	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 之后,CPU 跳转到 Linux 内核入口(如 0x80800000),此时不再返回。至此,U-Boot 完成了引导内核启动的全部工作。

三、U-Boot完整启动流程总结

经过上文的完整流程分析,最后对于完整流程做一个汇总,如下图所示:

U-Boot 的启动本质上是:从汇编建立最小运行环境 → C 语言规划内存布局 → 自我重定位到 DDR 高端 → 初始化各类外设 → 根据环境变量自动引导内核。

整个过程中,gd 是贯穿前后的“数据总线”。

理解 U-Boot 的启动流程,光靠文字描述是远远不够的。真正想要掌握并能灵活运用 U-Boot 的引导机制,还是需要:

  • 一头扎进庞大的源码深处,沿着本文梳理的主线,逐个函数、逐个文件地去阅读和理解。
  • 多加日志打印,在关键路径上添加 debug() 或 printf(),观察实际运行时的流程走向——这是理解 U-Boot 行为最直接、最有效的方法之一。

说实话,U-Boot 的代码量非常大,设计也很精巧。博主本人目前也只是学习到了其中的冰山一角,文章中难免有理解不到位或表述不准确的地方,还请大家多多指正。

如果你在阅读 U-Boot 源码、移植 U-Boot、或者调试启动过程时遇到任何问题,欢迎随时与博主讨论交流。一起啃源码、一起填坑,才是嵌入式学习路上最有意思的事情。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇