u-boot启动过程(bootROM到SPL到u-boot)

因为整个启动过程会两次运行start.S和crt0.S的代码,所以我以这两个文件为脉络来学习整个启动过程

start.S(SPL)

注意:start.S和crt0.S分别会被spl和u-boot运行一次

主体内容阅读

以我手头的imx6ull的coretex-A7内核对应的start.S作为学习样本,逐步了解u-boot启动过程

c 复制代码
	.globl	reset
	.globl	save_boot_params_ret
#ifdef CONFIG_ARMV7_LPAE
	.global	switch_to_hypervisor_ret
#endif
reset:
	/* Allow the board to save important registers */
	b	save_boot_params
save_boot_params_ret:

开头这段主要是提供一个上电复位的全局入口标号reset,芯片上电复位(Reset)后,异常向量表中的复位跳转指令(如 b reset)就会精准跳转到这里的 reset: 标号处开始执行,

并且提供一个save_boot_params函数,其负责先把BootROM留下的一些参数(例如:芯片是从 SD 卡、NAND Flash 还是 USB 启动的,或者一些安全密钥、Boot ROM 传递的参数指针)存到内存中再回过头来初始化

CONFIG_ARMV7_LPAE 则是一个32位物理地址扩展功能,其能把4G的虚拟地址空间往1TB的物理地址空间中映射,我的这个imx6ull显然没有开启这个功能,所以跳过

switch_to_hypervisor_ret可以理解为对内存的一种超级虚拟化,这里提供一个Hypervisor / Virtualization 模式切换后的返回点

save_boot_params_ret: 是特定的返回入口标号。 在 save_boot_params 函数执行完毕后,它不会用 bx lr(因为此时连 SP 栈都还没建立),而是直接使用 b save_boot_params_ret 强行跳回这里,继续往下执行 CPU 的基础硬件初始化(如关 Cache、关 MMU、切 SVC 模式等)。

c 复制代码
#ifdef CONFIG_ARMV7_LPAE
/*
 * check for Hypervisor support
 */
	mrc	p15, 0, r0, c0, c1, 1		@ read ID_PFR1   (Processor Feature Register 1,处理器特征寄存器 1)
	and	r0, r0, #CPUID_ARM_VIRT_MASK	@ mask virtualization bits
	cmp	r0, #(1 << CPUID_ARM_VIRT_SHIFT)
	beq	switch_to_hypervisor
switch_to_hypervisor_ret:
#endif
	/*
	 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
	 * except if in HYP mode already
	 */
	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

#ifdef CONFIG_ARMV7_LPAE中包含的是对hypervisor模式的检查与配置,最后会通过开头定义的全局标号在栈还未初始化的情况下,精确跳转回来switch_to_hypervisor_ret

接下来的一些语句就很清晰了,注释写的很清楚

关中断,并且将cpu切换为supervisor模式,让U-Boot 拥有全盘操控硬件的最高特权级

  • mrs r0, cpsr : 读取当前程序状态寄存器(CPSR)的值到 r0
  • and r1, r0, #0x1f : 提取 CPSR 的低 5 位 M[4:0](Mode bits),即当前 CPU 处于什么模式。
  • teq r1, #0x1a : 测试当前模式是否为 HYP 模式(编码为 0x1A
    • 注:TEQ 是按位异或测试。如果相等,Z 标志位置 1,后续带有 ne (Not Equal) 条件后缀的指令就会跳过不执行。
  • bicne r0, r0, #0x1f如果不等于 HYP 模式(ne :将低 5 位模式位全部清零(BIC = Bit Clear)。
  • orrne r0, r0, #0x13如果不等于 HYP 模式(ne :将低 5 位设置为 0x130x13 代表 SVC32 模式 ,即 Supervisor 模式)。
    • 逻辑小结:如果 CPU 已经处于 HYP 模式(比如刚才完成了 Hypervisor 初始化),就不去改变它的模式,让它保持在 HYP 模式;如果是 User、IRQ、System 等其他模式,则强制改成 SVC32 模式。
  • orr r0, r0, #0xc0 : 将 r0 的第 6 位(F)和第 7 位(I)置 1(0xC0 = 1100 0000b)。在 ARM 中,将 I 和 F 位置 1 代表禁止 IRQ 和 FIQ 中断
  • msr cpsr, r0 : 把修改后的 r0 值写回 cpsr 寄存器,模式切换和关中断在此指令完成后立即生效!

(mrs)M ove to R egister from S pecial System Register和(msr) M ove to S pecial System Register from R egister的缩写。

