深度解析:从A/B分区设备的boot.img中精准提取recovery.img的技术实践
Essential PH-1这类采用A/B分区的设备,其系统更新机制与传统Android设备有着本质区别。最显著的特征就是recovery分区不再独立存在,而是被整合到了boot.img中。这种设计虽然提升了系统更新的可靠性和无缝体验,却给需要提取recovery.img进行定制开发的用户带来了新的挑战。本文将详细剖析这一技术实现原理,并提供一个完整的解决方案。
1. A/B分区机制与recovery整合的技术背景
现代Android设备采用A/B分区设计的主要目的是实现无缝系统更新(Seamless Updates)。在这种机制下,设备会维护两套完整的系统分区(A和B),当更新系统时,会在后台更新非活动分区,用户无需长时间等待更新完成,只需简单重启即可切换到新系统。
这种设计带来的关键变化包括:
- recovery功能被整合到boot分区中,不再需要独立的recovery分区
- 系统更新过程更加可靠,即使更新失败也能回退到原系统
- 减少了用户可见的更新等待时间
在传统的非A/B分区设备中,recovery.img是一个独立的分区镜像,包含了完整的恢复环境和工具。而在A/B分区设备中,恢复功能被实现为boot.img中ramdisk的一部分,通常被称为"recovery-as-boot"模式。
A/B分区与传统分区的关键差异对比:
| 特性 | A/B分区设备 | 传统分区设备 |
|---|---|---|
| recovery位置 | 整合到boot.img的ramdisk中 | 独立recovery分区 |
| 系统更新方式 | 后台更新非活动分区 | 直接更新当前分区 |
| 更新失败处理 | 自动回退到原系统 | 可能导致系统无法启动 |
| 用户影响 | 几乎无感知 | 需要等待更新完成 |
理解这一技术背景对于后续正确提取recovery.img至关重要。开发者需要明确,在A/B分区设备上寻找独立的recovery.img是徒劳的,必须从boot.img入手。
2. 获取boot.img的完整流程
要从设备获取boot.img,有几种不同的方法,具体取决于你的设备和可用的资源。以下是三种常见的获取方式及其详细步骤:
2.1 从官方固件包提取
大多数厂商会提供完整的固件包,其中包含boot.img。以Essential PH-1为例:
- 下载官方固件包(通常为.zip格式)
- 解压后查找payload.bin文件
- 使用payload-dumper工具提取boot.img:
git clone https://github.com/vm03/payload_dumper cd payload_dumper pip install -r requirements.txt python payload_dumper.py payload.bin提取完成后,你会在output目录中找到boot.img。
2.2 从设备直接dump
如果你已经root了设备,可以直接从设备上dump出boot分区:
adb shell su dd if=/dev/block/bootdevice/by-name/boot_a of=/sdcard/boot.img exit adb pull /sdcard/boot.img2.3 从LineageOS等第三方ROM提取
如果你正在编译LineageOS,可以从构建输出中获取boot.img:
cd $OUT ls boot.img无论采用哪种方式获取boot.img,都建议在操作前验证文件的完整性:
file boot.img # 应显示为Android bootimg ls -lh boot.img # 检查文件大小是否合理3. 解析boot.img结构:深入理解Android启动镜像格式
Android的boot.img是一个结构化的二进制文件,遵循特定的格式标准。要准确提取其中的recovery部分,必须首先理解其内部结构。
3.1 boot.img头部结构
boot.img的开头是一个固定大小的头部,包含了加载内核和ramdisk所需的所有信息。我们可以通过分析system/core/mkbootimg/include/bootimg/bootimg.h头文件来理解其结构:
struct boot_img_hdr_v0 { uint8_t magic[BOOT_MAGIC_SIZE]; // "ANDROID!" uint32_t kernel_size; // 内核大小 uint32_t kernel_addr; // 内核加载地址 uint32_t ramdisk_size; // ramdisk大小 uint32_t ramdisk_addr; // ramdisk加载地址 uint32_t second_size; // 第二阶段加载器大小 uint32_t second_addr; // 第二阶段加载器地址 uint32_t tags_addr; // 内核标签地址 uint32_t page_size; // 页大小(通常为4096) uint32_t header_version; // 头部版本 uint32_t os_version; // OS版本信息 uint8_t name[BOOT_NAME_SIZE]; // 产品名称 uint8_t cmdline[BOOT_ARGS_SIZE]; // 内核命令行 uint32_t id[8]; // 唯一标识 uint8_t extra_cmdline[BOOT_EXTRA_ARGS_SIZE]; // 额外命令行 };3.2 使用hexdump分析实际boot.img
我们可以使用hexdump工具查看boot.img的实际内容:
hexdump -C -n 64 boot.img输出示例:
00000000 41 4e 44 52 4f 49 44 21 a4 1c f2 00 00 80 00 00 |ANDROID!........| 00000010 1a 1c cc 00 00 00 20 02 00 00 00 00 00 00 f0 00 |...... .........| 00000020 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|从输出中可以解析出:
- 魔数:41 4E 44 52 4F 49 44 21 ("ANDROID!")
- 内核大小:a4 1c f2 00 (小端,0x00f21ca4 = 15867044字节)
- 内核地址:00 80 00 00 (0x00008000)
- ramdisk大小:1a 1c cc 00 (0x00cc1c1a = 13382682字节)
- ramdisk地址:00 00 20 02 (0x02200000)
3.3 计算ramdisk的实际偏移
根据boot.img的格式规范,各部分内容都是按页大小对齐的。通常页大小为4096字节(0x1000)。计算ramdisk偏移的步骤如下:
- 头部占用1页:4096字节
- 内核大小:15867044字节
- 计算页数:ceil(15867044 / 4096) = 3875页
- ramdisk偏移 = 头部 + 内核 = 4096 + 15867044 = 15871140字节
- 十六进制:0x00F23000
可以使用以下命令验证:
printf "0x%X\n" $(( (15867044 + 4096 + 4095) / 4096 * 4096 ))4. 精确提取ramdisk中的recovery内容
理解了boot.img结构后,我们可以准确提取包含recovery的ramdisk部分。
4.1 使用dd提取ramdisk
根据前面计算的偏移量,使用dd命令提取ramdisk:
dd if=boot.img of=ramdisk-recovery.img bs=4096 skip=3875参数说明:
- if=boot.img:输入文件
- of=ramdisk-recovery.img:输出文件
- bs=4096:块大小为4096字节(1页)
- skip=3875:跳过3875个块(头部1块 + 内核3874块)
4.2 验证提取的ramdisk
提取完成后,应该验证文件的有效性:
file ramdisk-recovery.img预期输出类似于:
ramdisk-recovery.img: gzip compressed data, from Unix如果输出不符合预期,可能是偏移计算错误,需要重新检查。
5. 解压和处理ramdisk内容
提取出的ramdisk-recovery.img实际上是经过gzip压缩的cpio归档文件,需要进一步处理才能得到可用的文件系统。
5.1 解压gzip压缩
首先重命名文件以反映其实际格式,然后解压:
mv ramdisk-recovery.img ramdisk-recovery.gz gunzip -v ramdisk-recovery.gz解压后会得到ramdisk-recovery文件。
5.2 识别解压后的格式
再次使用file命令确认文件类型:
file ramdisk-recovery预期输出:
ramdisk-recovery: ASCII cpio archive (SVR4 with no CRC)5.3 提取cpio归档内容
创建一个目录用于存放提取的文件,然后使用cpio解包:
mkdir recovery cd recovery cpio -idv < ../ramdisk-recovery成功执行后,当前目录会包含recovery环境的完整文件结构。
5.4 验证提取结果
检查提取的文件是否完整:
ls -l应该能看到典型的Android根目录结构,包括:
- init:初始化程序
- init.rc:初始化脚本
- sbin/:系统工具目录
- etc/:配置文件目录
- system/:系统目录链接
6. 常见问题与解决方案
在实际操作过程中,可能会遇到各种问题。以下是几个常见问题及其解决方法:
6.1 提取的ramdisk无法解压
症状:执行gunzip时报告"not in gzip format"错误。
可能原因:
- 偏移计算错误,提取的内容不正确
- 设备使用了非标准的boot.img格式
解决方案:
- 重新验证偏移计算
- 尝试不同的偏移量(前后调整几页)
- 检查设备是否使用了特殊的压缩或加密方式
6.2 cpio提取失败
症状:cpio命令报告"no cpio magic"或类似错误。
可能原因:
- ramdisk可能使用了其他归档格式(如Android的new format)
- 文件在解压过程中损坏
解决方案:
- 尝试使用
file命令确认实际格式 - 对于Android新的格式,可能需要使用
magiskboot工具处理 - 重新从原始boot.img开始流程
6.3 提取的文件不完整
症状:提取后的文件系统中缺少关键组件(如init二进制)。
可能原因:
- 设备使用了双层ramdisk结构
- 某些文件被压缩或打包在子归档中
解决方案:
- 检查init是否可能是符号链接
- 查找是否有额外的cpio或压缩文件需要处理
- 参考设备特定的文档或开发者资源
7. 自动化脚本实现
为了提高效率,可以将上述流程编写为自动化脚本。以下是一个示例脚本:
#!/bin/bash # 参数检查 if [ $# -ne 1 ]; then echo "Usage: $0 <boot.img>" exit 1 fi BOOTIMG=$1 OUTDIR=recovery_extracted # 解析boot.img头部 HEADER=$(hexdump -n 32 -e '8/1 "%02X ""\n"' $BOOTIMG) MAGIC=$(echo $HEADER | awk '{print $1$2$3$4$5$6$7$8}') if [ "$MAGIC" != "414E44524F494421" ]; then echo "Invalid boot.img magic: $MAGIC" exit 1 fi # 提取内核和ramdisk大小 KERNEL_SIZE=$((0x$(echo $HEADER | awk '{print $9$10$11$12}' | sed 's/\(..\)\(..\)\(..\)\(..\)/\4\3\2\1/'))) RAMDISK_SIZE=$((0x$(echo $HEADER | awk '{print $13$14$15$16}' | sed 's/\(..\)\(..\)\(..\)\(..\)/\4\3\2\1/'))) # 计算ramdisk偏移 PAGE_SIZE=4096 HEADER_PAGES=1 KERNEL_PAGES=$(( ($KERNEL_SIZE + $PAGE_SIZE - 1) / $PAGE_SIZE )) RAMDISK_OFFSET=$(( ($HEADER_PAGES + $KERNEL_PAGES) * $PAGE_SIZE )) # 提取ramdisk echo "Extracting ramdisk from offset $RAMDISK_OFFSET..." dd if=$BOOTIMG of=ramdisk-recovery.img bs=$PAGE_SIZE skip=$((RAMDISK_OFFSET/PAGE_SIZE)) 2>/dev/null # 解压处理 echo "Processing ramdisk..." mkdir -p $OUTDIR ( mv ramdisk-recovery.img ramdisk-recovery.gz gunzip ramdisk-recovery.gz || { echo "gunzip failed, trying alternative methods..."; exit 1; } cd $OUTDIR cpio -idv < ../ramdisk-recovery 2>/dev/null ) echo "Recovery files extracted to $OUTDIR/"使用方式:
chmod +x extract_recovery.sh ./extract_recovery.sh boot.img8. 进阶技巧与优化
对于需要频繁进行此类操作的高级用户,可以考虑以下优化:
8.1 使用更专业的工具链
除了基本的dd和cpio,还可以使用专门为Android开发的工具:
- magiskboot:来自Magisk项目的多功能boot.img处理工具
- abootimg:专门用于处理Android boot镜像的工具集
- unpackbootimg:传统的boot.img解包工具
例如,使用magiskboot可以简化流程:
magiskboot unpack boot.img magiskboot cpio ramdisk.cpio extract8.2 处理特殊压缩格式
某些设备可能使用非标准的压缩方式:
- LZ4压缩:需要使用lz4工具解压
- XZ压缩:需要使用xz工具解压
- 多层压缩:可能需要多次解压
8.3 验证提取的完整性
为确保提取的内容完整,可以进行以下验证:
- 检查init二进制是否存在且可执行
- 验证关键目录结构(如/sbin、/etc)
- 检查设备特定的配置文件(如fstab.*)
8.4 重新打包ramdisk
在某些情况下,可能需要修改后重新打包ramdisk:
cd recovery find . | cpio -o -H newc > ../new-ramdisk.cpio cd .. gzip new-ramdisk.cpio mv new-ramdisk.cpio.gz new-ramdisk.img9. 实际应用场景
成功提取recovery.img后,可以应用于多种场景:
9.1 提取专有驱动文件
如文章开头提到的场景,LineageOS编译需要从recovery中提取专有驱动文件:
./extract-files.sh --from-recovery9.2 定制recovery环境
可以修改提取的文件,创建自定义的recovery环境:
- 添加自定义工具到/sbin
- 修改init.rc脚本
- 替换或修改恢复界面
9.3 调试和分析
提取的recovery环境可用于:
- 分析厂商的恢复实现
- 调试硬件初始化问题
- 研究系统启动流程
10. 安全注意事项
在进行这些操作时,需要注意以下安全事项:
- 始终备份原始boot.img
- 在修改前验证所有更改
- 避免在生产设备上实验未经验证的修改
- 注意文件权限和SELinux上下文
- 确保任何定制不会破坏系统完整性验证
对于采用verified boot的设备,修改boot.img可能需要处理签名验证:
# 禁用验证(仅限调试) fastboot flashing unlock fastboot boot modified_boot.img