4. U-Boot / Little Kernel (LK) 启动

在 Android 系统的启动链中,U-Boot 或 Little Kernel (LK) 作为 Bootloader 的第二阶段,承担着硬件初始化、内存布局建立以及最关键的任务——加载并传递设备树(Device Tree)给后续的 Linux 内核。本章将深入剖析 U-Boot 与 LK 的入口逻辑、设备树的加载机制以及传递协议。

4.1 Bootloader 第二阶段入口分析

无论是 U-Boot 还是 LK,其入口点通常由芯片厂商的 ROM Code 或 SPL (Secondary Program Loader) 跳转而来。入口代码通常使用汇编语言编写,负责设置异常向量表、初始化栈指针以及清理 BSS 段。

4.1.1 U-Boot 入口流程

U-Boot 的入口通常位于 arch/arm/lib/vectors.S 或 arch/arm/cpu/armv8/start.S。核心步骤包括:

  1. 设置异常向量表:将向量表基址写入 VBAR 寄存器。
  2. 进入 SVC 模式:确保 CPU 处于特权模式。
  3. 初始化栈指针:为 C 语言运行环境准备栈空间。
  4. 清零 BSS 段:确保全局变量初始值为 0。
  5. 跳转到 board_init_f:执行板级初始化前的准备工作。

关键入口符号: U-Boot 的链接脚本(u-boot.lds)定义了入口点为 _start,该符号位于 vectors.S 中。对于 ARMv8 架构,入口点通常为 _armv8_start。

4.1.2 LK 入口流程

LK 的入口相对简洁,通常从 arch/arm/crt0.S 或 platform/msm_shared/start.S 开始。其核心步骤与 U-Boot 类似,但更侧重于快速跳转到 C 语言主函数:

  1. CPU 模式切换:确保所有核心处于安全或非安全模式。
  2. MMU 与 TLB 初始化:部分平台在 LK 阶段即开启 MMU。
  3. 跳转到 lk_main:进入 C 语言主循环。

调试技巧: 在 GDB 中,可通过 info registers 检查 PC 是否指向 _start 或 lk_main 地址。使用 add-symbol-file 加载未剥离符号的 u-boot 或 lk.elf 文件,可进行源码级调试。

4.2 设备树 (Device Tree) 的加载机制

设备树(DTB)是描述硬件拓扑结构的数据结构。Bootloader 必须从存储介质(如 eMMC、UFS)中读取 DTB 文件,并将其放置在内存中的特定位置,以便内核能够访问。

4.2.1 DTB 的存储与加载路径

存储介质 典型分区名 加载方式 U-Boot 命令/函数
eMMC dtb 或 dtbo 通过 mmc read 读取到内存 load mmc 0:1 $fdt_addr_r dtb.img
UFS dtb 通过 ufs read 读取 ufs read $fdt_addr_r $dtb_offset $dtb_size
SPI Flash DTB 分区 通过 sf read 读取 sf read $fdt_addr_r $dtb_offset $dtb_size
RAM (预加载) 由前一阶段 BootROM 加载 直接使用固定地址 通过 fdt addr 设置工作地址

4.2.2 DTB 的校验与覆盖

现代 Android 设备支持 DTBO (Device Tree Blob Overlay) 机制。Bootloader 需要将基础 DTB 与多个 DTBO 片段合并,生成最终的设备树。

  • 基础 DTB:包含 SoC 核心硬件描述。
  • DTBO:包含板级差异(如屏幕、传感器、GPIO 配置)。
  • 合并流程:U-Boot 使用 fdt apply 命令或 fdt_overlay_apply() 函数将 DTBO 合并到基础 DTB 中。

常见问题: 如果 DTB 地址与内核解压地址重叠,会导致内核启动时解压失败。务必确保 $fdt_addr_r 与 $kernel_addr_r 不冲突。通常建议将 DTB 放置在内存末尾附近,例如 0x83000000(假设内存起始为 0x80000000,大小为 512MB)。

4.3 设备树传递协议

Bootloader 通过特定的寄存器约定将 DTB 地址传递给 Linux 内核。ARM 和 ARM64 架构的传递方式有所不同。

4.3.1 ARM32 传递协议

根据 Linux 内核的 Booting ARM Linux 文档,Bootloader 必须满足以下条件:

  • r0:必须为 0。
  • r1:机器类型编号(对于使用设备树的内核,此值被忽略,但建议设置为 ~0)。
  • r2:DTB 在内存中的物理地址(必须 4 字节对齐)。

4.3.2 ARM64 传递协议