它们是 ARM 架构中专门用于在通用寄存器(R0~R15)特殊系统程序状态寄存器(如 CPSR / SPSR)之间传输数据的指令。

c 复制代码
/*
 * Setup vector:
 * (OMAP4 spl TEXT_BASE is not 32 byte aligned.
 * Continue to use ROM code vector only in OMAP4 spl)
 */
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
	/* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
	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

	/* Set vector address in CP15 VBAR register */
	ldr	r0, =_start
	mcr	p15, 0, r0, c12, c0, 0	@Set VBAR
#endif

	/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
	bl	cpu_init_cp15
#ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY
	bl	cpu_init_crit
#endif
#endif

	bl	_main

#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))在SPL代码中显然是通过的

第一段if中主要做的是确定了异常向量表的位置在哪里,这里通过 bic r0, #CR_VV 位置 0,解锁对 VBAR 寄存器的使用,异常向量表的位置就由 VBAR 寄存器说了算(也就是允许u-boot自定义异常向量表)

接着/* Set vector address in CP15 VBAR register */这代码将U-Boot 自己写的异常向量表首地址)写入 CP15 的 VBAR(Vector Base Address Register,向量基地址寄存器)

那么在u-boot的运行阶段,发生异常或者中断就有地方去处理了

紧接着

  • bl cpu_init_cp15 : 跳转执行 cpu_init_cp15 函数。
    • 作用: 彻底复位与初始化 CP15 协处理器。清除并禁用 TLB(页表转换旁路缓存)I-Cache(指令缓存)D-Cache(数据缓存)MMU(内存管理单元),保证 CPU 处于最干净的裸机物理寻址状态。
    • 禁用I-Cache是因为在芯片初始化阶段,内存中的指令代码可能会被修改,关闭I-Cache这个指令加速器保证读到的一直是最新的指令
    • 禁用D-Cache也是同理,往内存写数据(代码)必须随时保持最新----代码(指令)也是数据
    • 禁用MMU和TLB则是因为此时页表还没建立,CPU发出的地址如果经过MMU,则必定出错
    • 这里的D-Cache和I-Cache其实是哈佛架构(数据和代码指令分开)的一种体现,本质上是为了适配CPU和内存的速度差距
  • bl cpu_init_crit : 跳转执行 cpu_init_crit 函数(Critical Initialization,关键底座初始化)。
    • 作用: 调用平台/开发板级别的低级初始化(如 lowlevel_init)。主要在此处通过汇编操作硬件寄存器去初始化芯片的主频时钟(PLL)和外置 DRAM/DDR 内存控制器
  • bl _main : 则是进入c语言的世界
    • 这里的 _main 位于 arch/arm/lib/crt0.S(C Runtime 0,C 运行时初始化汇编)中

crt0.S(SPL)

crt0.S中的代码就是给start.S执行完后给c语言准备运行环境用的

c 复制代码
ENTRY(_main)
/*
 * Set up initial C runtime environment and call board_init_f(0).
 */
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
	ldr	sp, =(CONFIG_SPL_STACK)
#else
	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
#if defined(CONFIG_CPU_V7M)	/* v7M forbids using SP as BIC destination */
	mov	r3, sp
	bic	r3, r3, #7
	mov	sp, r3
#else
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */
#endif
	mov	r0, sp
	bl	board_init_f_alloc_reserve
	mov	sp, r0
	/* set up gd here, outside any C code */
	mov	r9, r0
	bl	board_init_f_init_reserve

	mov	r0, #0
	bl	board_init_f

上面这一段代码就是从start.S跳转过来的运行的_main

首先判断有没有使用SPL,如果使用了SPL,将 SP 指向 SPL 专用的栈顶地址。

c 复制代码
#if defined(CONFIG_CPU_V7M)	/* v7M forbids using SP as BIC destination */
	mov	r3, sp
	bic	r3, r3, #7
	mov	sp, r3
#else
	bic	sp, sp, #7	/* 8-byte alignment for ABI compliance */

这段代码是为了符合ARM规范,在发生函数调用前SP必须强制8字节对齐

因为栈是向下生长的,所以这种对齐方式就相当于在原来的栈地址上增加了一小块填充,不会影响之前的数据

bic sp, sp, #7 是位清除(Bit Clear)指令,把 SP 的低 3 位强行清零(等价于 SP = SP & ~0x7)。

c 复制代码
	mov	r0, sp
	bl	board_init_f_alloc_reserve
	mov	sp, r0
	/* set up gd here, outside any C code */
	mov	r9, r0
	bl	board_init_f_init_reserve

	mov	r0, #0
	bl	board_init_f

