深入解析 OpenWrt Chainloader 引导机制:以 AN7581 和 MT7988A 平台为例

在嵌入式 OpenWrt 设备中,引导加载程序(Bootloader)的定制和升级一直是高风险操作。传统方案需要完全替换原厂 U-Boot,一旦失败设备即变砖。Chainloader(链式加载)机制提供了一种优雅的解决方案:在不覆盖原厂 Bootloader 的前提下,通过二级引导程序加载 OpenWrt 内核,既保留了原厂救砖能力,又能享受开源 U-Boot 的新特性。

本文将结合 Airoha AN7581MediaTek MT7988A 两个平台的实际案例,系统讲解 Chainloader 的技术原理、打包方法、分区布局和刷写实践。


为什么需要 Chainloader?

原厂 Bootloader 的局限性

  • 功能缺失:不支持 FIT 镜像、HTTP 恢复、网卡驱动缺陷等

  • 闭源不透明:难以调试和扩展

  • 更新风险高:直接替换原厂 U-Boot,操作复杂,每个机型需要一套教程,且设备容易变砖

Chainloader 的核心思想

保留原厂第一阶段引导程序不变,在其基础上通过**二级引导程序(Secondary U-Boot)**接管启动流程:

1
[原厂 Bootloader] -> [加载 Chainloader U-Boot] -> [加载 OpenWrt FIT 内核镜像]

这种方案的优点:

  • 安全性:原厂 Bootloader 保留,可通过串口救砖

  • 灵活性:二级 U-Boot 可以自由定制、升级、增加功能

  • 可回滚:Chainloader 分区独立,不影响原厂分区


Chainloader 镜像打包方式

OpenWrt 标准方式:Build/an7581-chainloader 宏

这是 OpenWrt 构建系统中通用的 Chainloader 打包模板,被 AN7581 平台采用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define Build/an7581-chainloader
$(INSTALL_DIR) $(KDIR)/chainload-fit-$(notdir $@)
@if [ -f "$(STAGING_DIR_IMAGE)/an7581_$1-u-boot.lzma" ]; then \
KERNEL="$(STAGING_DIR_IMAGE)/an7581_$1-u-boot.lzma"; \
COMP="lzma"; \
else \
KERNEL="$(STAGING_DIR_IMAGE)/an7581_$1-u-boot.bin"; \
COMP="none"; \
fi; \
$(TOPDIR)/scripts/mkits.sh \
-D $(DEVICE_NAME) \
-o $(KDIR)/chainload-fit-$(notdir $@)/u-boot.its \
-k $$KERNEL \
-C $$COMP \
-a 0x80200000 -e 0x80200000 \
-c conf-uboot \
-A arm64 -v u-boot \
-d $(STAGING_DIR_IMAGE)/an7581_$1-u-boot.dtb \
-s 0x82000000
PATH=$(LINUX_DIR)/scripts/dtc:$(PATH) \
$(STAGING_DIR_HOST)/bin/mkimage \
-D "-i $(KDIR)/chainload-fit-$(notdir $@)" \
-f $(KDIR)/chainload-fit-$(notdir $@)/u-boot.its \
$(STAGING_DIR_IMAGE)/an7581_$1-chainload-u-boot.itb
cat $(STAGING_DIR_IMAGE)/an7581_$1-chainload-u-boot.itb >> $@
endef

工作流程解析:

  1. 检测压缩格式:优先使用 .lzma 压缩的 U-Boot,否则使用原始 .bin

  2. 生成 ITS 源文件:通过 mkits.sh 生成 FIT 描述文件

    • 加载地址/入口地址:0x80200000(AN7581 的 DDR 加载基址)
    • 启动脚本地址:0x82000000
    • 包含设备树 DTB
  3. 打包 FIT 镜像:使用 mkimage 将 ITS 编译为 .itb 文件

  4. 追加到内核尾部:通过 cat.itb 附加到 Linux 内核镜像末尾

最终产物结构:

1
2
3
4
5
6
7
8
9
10
+----------------------------------+
| Linux 内核镜像 (zImage) |
+----------------------------------+
| U-Boot FIT 镜像 (.itb) |
| +----------------------------+ |
| | U-Boot 二进制 (LZMA/raw) | |
| | 设备树 DTB | |
| | 启动脚本 (bootcmd) | |
| +----------------------------+ |
+----------------------------------+

