Lazy loaded image
Words 0Read Time 1 min
Invalid Date
notion image
U-Boot 初始化硬件、分配内存后,将控制权交给 Linux 内核。

引导加载与内核启动的分层架构

U-Boot 作为独立引导加载程序,具备完全自主执行能力,无需外部干预即可完成初始化。Linux 内核则必须依赖 U-Boot 提供的预配置环境才能正常运行。
U-Boot 承担硬件抽象层的关键职责,核心任务如下:
⚙️
硬件初始化与寄存器配置 — 直接与底层硬件交互,完成 CPU、内存控制器、时钟树、GPIO 等关键组件的寄存器配置,确保硬件平台处于可用状态。
📦
内存重定位服务 — 将 Linux 内核镜像从存储设备(NAND Flash、eMMC 等)加载到 DRAM 指定地址空间,处理内核镜像的重定位工作,解决地址依赖问题。
🔧
系统环境准备 — 配置 MMU 基础映射关系、设置栈指针、准备设备树(Device Tree)等运行时环境。
📤
启动参数传递 — 通过标准化参数传递机制(ARM 架构寄存器约定),将启动参数、设备树地址、机器 ID 等关键信息传递给 Linux 内核。
所有准备工作完成后,U-Boot 通过跳转指令将 CPU 控制权移交给 Linux 内核。地址 0x80800000 是控制权交接的关键节点 — Linux 内核入口点(Entry Point),标志从引导加载阶段向操作系统内核执行阶段的完整过渡。
这种分层设计体现了嵌入式系统的模块化思想:U-Boot 专注硬件抽象和启动服务,Linux 内核专注操作系统功能实现,两者通过标准化接口实现有序的控制权移交。

从哪找内核镜像加载到 DDR

imx6ull EVK pro 开发板支持 3 种启动方式:
  1. NAND / EMMC 启动
  1. SD 卡启动
  1. USB 启动
此外,U-Boot 还支持网络启动:直接从远程服务器下载镜像到本地 DDR 运行,并可挂载根文件系统到远程服务器。

Linux 内核启动源码

bootz 函数

U-Boot 执行 bootcmd 命令后,最终调用 do_bootz 函数启动 Linux 内核。

UBI_BOOT 启动脚本

脚本关键流程:检查 uEnv.txt → 加载环境变量 → 设置设备树 → 最终执行 bootz ${loadaddr} ${rdaddr}:${rdsize} ${fdtaddr} 启动内核。

bootz 命令注册


do_bootz 函数

核心步骤:
  1. bootz_start — 设置并找到 Linux 镜像入口点
  1. bootm_disable_interrupts — 加载内核前必须关闭中断
    1. 为什么加载内核前必须关闭中断?
      此时 U-Boot 即将把 CPU 控制权移交给 Linux 内核,中断处理的"主权"正在交接。如果不关闭中断,会导致以下问题:
      1. 中断向量表失效 — U-Boot 的中断向量表(VBAR 指向 0x9FE70000)即将被废弃,而 Linux 内核尚未建立自己的中断向量表。若此时触发中断,CPU 会跳转到一个即将无效的处理程序,导致系统崩溃。
      1. 中断处理程序上下文不一致 — U-Boot 的中断处理程序依赖 U-Boot 的栈、GD、驱动模型等运行时环境。一旦开始执行内核代码,这些环境将被内核覆盖或重新初始化,中断处理程序的执行上下文不再安全。
      1. 内存布局被破坏 — 内核加载过程涉及设备树重定位(boot_relocate_fdt)、内存预留(lmb_reserve)等操作,中断处理程序可能读写正在被修改的内存区域,造成数据竞争。
      1. Linux 内核的硬性要求 — ARM Linux 启动协议(Documentation/arm/booting.rst)明确要求:进入内核入口时 IRQ 和 FIQ 必须处于禁止状态。内核会在自身初始化完成后,建立好自己的中断向量表和处理框架,再主动使能中断。
      🔄
      与第二阶段打开中断的对比
      第二阶段通过 interrupt_init + initr_enable_interrupts 使能中断,原因完全不同:
      • 第二阶段需要中断:U-Boot 的 main_loop 需要响应定时器中断(用于 bootdelay 倒计时超时检测)、网络中断(TFTP/NFS 启动依赖网络收发)、串口中断(控制台输入响应)等。此时 U-Boot 完全掌控中断向量表、处理程序和运行时环境,中断处理是安全的。
      • 内核加载前关闭中断:U-Boot 使命即将结束,中断的"所有权"要移交给内核。关闭中断是将硬件状态"清零"交还给内核的一部分,与 cleanup_before_linux(关闭/清空 cache)属于同一类"交接清理"动作。
      本质:第二阶段开中断是因为 U-Boot 自身需要异步事件驱动;加载内核前关中断是因为中断基础设施的"主权"正在从 U-Boot 移交给 Linux 内核,过渡期间必须静默。
  1. do_bootm_states — 执行状态机完成内核引导

