手把手教你从A/B OTA的boot.img里挖出recovery.img(以Essential PH-1为例)
深度解析从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.imggit 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.imgcd $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命令提取ramdiskdd ifboot.img oframdisk-recovery.img bs4096 skip3875参数说明ifboot.img输入文件oframdisk-recovery.img输出文件bs4096块大小为4096字节(1页)skip3875跳过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 OUTDIRrecovery_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_SIZE4096 HEADER_PAGES1 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 oframdisk-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在某些情况下可能需要修改后重新打包ramdiskcd 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