第四章:升级包格式——OTA包结构、payload.bin解析、元数据与签名机制
好,咱们今天聊聊升级包到底长什么样。
很多人觉得OTA升级就是“把新系统下载下来,然后刷进去”。嗯,这么说也没错,但实际落地的时候,这个“包”的结构远比你想的复杂。我当年第一次接触OTA包时,直接拿十六进制编辑器打开payload.bin,结果看了半天,满屏的二进制数据,差点以为自己打开了个加密文件。
说白了,OTA包不是简单的zip压缩包,它是一套精心设计的“交付物”。里面包含了元数据、签名、以及真正的升级数据。今天我就带你一层层剥开它。
4.1 OTA包的整体结构
一个标准的OTA升级包,通常是一个zip文件。但你别被这个后缀骗了,它内部的结构是有严格规范的。我个人习惯把OTA包想象成一个“集装箱”:
- 最外层:zip容器,负责打包和传输
- 中间层:元数据文件,描述“这个包要干什么”
- 核心层:payload.bin,真正的升级数据
- 安全层:签名文件,保证没人篡改过
我拆解过几个主流厂商的OTA包,结构大同小异。下面是一个典型的目录结构:
update.zip
├── META-INF/
│ ├── MANIFEST.MF # 清单文件,记录所有文件的哈希值
│ ├── CERT.SF # 签名文件,对MANIFEST.MF的签名
│ └── CERT.RSA # 公钥证书,用于验证签名
├── payload.bin # 核心升级数据,二进制格式
├── payload_properties.txt # payload的元数据,比如大小、哈希
├── care_map.pb # 分区映射表(protobuf格式)
└── compatibility.zip # 兼容性检查数据(可选)
这里要注意,META-INF目录是Java签名体系的标准做法,Android从早期版本就沿用了这套机制。但真正决定升级内容的,是payload.bin和payload_properties.txt。
payload.bin特别大,别慌。它里面可能包含了完整的系统镜像,也可能是差分补丁。具体是哪种,看payload_properties.txt里的FILE_HASH字段就能判断。
4.2 payload.bin深度解析
payload.bin是整个升级包的核心。它不是一个简单的二进制文件,而是一个“流式”结构。我刚开始研究它时,最困惑的就是:为什么不能直接解压?
原因很简单——payload.bin是设计给增量升级用的。它里面包含了大量的“操作指令”,比如“在偏移量0x1000处写入1024字节”、“从旧分区读取数据并计算差异”等等。
payload.bin的头部结构如下:
// payload.bin 头部(固定42字节)
struct PayloadHeader {
uint8_t magic[4]; // 魔数,固定为 "CRAU"
uint32_t version; // 版本号,目前主流是2
uint64_t manifest_size; // 后续manifest数据的大小
uint64_t metadata_size; // 元数据块大小
uint32_t metadata_crc32; // 元数据块的CRC32校验
uint32_t header_size; // 头部自身大小,固定42
};
头部之后,紧接着是manifest数据。manifest是一个protobuf序列化的结构,描述了整个升级过程的所有操作。我把它理解为“施工图纸”——告诉升级程序:先擦除哪个分区,再写入哪些数据,最后校验什么。
manifest的核心字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| partitions | repeated Partition | 所有需要操作的分区列表 |
| block_size | uint32 | 操作块大小,通常是4096字节 |
| minor_version | uint32 | 次版本号,用于兼容性判断 |
每个Partition又包含:
- partition_name:分区名,比如"system"、"vendor"
- operations:操作列表,每个操作是一个
InstallOperation - old_partition_info:旧分区的哈希值(用于差分升级)
- new_partition_info:新分区的哈希值(用于校验)
而InstallOperation的类型,我遇到过的主要有这几种:
- REPLACE:直接替换,把新数据写入指定偏移
- REPLACE_BZ:先bzip2解压,再写入
- SOURCE_COPY:从旧分区复制数据
- SOURCE_BSDIFF:使用bsdiff算法生成差分数据
- ZERO:填充零,用于擦除操作
- DISCARD:丢弃数据,用于缩小分区
4.3 元数据与签名机制
元数据,说白了就是“关于数据的数据”。在OTA包里,元数据主要分布在两个地方:
- payload_properties.txt:记录payload.bin的哈希值、大小、升级类型等
- META-INF目录:记录所有文件的哈希值和签名
我见过不少开发者只关注payload.bin,却忽略了元数据的校验。结果呢?升级包在传输过程中被截断,或者被恶意替换,导致设备变砖。嗯,这种坑我踩过不止一次。
签名机制是OTA安全的第一道防线。Android使用了一套基于JAR签名的体系:
- 计算每个文件的SHA-256哈希,写入MANIFEST.MF
- 对MANIFEST.MF进行签名,生成CERT.SF
- 将公钥证书放入CERT.RSA
升级程序在验证时,会做三件事:
- 用CERT.RSA里的公钥解密CERT.SF,得到原始哈希
- 重新计算MANIFEST.MF的哈希,与解密结果对比
- 遍历所有文件,计算哈希,与MANIFEST.MF里的值对比
这三层校验,任何一层失败,升级都会中止。我曾经在调试一个定制ROM时,发现签名总是验证失败。查了半天,原来是打包脚本里把payload.bin的路径写错了,导致MANIFEST.MF里记录的是./payload.bin,而实际文件是payload.bin。差一个点,校验就过不去。
4.4 实战:如何手动解析一个OTA包
光说不练假把式。我建议你找个真实的OTA包,亲手拆解一次。下面是我常用的步骤:
- 解压zip包:
unzip update.zip -d ota_dir - 查看payload_properties.txt:确认升级类型和哈希
- 解析payload.bin头部:用Python读取前42字节,解析出manifest大小
- 提取manifest:根据manifest_size读取protobuf数据
- 反序列化manifest:使用
update_metadata_pb2.py(来自chromiumos)解析 - 验证签名:使用
openssl验证CERT.RSA和CERT.SF
这里给一个简单的Python代码片段,用于读取payload.bin头部:
import struct
def parse_payload_header(filepath):
with open(filepath, 'rb') as f:
header = f.read(42)
magic = header[0:4]
version = struct.unpack('<I', header[4:8])[0]
manifest_size = struct.unpack('<Q', header[8:16])[0]
metadata_size = struct.unpack('<Q', header[16:24])[0]
metadata_crc32 = struct.unpack('<I', header[24:28])[0]
header_size = struct.unpack('<I', header[28:32])[0]
print(f"魔数: {magic}")
print(f"版本: {version}")
print(f"manifest大小: {manifest_size} 字节")
print(f"元数据大小: {metadata_size} 字节")
print(f"头部大小: {header_size} 字节")
# 使用示例
parse_payload_header('payload.bin')
运行这段代码,你会看到类似这样的输出:
魔数: b'CRAU'
版本: 2
manifest大小: 123456 字节
元数据大小: 789 字节
头部大小: 42 字节
看到“CRAU”这个魔数了吗?它代表“Chrome OS Update”。没错,Android的OTA机制就是从Chrome OS移植过来的。历史渊源,了解一下。
4.5 总结与思考
OTA包的结构,说白了就是“集装箱+施工图纸+防伪标签”的组合。集装箱是zip,施工图纸是manifest,防伪标签是签名。
理解了这个结构,你就能回答很多实际问题:
- 为什么OTA包不能直接解压刷写?——因为payload.bin里是操作指令,不是镜像文件
- 为什么升级失败后设备还能启动?——因为升级是事务性的,失败会回滚
- 为什么有些OTA包特别大?——因为它是全量包,包含了完整的分区镜像
嗯,这一章的内容就到这里。下一章我们会深入payload的生成过程,看看差分算法到底是怎么工作的。到时候我会分享一个我亲手踩过的坑——bsdiff的参数调优,差点把服务器搞崩了。
记住,OTA包的结构是升级系统的基石。搞懂了它,你就能在升级问题面前游刃有余。