这种方案的特点是 U-Boot 与内核合并在一个文件中,刷写内核分区即同时更新 U-Boot。


XR1710G 定制方案:双入口 Slot 镜像

XR1710G 的方案更加复杂,它需要兼容原厂 ECNT/AXON U-Boot 的两种启动地址:

最终产物 xr1710g-chainloader-slot.bin 的结构:

1
2
3
4
5
6
7
8
9
+---------------------------------------------------+  <-- 文件开头
| 1. Legacy Shim (chainloader-prefix-shim.uImage) | 魔数: 0x27051956 (IH_MAGIC)
| 约 8KB,用于处理无参数 bootm 启动 |
+---------------------------------------------------+ <-- 填充至 0x2100 字节
| 2. 填充字节 (Padding) | 用 0x00 填满
+---------------------------------------------------+ <-- 固定偏移 0x2100
| 3. 完整的 FIT 镜像 (.itb) | 魔数: 0xd00dfeed (FDT_MAGIC)
| 包含:控制 DTB + 真正的 u-boot.bin |
+---------------------------------------------------+

为什么是 0x2100 字节偏移?

原厂 ECNT U-Boot 环境变量中通常有两种 bootcmd

1
2
3
4
5
# 方式一:从基址读取(会加载 Legacy Shim)
flash read 0x600000 0x100000 $loadaddr; bootm

# 方式二:从偏移 0x2100 读取(直接加载 FIT)
flash read 0x602100 0x100000 $loadaddr; bootm 0x81800000

通过在文件开头放置 Shim 并在偏移 0x2100 放置真正的 FIT 镜像,无论原厂使用哪种读取方式,都能正常启动

打包脚本核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
# 1. 生成 FIT 镜像
"$mkimage" -f "$its" "$output_fit"

# 2. 复制前缀 Shim
cp "$prefix_shim" "$output_slot"

# 3. 填充至 0x2100 字节
dd if=/dev/zero bs=1 count=$((0x2100 - prefix_size)) >> "$output_slot"

# 4. 追加 FIT 镜像
cat "$output_fit" >> "$output_slot"

MT7988A 平台的 Chainloader

tplink_be805 的设备定义中,Chainloader 的生成被拆解为清晰的流水线操作,核心代码如下:

1
2
3
ARTIFACTS := second-u-boot.bin
ARTIFACT/second-u-boot.bin := mt7988-uboot-raw tplink_be805 | libdeflate-gzip | \
uImage gzip -T standalone -a 0x44000000 -e 0x44000000 -n "OpenWrt U-Boot Chainloader"

这套流水线按顺序执行了以下三步操作:

步骤命令作用
1. 提取原始载荷mt7988-uboot-raw tplink_be805这是一个自定义的构建命令,本质上是执行 cat $(STAGING_DIR_IMAGE)/mt7988_tplink_be805-u-boot.bin >> $@,即获取刚刚编译好的、未经任何包装的原始 u-boot.bin 二进制文件。
2. 压缩体积libdeflate-gzip使用高效的 libdeflate 算法对 U-Boot 二进制文件进行 Gzip 压缩。这对于 NAND 闪存空间紧张的设备至关重要,能显著减小二级引导程序占用的存储空间。
3. 添加 U-Boot 标准头uImage gzip -T standalone ...这是最关键的一步。它使用 mkimage 工具将压缩后的数据打包成一个标准的 U-Boot 镜像(uImage)。参数含义:
- -T standalone:指定镜像类型为独立应用程序(Standalone Application),这是 U-Boot 可执行 payload 的标准类型。
- -a 0x44000000:指定加载地址(Load Address),即原厂 Bootloader 需要将此镜像复制到内存的 0x44000000 处。
- -e 0x44000000:指定入口地址(Entry Point),即解压或跳转执行的起点。
- -n "OpenWrt U-Boot Chainloader":镜像描述名称。

最终,构建系统会生成一个名为 openwrt-mediatek-filogic-tplink_be805-v1-second-u-boot.bin 的独立工件(Artifact)。