bootz_start 函数

bootz_start 的作用是先通过 do_bootm_states(..., BOOTM_STATE_START) 初始化 bootm 流程,再确定 zImage 入口地址 images->ep(默认 load_addr 或由参数解析得到如 0x80800000),调用 bootz_setup 解析 zImage 起止范围并用 lmb_reserve 预留该内存区,随后 bootm_find_images 继续定位/准备 ramdisk 与 FDT 等镜像组件,最后在开启 Secure Boot 时对 zImage 做鉴权,任一步失败即返回错误终止启动。

do_bootm_states 函数(状态机)

镜像执行状态流转:BOOTM_STATE_STARTBOOTM_STATE_RAMDISKBOOTM_STATE_OS_PREPBOOTM_STATE_OS_FAKE_GOBOOTM_STATE_OS_GO
状态流转含义(把 bootm 状态机按“准备 → 校验 → 交接”拆开理解)
  1. BOOTM_STATE_START:启动 bootm 流程的“总入口”。解析参数并初始化 images 结构,建立 LMB 内存管理上下文,为后续加载、预留、重定位做准备。
  1. BOOTM_STATE_RAMDISK:处理 initramfs / ramdisk(如果传入)。包括定位 ramdisk、必要的搬移,以及在 LMB 中预留其占用的内存区,避免与内核、FDT 发生覆盖。
    1. 📦
      initramfs / ramdisk 是什么?
      它们都是 内存中的临时根文件系统,在 Linux 内核启动早期、真正的根文件系统(eMMC/NAND/NFS 上的 rootfs)尚未挂载时,提供一个最小化的运行环境。
      为什么需要它?
      内核启动后需要挂载根文件系统,但挂载根文件系统本身可能需要驱动(如 eMMC 控制器驱动、文件系统模块、LVM/加密层等),而这些驱动可能是以内核模块形式存在的,存放在根文件系统上 — 这就形成了“鸡生蛋、蛋生鸡”的死循环。initramfs/ramdisk 就是打破这个循环的“过渡桥”。
      两者的区别:
      • ramdisk(传统方式):一个压缩的文件系统镜像(通常是 ext2 格式),由 U-Boot 加载到 DDR 的指定地址(rdaddr)。内核启动时将其解压到一个 固定大小的 RAM 块设备/dev/ram0)中,作为临时根文件系统挂载。占用双份内存(压缩镜像 + 解压后的块设备),且大小固定不可动态调整。
      • initramfs(现代方式,Linux 2.6+):一个 cpio 格式的归档,可以嵌入内核镜像或由 U-Boot 单独加载。内核启动时将其解压到 tmpfs(基于页缓存的内存文件系统)中,不需要块设备抽象层。内存利用率更高(无双份占用),大小可动态伸缩。
      在 U-Boot 中的体现:
      bootz ${loadaddr} ${rdaddr}:${rdsize} ${fdtaddr} 中的第二个参数 ${rdaddr}:${rdsize} 就是 ramdisk/initramfs 的加载地址和大小。如果不使用 ramdisk,传 - 即可:bootz ${loadaddr} - ${fdtaddr}
      典型使用场景:
      • 挂载根文件系统需要特殊驱动(LVM、RAID、加密磁盘、网络启动等)
      • 嵌入式救援/恢复系统
      • 完全无存储设备的系统(如网络启动的无盘节点)
      • 对于 i.MX6ULL 开发板,如果 rootfs 直接在 eMMC/SD 分区上且内核已内建对应驱动,则可不使用 ramdisk,直接通过 root=/dev/mmcblk1p2 挂载根文件系统。
  1. BOOTM_STATE_OS_PREP:OS 启动前准备阶段。对 Linux 来说主要是 准备启动参数与设备树,例如设置 bootargs/chosen,为内存、网卡等节点做 fixup,必要时重定位 FDT,并在 LMB 中预留 FDT 相关区域。
  1. BOOTM_STATE_OS_FAKE_GO:一次“模拟 go”的过渡态。用于在真正跳转前完成收尾检查或打印信息,并触发一些与 GO 同路径的准备逻辑,但不真正把控制权交出去,便于在同一套框架里兼容不同镜像类型和启动路径。
  1. BOOTM_STATE_OS_GO:真正的交接点。执行 boot_selected_os,走到 boot_jump_linux,完成 cleanup_before_linux(关中断、清 cache 等),然后调用 kernel_entry(0, machid, fdt_addr) 跳入内核入口,U-Boot 使命结束。
 

