系列目录 :Android 7系统编译与刷机(一):源码环境搭建与完整编译流程 | (二):Image镜像生成机制深度解析 | (三):从编译产物到高通刷机包 | (四):QFIL刷机工具详解 | (五):特定分区镜像的单独导出 | (六):特定分区刷入实战
一、为什么要研究刷机包打包流程
编译完系统后,out/target/product/<设备名>/ 下有一堆 .img 文件。但如果直接把 system.img 和 boot.img 丢进 QFIL,你会发现刷不进去------要么提示"找不到烧录脚本",要么直接报错。
原因很简单:QFIL 不认裸镜像,它认的是 XML 脚本驱动的烧录包。
一个完整的高通 QFIL 刷机包,目录结构是这样的:
刷机包/
├── rawprogram0.xml ← 核心:描述每个镜像的烧录位置和参数
├── patch0.xml ← 分区表修补:根据实际存储大小调整分区
├── partition.xml ← 分区表定义:各分区的名称、大小、类型
├── prog_emmc_firehose_xxx.elf ← Firehose 协议通信程序
├── gpt_main0.bin ← 主 GPT 分区表
├── gpt_backup0.bin ← 备份 GPT 分区表
├── system.img ← 系统镜像(可能被分割成多个片段)
├── boot.img ← 启动镜像
├── userdata.img ← 用户数据镜像
└── ... ← 其他镜像
这篇就来讲清楚:编译产物怎么变成这个刷机包。
二、刷机包核心文件详解
2.1 partition.xml --- 分区表定义
partition.xml 定义了设备存储的分区布局,包括每个分区的名称、大小、类型和属性。
xml
<?xml version="1.0" encoding="UTF-8"?>
<partition_definition>
<!-- 分区表基础信息 -->
<device_info>
<sector_size>512</sector_size> <!-- 扇区大小,eMMC 通常为 512 -->
<total_sectors>61071360</total_sectors> <!-- 总扇区数 -->
<storage_type>eMMC</storage_type> <!-- 存储类型:eMMC 或 UFS -->
</device_info>
<partition>
<name>sbl1</name> <!-- 分区名称 -->
<size>1048576</size> <!-- 分区大小(字节) -->
<type>DEA0BA2C-CBDD-4805-B4F9-F428251C3E98</type> <!-- GPT 分区类型 GUID -->
<bootable>true</bootable> <!-- 是否可启动 -->
<filename>sbl1.mbn</filename> <!-- 对应的镜像文件名 -->
</partition>
<partition>
<name>system</name>
<size>2684354560</size> <!-- 2.5GB -->
<type>97D7B011-54DA-4835-B3C4-917AD6E73D74</type>
<bootable>false</bootable>
<filename>system.img</filename>
</partition>
<partition>
<name>userdata</name>
<size>0</size> <!-- 0 表示自动扩展到剩余空间 -->
<type>1B81E7E6-F50D-419B-A739-2AEEF8DA3335</type>
<bootable>false</bootable>
<filename>userdata.img</filename>
</partition>
</partition_definition>
关键设计 :
userdata分区的size设为0,表示"自动扩展到存储设备剩余空间"。这是高通平台的标准做法------userdata不需要固定大小,patch0.xml会在刷写时根据实际存储容量自动计算。
2.2 rawprogram0.xml --- 烧录程序脚本
rawprogram0.xml 是 QFIL 的核心指令文件,描述了每个镜像文件要烧到哪个物理扇区、以什么方式烧。
xml
<?xml version="1.0" encoding="UTF-8"?>
<data>
<!-- system 分区的写入指令 -->
<program
SECTOR_SIZE_IN_BYTES="512"
file_sector_offset="0" <!-- 从文件的第 0 个扇区开始读 -->
filename="system.img" <!-- 镜像文件名 -->
label="system" <!-- 分区标签 -->
num_partition_sectors="5242880" <!-- 分区占用的扇区数 -->
physical_partition_number="0" <!-- 物理分区号(LUN) -->
size_in_KB="2621440" <!-- 分区大小(KB) -->
sparse="true" <!-- 是否 sparse 格式 -->
start_sector="131072" <!-- 起始扇区号 -->
/>
<!-- boot 分区的写入指令 -->
<program
SECTOR_SIZE_IN_BYTES="512"
file_sector_offset="0"
filename="boot.img"
label="boot"
num_partition_sectors="131072"
physical_partition_number="0"
size_in_KB="65536"
sparse="false"
start_sector="2048"
/>
</data>
每个 <program> 元素的关键属性:
| 属性 | 含义 | 典型值 |
|---|---|---|
start_sector |
写入起始扇区 | 由 GPT 分区表决定 |
num_partition_sectors |
写入扇区数 | 分区大小 / 扇区大小 |
filename |
镜像文件名 | system.img |
sparse |
是否 sparse image | system/userdata 为 true,boot 为 false |
file_sector_offset |
文件内偏移(扇区) | 通常为 0,分包时不为 0 |
physical_partition_number |
物理分区号 | eMMC 通常为 0 |
关键设计 :
sparse="true"告诉 Firehose 协议,这个镜像文件是 sparse 格式。Firehose 在写入时会解析 sparse header,跳过 DONTCARE chunk,只写入有效数据。如果标记错了(比如 boot.img 是 raw 格式却标了 sparse="true"),就会触发Invalid sparse file format at header错误。
2.3 patch0.xml --- 分区表修补
patch0.xml 的职责是在烧录完成后修补 GPT 分区表,确保分区表校验和正确,以及 userdata 等自动扩展分区被正确填充到实际存储末尾。
xml
<?xml version="1.0" encoding="UTF-8"?>
<data>
<!-- 修补主 GPT 头部 -->
<patch
SECTOR_SIZE_IN_BYTES="512"
filename="gpt_main0.bin"
physical_partition_number="0"
size_in_KB="16"
start_sector="0"
byte_offset="32"
value="CRC32"
what="Update-CRC"
/>
<!-- 修补 userdata 分区大小 -->
<patch
SECTOR_SIZE_IN_BYTES="512"
filename=""
physical_partition_number="0"
start_sector="0"
what="Update-last-partition"
/>
</data>
关键设计 :
patch0.xml中的Update-last-partition指令是动态计算userdata分区大小的关键。它读取实际存储的总扇区数,减去所有固定分区占用的扇区,将剩余空间全部分配给最后一个分区。
三、打包脚本:从编译产物到刷机包
3.1 打包流程总览
高通平台的标准打包流程:
编译产物(out/target/product/xxx/)
│
├── system.img ──────────────┐
├── boot.img ────────────────┤
├── userdata.img ────────────┤
├── vendor.img ──────────────┤
└── ... │
▼
┌──────────────────────┐
│ update_common_info.py │ ← 高通官方打包脚本
│ - 分割大镜像 │
│ - 更新 GPT 分区表 │
│ - 生成 rawprogram.xml │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ pack.sh │ ← 自定义打包脚本
│ - 链接镜像文件 │
│ - 调用 update_common │
│ - 收集所有文件打包 │
└──────────────────────┘
│
▼
刷机包.zip
关键设计 :打包流程分两层------底层是高通官方的
update_common_info.py负责镜像分割和 XML 生成;上层是自定义的pack.sh负责收集文件和最终打包。这种分层设计便于在不同项目间复用打包逻辑。
3.2 镜像文件分割
对于大镜像(如 system.img),高通工具会将其分割成多个小文件,避免单个文件过大导致传输失败:
bash
# 分割后的 system.img 文件
system.img_0 # 第 0 个片段
system.img_1 # 第 1 个片段
system.img_2 # 第 2 个片段
对应的 rawprogram0.xml 中也会生成多个 <program> 元素:
xml
<!-- system 分片 0 -->
<program file_sector_offset="0" filename="system.img_0" ... />
<!-- system 分片 1 -->
<program file_sector_offset="262144" filename="system.img_1" ... />
<!-- system 分片 2 -->
<program file_sector_offset="524288" filename="system.img_2" ... />
关键设计 :镜像分割是高通为了解决大文件传输限制而设计的。
file_sector_offset告诉 Firehose 从文件的第几个扇区开始读数据,写入到start_sector + file_sector_offset对应的物理位置。每个分片独立写入,互不干扰。
3.3 自定义打包脚本示例
bash
#!/bin/bash
# pack.sh --- 将编译产物打包为 QFIL 刷机包
ROOT=$(pwd)
LNX_OUT=out/target/product/msm8974
PACK_DIR=$ROOT/flash_package
# 1. 创建打包目录
mkdir -p $PACK_DIR
# 2. 链接关键镜像文件
cp $LNX_OUT/boot.img $PACK_DIR/
cp $LNX_OUT/system.img $PACK_DIR/
cp $LNX_OUT/userdata.img $PACK_DIR/
cp $LNX_OUT/recovery.img $PACK_DIR/
# 3. 链接分区表相关文件
cp device/qcom/msm8974/gpt_main0.bin $PACK_DIR/
cp device/qcom/msm8974/gpt_backup0.bin $PACK_DIR/
cp device/qcom/msm8974/partition.xml $PACK_DIR/
# 4. 执行官方打包脚本
python device/qcom/common/update_common_info.py \
--partition_file=$PACK_DIR/partition.xml \
--output_dir=$PACK_DIR
# 5. 打包为 zip
cd $PACK_DIR && zip -r ../flash_package.zip ./*
关键设计 :打包脚本的核心是"收集 + 生成 + 打包"三步走。先用
cp收集所有需要的文件,再调用高通官方脚本生成 XML,最后打包成 zip。脚本中硬编码了路径,实际使用时应根据项目配置动态获取。
四、AOSP 编译产物与高通刷机包的对应关系
| AOSP 编译产物 | 刷机包中对应文件 | 说明 |
|---|---|---|
out/.../boot.img |
boot.img |
直接对应 |
out/.../system.img |
system.img 或 system.img_0/1/2 |
可能需要分割 |
out/.../userdata.img |
userdata.img |
直接对应 |
out/.../recovery.img |
recovery.img |
直接对应 |
| 无(需单独提供) | prog_emmc_firehose_xxx.elf |
高通平台专用,须从厂商获取 |
| 无(需单独提供) | partition.xml |
由设备树定义 |
| 无(由工具生成) | rawprogram0.xml |
由 update_common_info.py 生成 |
| 无(由工具生成) | patch0.xml |
由 update_common_info.py 生成 |
关键设计:这张表是"编译产物 → 刷机包"的映射速查。AOSP 编译产物只能直接提供镜像文件,分区表和 XML 脚本需要高通工具链生成,Firehose 文件需要从芯片厂商获取。
五、常见问题排查
问题一:分区大小不匹配
编译时 BOARD_SYSTEMIMAGE_PARTITION_SIZE 和 partition.xml 中的 system 分区大小不一致,导致刷写失败。
解决 :确保 BoardConfig.mk 中的分区大小和 partition.xml 中的定义一致:
makefile
# BoardConfig.mk
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 2684354560 # 2.5GB
xml
<!-- partition.xml -->
<partition>
<name>system</name>
<size>2684354560</size>
<!-- 必须一致 -->
</partition>
注意 :分区大小不一致是最常见的刷机失败原因之一。修改
BoardConfig.mk后,必须同步更新partition.xml,否则会导致刷写失败或分区表损坏。
问题二:sparse 格式标记错误
rawprogram0.xml 中 boot.img 的 sparse 属性误标为 true,导致 QFIL 报 Invalid sparse file format at header。
解决 :boot.img 和 recovery.img 不是 ext4 镜像,不是 sparse 格式,sparse 属性必须为 false。
注意:只有 ext4 文件系统镜像(system、vendor、userdata、cache)才可能是 sparse 格式。boot 和 recovery 是自定义打包格式,永远是 raw 格式。
问题三:Firehose 文件不匹配
prog_emmc_firehose_xxx.elf 和芯片型号不匹配,导致 QFIL 无法建立 Sahara 协议通信。
解决:确认芯片型号(如 MSM8974、MSM8996),从厂商获取对应的 Firehose 文件。
警告:Firehose 文件与芯片型号强绑定,不同芯片甚至不同版本的芯片都可能不兼容。使用错误的 Firehose 文件会导致 Sahara 协议握手失败,设备无法进入刷机模式。
六、关键文件索引
| 文件 | 作用 |
|---|---|
partition.xml |
分区表定义,定义各分区名称、大小、类型 |
rawprogram0.xml |
烧录指令脚本,描述每个镜像的写入位置和方式 |
patch0.xml |
分区表修补脚本,更新 CRC 校验和动态分区 |
prog_emmc_firehose_xxx.elf |
Firehose 协议通信程序 |
gpt_main0.bin / gpt_backup0.bin |
主/备份 GPT 分区表 |
update_common_info.py |
高通官方打包脚本 |
关键设计:这些文件构成了高通刷机包的完整结构。缺少任何一个文件都会导致刷机失败。建议将这些文件统一存放在版本控制系统中,便于追溯和复用。
七、本篇总结
本篇覆盖了从编译产物到 QFIL 刷机包的完整打包流程:
- partition.xml 定义分区布局,
userdata的size=0表示自动扩展 - rawprogram0.xml 是 QFIL 的核心指令文件,描述每个镜像的写入扇区、大小、sparse 标记
- patch0.xml 负责烧录后的分区表修补和 CRC 校验
- 打包脚本 将编译产物、分区表文件、Firehose 文件收集到一起,生成可刷写的包
下一篇我们将进入 QFIL 工具本身------从 9008 模式进入、Firehose 协议握手,到实际刷机操作的完整流程。