2. 固件与 Chainloader 的“解耦”设计

与 XR1710G 最大的区别在于 sysupgrade.bin 固件与 second-u-boot.bin 是逻辑分离的

在 XR1710G 方案中,U-Boot 被强行合并(cat)进内核镜像或生成一个混合的 slot.bin。而在 MT7988 方案中:

  • second-u-boot.bin:作为 ARTIFACT 独立生成,单独提供给用户或刷写脚本,用于写入闪存的特定 uboot 分区(或通过 U-Boot 的 fitblk 挂载)。

  • sysupgrade.bin:仅包含 Linux 内核(通过 fit gzip ... 打包成 FIT)和根文件系统(RootFS)。注意其 KERNEL 变量定义为 kernel-bin | libdeflate-gzip并不包含任何 U-Boot 代码。

这种设计的好处是:

  1. 职责单一:Chainloader 更新与系统更新完全解耦。你可以在不重刷整个固件的情况下单独升级二级 U-Boot,反之亦然,极大地降低了变砖风险。

  2. 更符合 OpenWrt 哲学:U-Boot 作为引导程序,本就不应频繁变动。将其从系统固件中剥离,使得 sysupgrade 过程更轻量、更快速。

3. 引导链的最终形态

结合 KERNEL_IN_UBI := 1 配置(内核存放在 UBI 卷中),完整的 MT7988 启动流如下:

1
2
3
4
5
6
7
[原厂 Bootloader (BootROM)] 
-> 从 NAND 的固定偏移读取并加载 second-u-boot.bin 到 0x44000000
-> 执行 Secondary U-Boot
-> 初始化硬件(DDR、NAND、网卡)
-> 从 UBI 卷(通常是 ubi:fit)中读取 OpenWrt 内核 FIT 镜像
-> 验证并跳转到内核入口
-> Linux 挂载 rootfs 启动

分区布局设计

XR1710G 的 NAND 分区方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x00000000 +----------------------------------+
| vendor (6 MiB) | 原厂一级 U-Boot + MAC + 无线校准
| ├─ 0x00000000 bootloader (2MiB) |
| ├─ 0x00200000 uenv (2MiB) |
| └─ 0x00400000 dsd (2MiB) |
0x00600000 +----------------------------------+
| chainloader (1 MiB) | xr1710g-chainloader-slot.bin
0x00700000 +----------------------------------+
| ubi (约 503 MiB) | OpenWrt 主系统
| ├─ ubootenv / ubootenv2 |
| ├─ fit (内核 + DTB) |
| └─ rootfs_data |
0x1FE00000 +----------------------------------+
| reserved_bmt (2 MiB) | 坏块表预留
0x20000000 +----------------------------------+

关键设计思路:

  • vendor 分区只读:保存原厂 U-Boot 和硬件校准数据,是救砖的最后防线

  • chainloader 分区只读:存放二级 U-Boot,避免系统内误覆盖

两种的 Chainloader 实现对比

对比维度分离的 Chainloader整合的 Chainloader
分区独立性独立 chainloader 分区嵌入固件镜像,无独立分区
构建集成度需单独编译 U-Boot + 手动打包完全集成于 image/filogic.mk
兼容性处理双入口 Shim (0x600000 / 0x602100)通过 FIT 标准机制处理

总结

Chainloader 机制为 OpenWrt 设备提供了一种安全、灵活、可回滚的 Bootloader 定制方案。其核心价值在于:

  1. 不破坏原厂引导链,保留救砖能力

  2. 二级 U-Boot 可独立升级,不受原厂限制

  3. 支持 FIT 镜像,实现内核、DTB、启动脚本的统一管理

  4. 不同平台方案可复用,从手动脚本到主线集成逐步演进

虽然看起来 Chainloader 机制很完美,但是其实没有什么实际的必要性。因为 Airoha/MediaTek 平台具备便捷的救砖机制,而且其uboot/atf都存在开源实现。

但是我们终于可以不用去备份某些数据了(例如 MAC 地址、无线校准数据等等)。如果原厂的引导支持恢复模式,那么 Chainloader 机制才真正有意义。