3、Boot分区详解:内核与ramdisk结构、dtb/dtbo集成方式、boot.img打包与解包工具

好,咱们今天来啃一块硬骨头——Boot分区。

说实话,Boot分区是AOSP定制里最容易翻车的地方之一。我早期做项目时,就因为在dtbo上少打了个补丁,导致设备死活起不来,折腾了两天才找到原因。嗯,咱们今天就把这块彻底讲透。

3.1 Boot分区整体结构

先看一张我手绘的结构图(你脑子里想象一下):

+-----------------------+
|   Boot Header         |  <-- 固定大小,描述分区信息
+-----------------------+
|   Kernel (zImage)     |  <-- 压缩后的Linux内核
+-----------------------+
|   Ramdisk (initrd)    |  <-- 根文件系统,包含init进程
+-----------------------+
|   Second Stage        |  <-- 可选,某些平台用
+-----------------------+
|   DTB / DTBO          |  <-- 设备树,硬件描述信息
+-----------------------+
|   Recovery DTBO       |  <-- 仅recovery模式使用
+-----------------------+

说白了,boot.img就是把上面这些东西打包成一个文件。你刷机时刷的就是它。

我个人习惯把Boot分区比作「手机的起跑线」。内核是发动机,ramdisk是驾驶员,dtb是地图。任何一个出问题,车都开不动。

3.2 内核与ramdisk:谁先谁后?

启动流程是这样的:

  1. Bootloader加载boot.img到内存
  2. 解析Boot Header,找到内核起始地址
  3. 解压内核,跳转到内核入口
  4. 内核挂载ramdisk作为根文件系统
  5. 执行init进程,启动用户空间

这里有个关键点:ramdisk是临时文件系统。内核启动后,会从ramdisk里加载驱动、挂载真正的system分区,然后切换到system分区继续启动。

重要:ramdisk里必须包含init程序,否则内核启动后会直接panic。我见过有人把ramdisk删空了,结果手机变砖。

ramdisk的典型内容:

ramdisk/
├── init
├── init.rc
├── system/
├── data/
├── proc/
├── sys/
├── dev/
└── sbin/
    ├── adbd
    └── recovery

你想想看,init.rc里定义了服务启动顺序,比如zygote、surfaceflinger这些。如果ramdisk里少了某个关键服务,系统就卡在开机动画了。

3.3 DTB与DTBO:硬件描述的艺术

设备树(Device Tree)是Linux内核用来描述硬件信息的机制。说白了,就是告诉内核:「我这块板子上有哪些外设,地址是多少,中断号是多少」。

Android里分两种:

  • DTB (Device Tree Blob):编译后的设备树,直接嵌入到boot.img里
  • DTBO (Device Tree Blob Overlay):叠加层,用于支持多个硬件变体

为什么要搞DTBO?

举个例子:同一款手机有高配版和低配版,摄像头模组不同。如果每个版本都编译一个完整的boot.img,维护成本太高。DTBO允许你只修改叠加层,内核保持不变。

我的经验:在MTK平台上,DTBO经常用来区分不同屏幕面板。我曾经因为DTBO里少配了一个timing参数,导致屏幕闪烁。排查了三天才发现是设备树的问题。

DTB/DTBO的集成方式:

方式 位置 适用场景
内嵌到内核 内核镜像内部 单设备、调试阶段
附加到boot.img 内核之后、ramdisk之前 通用方案,兼容性好
独立DTBO分区 单独分区(如dtbo_a、dtbo_b) 多硬件变体、AB分区

我个人推荐第三种方式。为什么呢?因为独立分区方便OTA升级时只更新设备树,不用动内核。你想想看,如果只是换了个传感器,却要刷整个boot.img,多浪费流量。

3.4 boot.img打包与解包工具

工欲善其事,必先利其器。咱们来看看常用的工具。

3.4.1 mkbootimg:打包工具

这是AOSP自带的工具,源码在 system/tools/mkbootimg/ 下。

基本用法:

mkbootimg \
  --kernel zImage \
  --ramdisk ramdisk.img \
  --dtb dtb.img \
  --base 0x80000000 \
  --pagesize 2048 \
  --cmdline "console=ttyS0,115200" \
  --output boot.img

参数说明:

  • --kernel:压缩后的内核镜像
  • --ramdisk:ramdisk镜像(cpio格式压缩)
  • --dtb:设备树文件
  • --base:内存基地址,不同平台不一样
  • --pagesize:页大小,通常是2048或4096
  • --cmdline:内核启动参数

注意:base地址不能乱填。我见过有人把高通的base地址套用到MTK上,结果内核解压失败。每个SoC厂商都有自己的内存布局,一定要查datasheet。

3.4.2 unpackbootimg:解包工具

解包同样简单:

unpackbootimg -i boot.img -o output_dir/

解包后你会得到:

  • boot.img-zImage:内核
  • boot.img-ramdisk.gz:压缩的ramdisk
  • boot.img-dtb:设备树
  • boot.img-cmdline:启动参数
  • boot.img-base:基地址

ramdisk需要进一步解压:

gunzip boot.img-ramdisk.gz
mkdir ramdisk && cd ramdisk
cpio -idmv < ../boot.img-ramdisk

这样你就能看到ramdisk里的所有文件了。

3.4.3 第三方工具:AIK (Android Image Kitchen)

如果你觉得命令行太麻烦,可以用AIK。它提供了图形化界面,一键解包/打包。

但我个人还是推荐命令行。为什么呢?因为自动化脚本里你不可能点鼠标。CI/CD流水线里,一行命令搞定才是王道。

3.5 实战:定制一个boot.img

假设我们要修改ramdisk里的init.rc,添加一个自定义服务。

步骤:

  1. 解包:unpackbootimg -i boot.img -o my_boot/
  2. 解压ramdisk:cd my_boot && gunzip boot.img-ramdisk.gz && mkdir ramdisk && cd ramdisk && cpio -idmv < ../boot.img-ramdisk
  3. 修改init.rc:在文件末尾添加你的服务定义
  4. 重新打包ramdisk:find . | cpio -o -H newc | gzip > ../new_ramdisk.img
  5. 重新打包boot.img:mkbootimg --kernel boot.img-zImage --ramdisk new_ramdisk.img --dtb boot.img-dtb --base $(cat boot.img-base) --pagesize $(cat boot.img-pagesize) --cmdline "$(cat boot.img-cmdline)" --output new_boot.img

避坑指南:我曾经在重新打包时忘了指定pagesize,结果刷进去后内核直接崩溃。pagesize必须和原boot.img一致,否则内存对齐会出问题。

3.6 常见问题与排查

我整理了几个高频翻车点:

  • 内核panic:检查cmdline里的console参数,确保串口输出能看到日志
  • ramdisk挂载失败:检查ramdisk格式,必须是gzip压缩的cpio
  • 设备树不匹配:用dtc -I dtb -O dts dtb.img反编译,确认硬件信息正确
  • 启动卡在logo:大概率是init.rc里某个服务挂了,用adb logcat看日志

嗯,Boot分区的内容就这些。说白了,它就是内核+ramdisk+设备树的组合体。掌握了打包解包工具,你就能随心所欲地定制启动流程。

下一章咱们聊System分区,那是Android系统的核心所在。