5、OTA升级包制作(下):payload.bin结构解析、payload生成器源码分析、自定义OTA包签名、多分区打包策略
上一讲我们把OTA升级包的骨架搭好了,今天来啃最硬的骨头——payload.bin。说实话,我第一次看到这个二进制文件时也是一头雾水。但搞懂了它的结构,你就能掌控整个升级流程。
5.1 payload.bin 结构深度解析
payload.bin 不是随便拼出来的二进制流。它有一套严谨的协议格式。我习惯把它分成三个层次来看:
- 元信息层:文件头、manifest 描述
- 操作层:一系列 install 操作序列
- 数据层:实际的差分或全量数据块
来看它的核心结构:
// 简化后的 payload 结构
PayloadManifest {
repeated PartitionUpdate partitions // 分区列表
optional int64 block_size = 1 // 块大小,通常 4K
optional int64 minor_version = 2 // 版本兼容标记
}
PartitionUpdate {
string partition_name // 分区名,如 system、vendor
optional string hash_func // 哈希算法,如 sha256
repeated InstallOperation operations // 操作序列
optional uint64 new_partition_size // 新分区大小
optional bytes old_partition_info // 旧分区校验信息
}
InstallOperation {
enum Type {
REPLACE = 0 // 全量替换
REPLACE_BZ = 1 // bzip2 压缩替换
MOVE = 2 // 块移动
SOURCE_COPY = 4 // 源拷贝
ZERO = 5 // 填充零
DISCARD = 6 // 丢弃块
BROTLI_BSDIFF = 7 // brotli 差分
}
Type type
repeated Extent src_extents // 源数据范围
repeated Extent dst_extents // 目标数据范围
bytes data_offset // 数据在 payload 中的偏移
bytes data_length // 数据长度
}
这里有个关键点:操作类型决定了升级策略。全量升级用 REPLACE,差分升级用 BROTLI_BSDIFF。我在项目中遇到过一个问题——差分升级时如果源分区被修改过(比如 root 后改了 system),BROTLI_BSDIFF 会失败,因为源数据对不上。这时候就得回退到全量升级。
核心要点:payload.bin 的 manifest 是 protobuf 序列化的,你可以用 protoc 反序列化出来看。我经常用这个命令来调试:
# 从 payload 中提取 manifest 并解析
python3 -c "
import sys
sys.path.insert(0, 'path/to/update_engine/scripts')
import payload_info
payload_info.print_payload_info('payload.bin')
"
5.2 payload 生成器源码分析
生成 payload 的核心工具是 delta_generator。它的源码在 AOSP 的 system/update_engine/ 下。我建议你重点看这几个文件:
| 文件 | 作用 | 关键函数 |
|---|---|---|
| payload_generator.cc | 主入口,协调整个生成流程 | GeneratePayload() |
| delta_diff_utils.cc | 差分算法实现 | DiffPartitions() |
| payload_signer.cc | 签名逻辑 | SignPayload() |
| extent_utils.cc | 块范围管理 | ExtendExtents() |
生成流程大致是这样的:
- 读取新旧镜像文件
- 按分区逐个对比,生成 diff 操作
- 将操作序列写入 protobuf manifest
- 把数据块追加到 payload 尾部
- 计算整体哈希,写入签名
我举个例子,假设你要生成 system 分区的差分升级包:
# 命令行调用
delta_generator \
--new_image system_new.img \
--old_image system_old.img \
--new_partition_name system \
--old_partition_name system \
--operations_file ops.bin \
--out_file payload.bin
# 源码中核心逻辑
bool GeneratePayload(const PayloadGenerationConfig& config) {
// 1. 加载新旧镜像
ImageReader old_image(config.old_image_path);
ImageReader new_image(config.new_image_path);
// 2. 逐块对比
for (size_t block = 0; block < new_image.block_count(); ++block) {
if (old_image.ReadBlock(block) == new_image.ReadBlock(block)) {
// 相同块,跳过
continue;
}
// 不同块,生成差分操作
InstallOperation op;
if (TryBrotliDiff(old_image, new_image, block, &op)) {
// 成功生成差分
} else {
// 差分失败,回退到全量
GenerateReplaceOp(new_image, block, &op);
}
}
// 3. 序列化并写入
return WritePayload(manifest, data_blobs, output);
}
个人经验:生成 payload 时,--chunk_size 参数很关键。默认是 4K,但如果你分区很大(比如 4GB 的 system),建议调大到 64K 或 128K。这样能减少操作数量,加快生成速度。不过代价是差分精度会降低,升级包会大一点。需要权衡。
5.3 自定义 OTA 包签名
OTA 包的签名不是随便签的。它有一套完整的信任链。我见过不少团队在这上面栽跟头——签名对了但升级失败,因为公钥没烧进 bootloader。
标准签名流程是这样的:
# 1. 生成密钥对(如果还没有)
openssl genpkey -algorithm RSA -out ota_private.pem -pkeyopt rsa_keygen_bits:4096
openssl rsa -pubout -in ota_private.pem -out ota_public.pem
# 2. 对 payload 签名
delta_generator \
--in_file payload.bin \
--signature_size 256 \
--private_key ota_private.pem \
--out_file payload_signed.bin
# 3. 验证签名
delta_generator \
--in_file payload_signed.bin \
--public_key ota_public.pem \
--verify
但如果你要自定义签名方案,比如用 ECDSA 或者 SM2,就得改源码了。核心修改点在这里:
// payload_signer.cc 中的 SignPayload 函数
bool SignPayload(const std::string& payload_file,
const std::string& private_key_file,
const std::string& output_file) {
// 默认使用 RSA-PSS 签名
// 如果你想用 ECDSA,需要替换这里的签名算法
std::string signature;
if (!SignWithRsaPss(payload_data, private_key, &signature)) {
return false;
}
// 签名写入 payload 尾部
return WriteSignature(output, signature);
}
// 自定义签名示例:使用 ECDSA
bool SignWithEcdsa(const std::string& data,
const std::string& private_key,
std::string* signature) {
// 这里调用 OpenSSL 的 ECDSA_sign 接口
// 注意:需要同时修改验证端的签名算法
// 否则升级会失败
}
避坑指南:我曾经帮一个客户排查 OTA 失败问题,折腾了两天。最后发现是签名算法改了,但 recovery 里的公钥没更新。记住:签名和验签必须配对。你改了签名算法,recovery 镜像、bootloader 里的公钥都得同步更新。
5.4 多分区打包策略
现代车载系统不止 system 和 vendor 两个分区。我见过最多的有 12 个分区需要同时升级。多分区打包不是简单地把它们拼在一起,要考虑依赖关系和升级顺序。
先看一个典型的多分区 manifest:
partitions: {
partition_name: "boot"
operations: { type: REPLACE ... }
}
partitions: {
partition_name: "dtbo"
operations: { type: REPLACE ... }
}
partitions: {
partition_name: "system"
operations: { type: BROTLI_BSDIFF ... }
}
partitions: {
partition_name: "vendor"
operations: { type: BROTLI_BSDIFF ... }
}
partitions: {
partition_name: "vbmeta"
operations: { type: REPLACE ... }
}
打包策略我总结为三条原则:
- 依赖优先:boot 依赖 dtbo,所以 dtbo 要先升级
- 大小排序:小分区(如 vbmeta)放前面,大分区(如 system)放后面
- 类型分组:全量操作放一起,差分操作放一起
生成多分区 payload 的命令示例:
delta_generator \
--new_image system_new.img --old_image system_old.img \
--new_partition_name system \
--new_image vendor_new.img --old_image vendor_old.img \
--new_partition_name vendor \
--new_image boot_new.img --old_image boot_old.img \
--new_partition_name boot \
--new_image dtbo_new.img --old_image dtbo_old.img \
--new_partition_name dtbo \
--new_image vbmeta_new.img --old_image vbmeta_old.img \
--new_partition_name vbmeta \
--out_file payload.bin
实战建议:多分区打包时,我习惯先做一个依赖分析图。比如 boot 分区升级后,kernel 模块可能变了,vendor 里的驱动得匹配。所以 vendor 必须在 boot 之后升级。这个顺序错了,系统可能起不来。
最后,给你看一张我画的 payload 结构图,把今天讲的内容串起来:
嗯,payload.bin 的结构其实不复杂。你只要记住:头 + manifest + 数据 + 签名,四层结构。每一层都有固定的格式和校验。搞懂了这些,你就能自己写工具来生成、解析、甚至修改 payload 了。
我在实际项目中,经常用 Python 写脚本直接操作 payload.bin。比如批量替换分区镜像、修改签名算法、调整分区顺序。这些骚操作的前提,就是今天讲的这些底层知识。
公众号:蓝海资料掘金营,微信deep3321