对于 ARM64 (AArch64),协议更为严格:

  • x0:DTB 的物理地址。
  • x1 至 x3:必须为 0(保留)。
  • MMU 状态:必须关闭。
  • 缓存状态:必须禁用或刷新。

内核验证: 内核启动早期,head.S 中的 __vet_fdt 函数会检查 x0 寄存器指向的内存是否包含有效的 FDT 魔数(0xd00dfeed)。如果校验失败,内核将无法解析设备树并 panic。

4.4 启动流程与设备树传递 SVG 架构图

下图展示了从 Bootloader 入口到设备树加载、合并、传递的完整流程:

U-Boot/LK 启动与设备树传递流程 U-Boot / LK 启动与设备树传递流程 入口 (_start / lk_main) 硬件初始化 (CPU/MMU/CLK) 读取 DTB (mmc/ufs/sf) DTBO 合并 (fdt apply) 设置 x0/r2 = DTB 地址 跳转 Linux 内核 (booti/bootm) 寄存器传递细节 ARM32: r0=0, r1=~0, r2=DTB 物理地址 ARM64: x0=DTB 物理地址, x1-x3=0 要求: MMU 关闭, 缓存刷新, DTB 4 字节对齐 内核校验: 检查 FDT 魔数 0xd00dfeed 常见错误与调试 1. DTB 地址与内核解压地址重叠 → 调整 fdt_addr_r 2. DTBO 合并失败 → 检查 overlay 语法与 phandle 冲突 3. 内核 panic: "No FDT found" → 检查 x0/r2 寄存器值

4.5 关键代码片段与调试示例

4.5.1 U-Boot 中加载并传递 DTB

以下代码展示了 U-Boot 命令行中手动加载 DTB 并启动内核的典型流程:

# 1. 从 eMMC 分区加载内核与 DTB
mmc dev 0
fatload mmc 0:1 $kernel_addr_r Image
fatload mmc 0:1 $fdt_addr_r dtb.img

# 2. 可选:应用 DTBO 覆盖
fdt addr $fdt_addr_r
fdt resize 8192
fatload mmc 0:1 $dtbo_addr_r dtbo.img
fdt apply $dtbo_addr_r

# 3. 启动内核,自动传递 DTB 地址
booti $kernel_addr_r - $fdt_addr_r

4.5.2 LK 中设置 DTB 地址并跳转

LK 中通常通过 boot_linux() 函数实现跳转,关键代码片段如下:

// LK 跳转内核示例 (arch/arm/mach-msm/boot_linux.c)
void boot_linux(void *kernel, size_t kernel_size, void *dtb, size_t dtb_size) {
    // 1. 刷新缓存,确保 DTB 数据可见
    arch_clean_cache_range((addr_t)dtb, dtb_size);

    // 2. 设置内核入口参数
    struct kernel_args args;
    args.kernel = kernel;
    args.kernel_size = kernel_size;
    args.dtb = dtb;
    args.dtb_size = dtb_size;

    // 3. 调用汇编跳转函数
    // 对于 ARM64,该函数会将 args.dtb 放入 x0 寄存器
    arm64_linux_boot(&args);
}

4.5.3 GDB 调试 DTB 传递

在 GDB 中,可以通过以下命令验证 DTB 是否被正确加载到寄存器:

# 1. 在跳转内核前设置断点
(gdb) break *0x40080000  # 假设内核入口地址

# 2. 检查 ARM64 的 x0 寄存器
(gdb) info registers x0
x0            0x83000000   2197815296

# 3. 验证该地址是否包含有效的 FDT 魔数
(gdb) x/1wx 0x83000000
0x83000000:    0xd00dfeed

# 4. 如果魔数不正确,检查加载命令或分区内容
(gdb) x/4bx 0x83000000
0x83000000:    0x00    0x00    0x00    0x00  # 错误:全零,DTB 未加载

4.6 总结与最佳实践

阶段 关键动作 验证方法 常见陷阱
入口 设置栈、清零 BSS GDB 单步跟踪 _start 栈溢出导致异常
DTB 加载 从分区读取到内存 检查内存地址的 FDT 魔数 分区偏移或大小错误
DTBO 合并 应用 overlay 片段 使用 fdt list 查看节点 phandle 冲突或语法错误
寄存器传递 设置 x0/r2 断点检查寄存器值 MMU 未关闭导致地址转换错误
内核跳转 执行 booti 或 boot_linux 观察内核早期串口输出 内核与 DTB 地址重叠

工程建议: 在开发阶段,建议在 Bootloader 中添加 DTB 校验打印,输出 DTB 地址、大小以及魔数。这可以大幅减少因设备树问题导致的内核启动失败排查时间。使用 fdt header 命令可快速查看 DTB 头部信息。