这段代码把sp指针传递给board_init_f_alloc_reserve函数并且跳转执行再回来,这个函数内部计算了 sizeof(struct global_data) 以及早期 malloc 堆的大小。它将传入的 SP 地址向下减去这部分大小,并保证 16 字节对齐后,作为新的基地址通过 r0 返回 。r0就是被gd大小占用后的栈指针

r9是为了arm为了提高效率,专用的保存gd(global_data)指针的寄存器----这样就不用每次用gd都先到sram读取一次gd指针,然后再去读取gd的数据了,而是直接在寄存器r9就能找到gd数据的指针

mov r0, #0 :设置参数为 0(传递给 board_init_f(ulong xxx) 的第一个参数 xxx)。

bl board_init_f :跳转到SPL的board_init_f()函数进行执行

SPL的board_init_f()

imx6ull提供的board_init_f函数如下

c 复制代码
void board_init_f(ulong dummy)
{
	ccgr_init();//
	/* setup AIPS and disable watchdog */
	arch_cpu_init();
	/* iomux and setup of i2c */
	board_early_init_f();
	/* setup GP timer */
	timer_init();
	/* UART clocks enabled and gd valid - init serial console */
	preloader_console_init();
	/* DDR initialization */
	spl_dram_init();
	/* Clear the BSS. */
	memset(__bss_start, 0, __bss_end - __bss_start);
	/* load/boot image from boot device */
	board_init_r(NULL, 0);
}

board_init_f的函数内十分简洁,可以直接按照函数来阅读了

