因为整个启动过程会两次运行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 位设置为0x13(0x13代表 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_V 将 V 位置 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列表中的函数,里面的函数非常多
大体总结一下做的事情
- 微调一下cpu的配置
- 探明 DDR 总容量,从 DDR 最顶端开始向下把 MMU、FrameBuffer、异常向量表、U-Boot 代码段、堆区、
gd、新栈的空间全部规划好(并不分配写数据,只是预留) - 给u-boot复制一份设备树,并且给设备驱动预留malloc用的空间,这样u-boot就可以使用一些设备树中的设备了(比如海思用网口烧录内核就是运行在u-boot状态执行的)
- 但是要注意,这里的设备树会按照设备树中节点的属性(即要不要被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()中的内容
主要做了如下内容:
- 建立页表,开启MMU与D-Cache和I-Cache(也就意味着指令不会再被修改和重定位了)
- 初始化堆空间,让malloc可用
- 初始化存储介质并且从其中读取内核启动的环境变量
- 将一些通用外设的驱动进行加载,比如网卡,键盘,串口等
- 启动轻量级网络协议栈(比如用网卡烧写内核就是在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()