第四章:升级包格式——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.binpayload_properties.txt

小提示: 如果你在解压OTA包时发现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的类型,我遇到过的主要有这几种:

  1. REPLACE:直接替换,把新数据写入指定偏移
  2. REPLACE_BZ:先bzip2解压,再写入
  3. SOURCE_COPY:从旧分区复制数据
  4. SOURCE_BSDIFF:使用bsdiff算法生成差分数据
  5. ZERO:填充零,用于擦除操作
  6. DISCARD:丢弃数据,用于缩小分区
重点: 升级程序会严格按照manifest中的操作顺序执行。一旦某个操作失败,整个升级就会回滚。所以,manifest的正确性直接决定了升级的成败。

4.3 元数据与签名机制

元数据,说白了就是“关于数据的数据”。在OTA包里,元数据主要分布在两个地方:

  • payload_properties.txt:记录payload.bin的哈希值、大小、升级类型等
  • META-INF目录:记录所有文件的哈希值和签名

我见过不少开发者只关注payload.bin,却忽略了元数据的校验。结果呢?升级包在传输过程中被截断,或者被恶意替换,导致设备变砖。嗯,这种坑我踩过不止一次。

签名机制是OTA安全的第一道防线。Android使用了一套基于JAR签名的体系:

  1. 计算每个文件的SHA-256哈希,写入MANIFEST.MF
  2. 对MANIFEST.MF进行签名,生成CERT.SF
  3. 将公钥证书放入CERT.RSA

升级程序在验证时,会做三件事:

  • 用CERT.RSA里的公钥解密CERT.SF,得到原始哈希
  • 重新计算MANIFEST.MF的哈希,与解密结果对比
  • 遍历所有文件,计算哈希,与MANIFEST.MF里的值对比

这三层校验,任何一层失败,升级都会中止。我曾经在调试一个定制ROM时,发现签名总是验证失败。查了半天,原来是打包脚本里把payload.bin的路径写错了,导致MANIFEST.MF里记录的是./payload.bin,而实际文件是payload.bin。差一个点,校验就过不去。

避坑指南: 签名验证时,路径必须完全一致。包括大小写、斜杠方向、相对路径的写法。我曾经因为Windows和Linux的路径分隔符差异,折腾了整整一个下午。

4.4 实战:如何手动解析一个OTA包

光说不练假把式。我建议你找个真实的OTA包,亲手拆解一次。下面是我常用的步骤:

  1. 解压zip包unzip update.zip -d ota_dir
  2. 查看payload_properties.txt:确认升级类型和哈希
  3. 解析payload.bin头部:用Python读取前42字节,解析出manifest大小
  4. 提取manifest:根据manifest_size读取protobuf数据
  5. 反序列化manifest:使用update_metadata_pb2.py(来自chromiumos)解析
  6. 验证签名:使用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移植过来的。历史渊源,了解一下。

我的习惯: 每次拿到新的OTA包,我都会先跑一遍这个解析脚本。如果manifest_size异常大(比如超过10MB),那说明这个包可能是全量包,而不是差分包。全量包和差分包的升级策略完全不同,提前知道能省不少调试时间。

4.5 总结与思考

OTA包的结构,说白了就是“集装箱+施工图纸+防伪标签”的组合。集装箱是zip,施工图纸是manifest,防伪标签是签名。

理解了这个结构,你就能回答很多实际问题:

  • 为什么OTA包不能直接解压刷写?——因为payload.bin里是操作指令,不是镜像文件
  • 为什么升级失败后设备还能启动?——因为升级是事务性的,失败会回滚
  • 为什么有些OTA包特别大?——因为它是全量包,包含了完整的分区镜像

嗯,这一章的内容就到这里。下一章我们会深入payload的生成过程,看看差分算法到底是怎么工作的。到时候我会分享一个我亲手踩过的坑——bsdiff的参数调优,差点把服务器搞崩了。

记住,OTA包的结构是升级系统的基石。搞懂了它,你就能在升级问题面前游刃有余。