do_bootm_linux 函数(核心引导流程)


关键要点

🔑
kernel_entry — 函数指针指向内存中 Linux 镜像的入口地址,即 Linux 第一条执行代码的位置。
启动标志 — 内核正常启动时打印 "Starting kernel ..." 。若未出现此字符串,应检查传参是否正确。
📋
ARM 传参约定(ATPCS)
  • r0 = 0
  • r1 = machid(机器 ID,告知内核使用哪个 CPU,调用对应初始化函数)
  • r2 = 设备树首地址(FDT 方式,非传统 ATAG 方式)

总结

  1. 加载镜像 — 根据启动方式,从存储介质加载内核到 DDR
  1. 校验镜像 — 检查、校验镜像类型
  1. 传递参数 — 通过寄存器传递启动参数给内核
  1. 启动内核 — 跳转到入口点,运行 Linux 内核

U-Boot 完整启动流程总结

以下从全局视角梳理 Boot ROM → U-Boot 第一阶段 → U-Boot 第二阶段 → 内核加载 四个阶段的职责、关键动作与衔接关系。

四阶段职责一览

阶段
执行位置
核心目标
结束标志
Boot ROM
片内 ROM
确定启动介质、执行 DCD 初始化 DDR、将 U-Boot 拷贝到 DDR
跳转到 _start
第一阶段 board_init_f
DDR 低地址 (0x87800000),栈/GD 在 OCRAM
CPU 早期配置、DDR 探测、内存规划、代码重定位到 DDR 高地址
跳转到 board_init_r
第二阶段 board_init_r
DDR 高地址 (0x9FE70000),完整 malloc/DM 可用
DM 驱动初始化、eMMC/NAND/网络/控制台/中断初始化、环境变量加载
进入 main_loop
内核加载
DDR 高地址(U-Boot 侧)→ DDR 0x80800000(内核侧)
从存储介质加载内核镜像到 DDR、校验、传参、跳转启动 Linux
kernel_entry(0, machid, fdt_addr)

完整执行流程

关键地址速查(i.MX6ULL, 512MB DDR)

地址 / 符号
说明
DDR 起始
0x80000000
DRAM 基地址
U-Boot 加载地址
0x87800000
Boot ROM 拷贝目标 (__image_copy_start)
Linux 内核入口
0x80800000
kernel_entry,zImage 加载地址
OCRAM 临时栈
0x0091FF00
第一阶段 CONFIG_SYS_INIT_SP_ADDR
U-Boot 重定位地址
0x9FE70000
gd->relocaddr,第二阶段执行位置
DDR 顶端
0xA0000000
gd->ram_top (512MB)
Linux 可用区域
0x80000000 – ~0x9EE6BFEF
~494MB 连续内存

阶段间的衔接机制

🔗
Boot ROM → 第一阶段:Boot ROM 通过 IVT 中记录的入口地址跳转到 _start,此时 DDR 已由 DCD 初始化完成,U-Boot 镜像已在 DDR 低地址就位。
第一阶段 → 第二阶段_main 完成重定位后,通过 bl board_init_r 跳转。此时代码已在 DDR 高地址运行,栈/GD 已切换到 DDR,BSS 已清零,C 运行时完整可用。
第二阶段 → 内核main_loop 解析 bootcmd,执行 bootz 命令。经过镜像校验、设备树处理、中断关闭、cache 清理后,通过 kernel_entry(0, machid, fdt_addr) 将 CPU 控制权移交给 Linux 内核,U-Boot 使命结束。
 
上一篇
Data Structure and Algorithm
下一篇
用面试拷问嵌入式技术栈

Comments
Loading...