`

ccgr_init();

CCM(Clock Controller Module)i.MX 芯片的时钟控制模块。

这段代码内容十分简单粗暴,直接打开了所有片上外设的时钟

arch_cpu_init();

在进行底层的硬件初始化之前,先解除外设寄存器的访问限制(setup AIPS),并顺便把会在启动时捣乱、导致误复位的看门狗关掉(disable watchdog)

在对片上外设(如 UART、GPIO、I2C 等)进行配置前,必须先"打开权限",配置 AIPS 寄存器,允许系统对这些外设的寄存器进行读写访问。

board_early_init_f();

内容十分简单,就是设置一下uart1的引脚复用,方便后续SPL输出启动信息

timer_init();

初始化 通用硬件定时器(General Purpose Timer) ,将其配置为1MHz的频率,也就是周期1us

从而为后续的代码提供一个微秒级别的延时器

preloader_console_init();

350行,

首先在内部定义了一个board_data结构体,并且将其强制放在.data段,防止后续清零.bss段操作将其值清空

然后将该bd数据的指针赋值给global_data的bd指针

351行,将设定好的波特率赋值给global_data

然后初始化串口并且开启,并且往串口输出一段调试信息

spl_dram_init();

作用和其字面意思一样,配置ddr内存

memset(__bss_start, 0, __bss_end - __bss_start);

清空bss段,因为bss段规定是默认为0的数据段,但是上电后,ram中的数据是随机的,这样是为了符合c语言标准

c语言标准: 所有未显式初始化的全局变量和静态(static)变量,其初值必须默认为 0(或空指针 NULL)。

这里清空的bss段是指sram中规定的bss段,因为后续要调用board_init_r(NULL, 0),这个函数中会调用存储介质等的驱动函数,驱动函数内可能会有一些全局变量或者静态变量存在.

board_init_r(NULL, 0);

在SPL的board_init_f()的末尾,调用board_init_r(NULL, 0)

imx6ull的这个函数调用的是u-boot提供的在common/spl.c中的通用的board_init_r函数

c 复制代码
void board_init_r(gd_t *dummy1, ulong dummy2)
{
	u32 spl_boot_list[] = {
		BOOT_DEVICE_NONE,
		BOOT_DEVICE_NONE,
		BOOT_DEVICE_NONE,
		BOOT_DEVICE_NONE,
		BOOT_DEVICE_NONE,
	};
	struct spl_image_info spl_image;

	debug(">>spl:board_init_r()\n");
#if defined(CONFIG_SYS_SPL_MALLOC_START)
	mem_malloc_init(CONFIG_SYS_SPL_MALLOC_START,
			CONFIG_SYS_SPL_MALLOC_SIZE);
	gd->flags |= GD_FLG_FULL_MALLOC_INIT;
#endif
	if (!(gd->flags & GD_FLG_SPL_INIT)) {
		if (spl_init())
			hang();
	}
#ifndef CONFIG_PPC
	/*
	 * timer_init() does not exist on PPC systems. The timer is initialized
	 * and enabled (decrementer) in interrupt_init() here.
	 */
	timer_init();
#endif
#ifdef CONFIG_SPL_BOARD_INIT
	spl_board_init();
#endif
	memset(&spl_image, '\0', sizeof(spl_image));
	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_board_prepare_for_linux();
		jump_to_image_linux(&spl_image,
				    (void *)CONFIG_SYS_SPL_ARGS_ADDR);
#endif
	default:
		debug("Unsupported OS image.. Jumping nevertheless..\n");
	}
#if defined(CONFIG_SYS_MALLOC_F_LEN) && !defined(CONFIG_SYS_SPL_MALLOC_SIZE)
	debug("SPL malloc() used %#lx bytes (%ld KB)\n", gd->malloc_ptr,
	      gd->malloc_ptr / 1024);
#endif
	debug("loaded - jumping to U-Boot...\n");
	spl_board_prepare_for_boot();
	jump_to_image_no_args(&spl_image);
}

上面这个函数最主要的作用是两个函数

一个是boot_from_devices(&spl_image, spl_boot_list,ARRAY_SIZE(spl_boot_list))

其负责遍历board_boot_order(spl_boot_list);提供的启动顺序表,该表内的启动顺序既可以是厂家写死的,也可以实现拨动不同的开关,函数内部会去读取对应寄存器,从而从不同的介质启动u-boot

然后用启动顺序表去和SPL中真实有的提供的介质的驱动进行匹配,匹配上了则调用spl_load_image()

复制u-boot的内容到ddr上,并且将结果赋值给spl_image结构体,包括u-boot被转移到ddr的目标地址和u-boot入口地址entry_point

其中先调用spl__ll_find_loader()SPL的只读数据中的特定段获取编译SPL时提供的从每个介质上加载u-boot的结构体

获取过程如下:

其直接遍历==.u_boot_list_2_spl_image_loader_xxx==这个 section 中的结构体

这样设计的好处是让spl更通用,其无需识别具体的结构体,或者引入什么特定驱动的头文件,而是从指定的段内读取并且拼接出结构体

只要某个驱动结构体参与编译,就必须用一个声明用的宏,将其写入到.u_boot_list_2_spl_image_loader_xxx这个段里面

在经历一系列判断后调用jump_to_image_no_args(&spl_image)

其中image_entry是一个地址,但是被当做函数调用,本质上就是将pc指针指向了u-boot的在ddr上的入口地址entry_point

那么理所当然的,接下来就是从DDR中开始执行u-boot主体的第一句指令了

u-boot的第一条指令就是_start:段,被定义在vector.S中

start.S(u-boot)

其又会跳转回start.S中定义的reset标号,重新执行

上面这段代码执行的过程和一开始SPL中执行的过程类似,都是配置svc模式,并且关闭一些i-cache,d-cache等功能(保证修改了的指令能在ddr上保持最新状态)

最后还是一样,跳转回c语言(_main)的世界

就又回到了crt0.S执行

crt0.S(u-boot)

和之前spl执行的时候略有区别,这段代码此时就不会走CONFIG_SPL_BUILD这个分支了

所以栈指针指向的地址也理所当然的变了,变成了CONFIG_SYS_INIT_SP_ADDR,此时SPL已经运行结束,为了安全起见(因为ddr此时的空间大小及后续空间安排等等都还不清楚,也没有被规划),u-boot的栈还是暂时在sram上,重新换个栈地址并且8字节对齐 (因为此时SPL已经完成了自己的任务,所以其空间就无所谓了,包括中断向量表也已经被start.S中的代码安排DDR中的u-boot中断向量表接管了)

然后也临时在sram分配堆和存放global_data的空间

所以这里做的事情和SPL运行这段代码的事情差不多,都是在SRAM上配置一个C语言的运行环境

最后再调用u-boot主体提供的在common/board_f.c中的board_init_f函数

这段代码主要通过initcall_run_list()执行init_sequence_f列表中的函数,里面的函数非常多

大体总结一下做的事情

  1. 微调一下cpu的配置
  2. 探明 DDR 总容量,从 DDR 最顶端开始向下把 MMU、FrameBuffer、异常向量表、U-Boot 代码段、堆区、gd、新栈的空间全部规划好(并不分配写数据,只是预留)
  3. 给u-boot复制一份设备树,并且给设备驱动预留malloc用的空间,这样u-boot就可以使用一些设备树中的设备了(比如海思用网口烧录内核就是运行在u-boot状态执行的)
    1. 但是要注意,这里的设备树会按照设备树中节点的属性(即要不要被u-boot加载)从而进行精简

如果一切都执行成功,那么board_init_f中的hang()不会执行,则顺利返回调用该函数的汇编文件crt0.S那里,继续向下执行

继续向下执行,将栈指针进行8字节对齐,并且读取到global_data的指针

通过global_data中的数据,保存relocaddr到r0作为参数到relocate_code标号进行执行

也就是board.c中的这个函数

这个函数负责根据前面计算好的地址,把u-boot搬运到新地址(即靠近顶端的relocaddr)开始执行

这里要进行一次搬运是因为之前spl为了追求极小的体积,并不会添加复杂的内存容量动态探测与切分算法 ,其搬运u-boot到ddr的地址是一个所有ddr内存都绝对存在的、安全的固定物理地址

一般配置的都是CONFIG_SYS_TEXT_BASE=0x87800000这个低地址

然后在新的内存中,执行board_init_r

这个u-boot主体的board_init_r函数

最主要的还是initcall_run_list()中的内容

主要做了如下内容:

  1. 建立页表,开启MMU与D-Cache和I-Cache(也就意味着指令不会再被修改和重定位了)
  2. 初始化堆空间,让malloc可用
  3. 初始化存储介质并且从其中读取内核启动的环境变量
  4. 将一些通用外设的驱动进行加载,比如网卡,键盘,串口等
  5. 启动轻量级网络协议栈(比如用网卡烧写内核就是在u-boot执行的)
    最后进入run_main_loop函数,开启一个轻量级的终端,可以修改一些环境变量用于启动内核的时候传入
    没交互的话等待Hit any key to stop autoboot结束就会启动内核了

速记总结

bash 复制代码
[阶段 0: BootROM 固化代码]
   │  • 纯硬件探测(引脚/拨码开关),低速单向读取
   ▼
[阶段 1: SPL 阶段 (运行于 片内 SRAM)]
   │  • start.S -> crt0.S: 建立 SRAM 栈与向量表
   │  • board_init_f: 点亮串口、点亮 DDR (spl_dram_init)
   │  • board_init_r: 根据 board_boot_order 从介质加载 u-boot.bin 到 DDR (0x87800000)
   ▼ (jump_to_image_no_args 跳转到 DDR 初始地址)
[阶段 2: U-Boot Proper 前半段 (运行于 DDR 低/中地址 0x87800000)]
   │  • start.S (第2次): 关中断/MMU/Cache,重置硬件状态
   │  • crt0.S  (第2次): 借用 SRAM (CONFIG_SYS_INIT_SP_ADDR) 搭建临时栈与临时 gd
   │  • board_init_f: 探明 DDR 容量,自顶向下规划 MMU/代码段/堆/栈/设备树
   │  • relocate_code: 搬运代码到 DDR 顶端,修正 .rel.dyn 符号表,更新 VBAR
   ▼ (切换 SP 到 DDR 顶端新栈)
[阶段 3: U-Boot Proper 后半段 (运行于 DDR 物理顶端)]
   │  • board_init_r: 开启 MMU/D-Cache,注册 stdio,重载 MMC/网卡驱动与环境变量,用设备树和驱动注册设备
   │  • run_main_loop: 倒计时 / 交互终端 / 准备内核镜像与 DTB
   ▼ (关中断、刷 Cache、关 MMU,r0=0, r2=DTB 物理地址)
[阶段 4: Linux Kernel]
   └─► zImage 自解压 -> head.S 校验建页表 -> start_kernel()
相关推荐
祖力552 小时前
快速学会数据结构中的链式队列与循环队列
linux·c语言·数据结构·算法
原凉是这样的丶2 小时前
Qwen3.8-27B 开源后,我用双 RTX 4090 跑通了:vLLM 0.27.1、128K 上下文与 MTP 实测
linux·算法
北京迅为3 小时前
【迅为开发板专属工具】把烧写入口放进浏览器|Topeet RK Flash
linux·人工智能·嵌入式·rk3568·烧写
xiebingsuccess3 小时前
Ubuntu 22.04 在 VMware 上的安装与远程桌面配置 — 任务报告
linux·运维·ubuntu·ai+嵌入式开发
程序员AlbertTu3 小时前
Linux 系统 Bug 调试操作手册
linux·postgresql·bug
juesdo3 小时前
vulnos OS-00118靶场通关
linux·web安全·网络安全·docker
霸刀3 小时前
nslookup 与 dig 使用指南:DNS 查询、区别对比及真实案例解读
linux·cdn·dns
郝亚军3 小时前
asqlite-autoconf-3310100 的ubuntu 22.04 aarch64交叉编译
linux·运维·ubuntu