乐于分享
好东西不私藏

安卓OTA源码解析(二):把Payload包拆开看看里面到底装了哪些秘密

安卓OTA源码解析(二):把Payload包拆开看看里面到底装了哪些秘密

OTA 包虽然是zip包,但是里面的内容payload.bin才是核心,安卓原生中没有上来就对zip包做整包验签,而是对payload.bin处理过程中做的,payload.bin已经是高度压缩内容,在zip包里采用非压缩态存储,update_engine通过外部传参直接裸读zip包。payload.bin中格式是一套自描述的二进制协议。今天我们把它拆开,看看每个字节都是干嘛的。

先回答一个问题

上一篇聊了 update_engine 的整体架构,ActionProcessor 怎么编排流水线,各个 Action 怎么各司其职。

但有个关键问题我需要深入——DeltaPerformer 到底在写什么?

DownloadAction 从指定位置获取一堆数据,DeltaPerformer 一块块往 inactive slot 里塞。这些数据长什么样?它怎么知道"这块东西该写到哪个分区的哪个位置"?

答案就藏在 OTA Payload 的格式里。

四个字母:CrAU

zip包解开后,你拿一个 OTA payload 文件,打开十六进制编辑器,前四个字节:

43 72 41 55

ASCII 就是 CrAU——Chrome Auto Update 的缩写。

对,Android OTA 的 payload 格式直接沿用了 Chrome 浏览器自动更新的格式。Google 内部很早就搞了这套二进制协议,后来 Android 搞 A/B 更新,直接拿来用了,省得重新造轮子。

这四个字节就是"魔数"。任何解析器拿到数据,先看前四个字节是不是 CrAU,不是就直接报错。

整体布局:五段式

一个 OTA payload 文件,从头到尾长这样:

┌─────────────────────────────────────────────────────────┐ │                    Payload 文件                          │ │                                                          │ │  ┌──────────────────────────────────┐                   │ │  │  Payload Header (固定长度)         │                   │ │  │  ├─ magic: "CrAU" (4 bytes)       │                   │ │  │  ├─ major_version: 2 (8 bytes)    │                   │ │  │  ├─ manifest_size (8 bytes)       │                   │ │  │  ├─ metadata_signature_size (4B)  │                   │ │  │  └─ ...                           │                   │ │  └──────────────────────────────────┘                   │ │                                                          │ │  ┌──────────────────────────────────┐                   │ │  │  Manifest (protobuf 序列化)        │                   │ │  │  ├─ DeltaArchiveManifest          │                   │ │  │  │  ├─ PartitionUpdate[]           │                   │ │  │  │  │  ├─ partition_name           │                   │ │  │  │  │  ├─ operations[]             │                   │ │  │  │  │  └─ ...                      │                   │ │  │  │  └─ ...                         │                   │ │  └──────────────────────────────────┘                   │ │                                                          │ │  ┌──────────────────────────────────┐                   │ │  │  Metadata Signature (可选)         │                   │ │  │  └─ 对 Header+Manifest 的签名      │                   │ │  └──────────────────────────────────┘                   │ │                                                          │ │  ┌──────────────────────────────────┐                   │ │  │  Blob Data (实际数据载荷)           │                   │ │  │  ├─ operation 0 的数据              │                   │ │  │  ├─ operation 1 的数据              │                   │ │  │  ├─ ...                            │                   │ │  │  └─ operation N 的数据              │                   │ │  └──────────────────────────────────┘                   │ │                                                          │ │  ┌──────────────────────────────────┐                   │ │  │  Payload Signature                 │                   │ │  │  └─ 对整个 payload 的签名           │                   │ │  └──────────────────────────────────┘                   │ └─────────────────────────────────────────────────────────┘

五大部分,一段扣一段。下面这张图从字节偏移的角度拆解了每一段的细节:

Header → Manifest → Metadata Signature → Blob Data → Payload Signature,五段。

它们之间有严格的顺序依赖——你得先解析 Header 才能知道 Manifest 有多大,得解析 Manifest 才能知道 Blob Data 怎么用。一层扣一层。

Header:24 字节的开门钥匙

Header 是固定长度的,很直接。源码在 payload_metadata.h:

cpp

constexpr size_t kDeltaVersionSize = 8; constexpr size_t kDeltaManifestSize = 8; constexpr size_t kDeltaMetadataSigSize = 4; constexpr size_t kDeltaPayloadHeaderSize =      kDeltaVersionSize + kDeltaManifestSize + kDeltaMetadataSigSize; // = 8 + 8 + 4 = 20 bytes(不含 magic)

完整的 Header 结构:

偏移量    大小       字段名                说明 ────────────────────────────────────────────────────── 0x00     4 bytes   magic                 "CrAU" (0x43724155) 0x04     8 bytes   major_version         主版本号,目前固定为 2 0x0C     8 bytes   manifest_size         Manifest 部分的字节数 0x14     4 bytes   metadata_signature_size  元数据签名的字节数 ────────────────────────────────────────────────────── 总共: 24 bytes

ParsePayloadHeader() 的核心逻辑:

cpp

ErrorCode PayloadMetadata::ParsePayloadHeader(     const brillo::Blob& payload,      PayloadMetadataValues* values) {      if (payload.size() < kDeltaMagic.size() + kDeltaPayloadHeaderSize)     return ErrorCode::kDownloadInvalidMetadataMagicString;    // 校验魔数   if (!std::equal(kDeltaMagic.begin(), kDeltaMagic.end(),                    payload.begin()))     return ErrorCode::kDownloadInvalidMetadataMagicString;    uint64_t major_version;   memcpy(&major_version, payload.data() + kDeltaMagic.size(),           sizeof(major_version));   if (major_version != kDeltaVersionMajor)     return ErrorCode::kUnsupportedMajorPayloadVersion;    uint64_t manifest_size;   memcpy(&manifest_size,           payload.data() + kDeltaMagic.size() + kDeltaVersionSize,           sizeof(manifest_size));    uint32_t metadata_sig_size;   memcpy(&metadata_sig_size,           payload.data() + kDeltaMagic.size() + kDeltaVersionSize +           kDeltaManifestSize,           sizeof(metadata_sig_size));    values->manifest_size = manifest_size;   values->metadata_signature_size = metadata_sig_size;   return ErrorCode::kSuccess; }

有个细节:major_version 和 manifest_size 是小端序存储的。x86/ARM 原生字节序,直接 memcpy 就行,不用 ntohl 那套。

Header 解析完,就知道两件事:Manifest 有多大,签名从哪开始。

Manifest:施工图纸

Manifest 是整个 Payload 最核心的部分——一段 protobuf 序列化的二进制数据,描述"要把哪些分区改成什么样"。

proto 定义在 update_metadata.proto,这个文件是 update_engine 的灵魂。

DeltaArchiveManifest

protobuf

message DeltaArchiveManifest {   repeated PartitionUpdate partitions = 2;   optional uint64 max_timestamp = 14;   optional uint64 minor_version = 12;   optional DynamicPartitionMetadata dynamic_partition_metadata = 17;   optional uint64 security_patch_level = 18; }

partitions 是核心中的核心——一个数组,每个元素描述一个分区的更新。

PartitionUpdate

protobuf

message PartitionUpdate {   required string partition_name = 1;   // system, vendor, boot...   optional bool run_postinstall = 4;   optional string postinstall_path = 5;   optional uint64 new_partition_info = 8;   optional uint64 old_partition_info = 9;   repeated InstallOperation operations = 10;  // 操作列表   optional EstimateCowSize estimate_cow_size = 19;  // VABC 相关 }

operations 是真正的干活部分——每个元素就是一个"手术操作",告诉 DeltaPerformer "把这块数据写到哪"。

InstallOperation:12 种手术刀

protobuf

message InstallOperation {   required Type type = 1;   repeated Extent dst_extents = 2;   // 写到哪   optional uint64 data_offset = 3;   // Blob Data 中的偏移   optional uint64 data_length = 4;   repeated Extent src_extents = 5;   // 从哪读(增量更新)   optional uint64 src_length = 6;       optional bytes data_sha256_hash = 8;      enum Type {     REPLACE = 0;          // 直接替换     REPLACE_BZ = 1;       // bzip2 压缩     REPLACE_XZ = 2;       // xz 压缩     MOVE = 3;             // 已废弃     BSDIFF = 4;           // 二进制差分     SOURCE_COPY = 5;      // 相同块直接复制     SOURCE_BSDIFF = 6;    // 源二进制差分     ZERO = 10;            // 清零     DISCARD = 11;         // TRIM/UNMAP     PUFFDIFF = 12;        // puffin 差分(ELF)     LZ4DIFF_BSDIFF = 13;     LZ4DIFF_PUFFDIFF = 14;     ZUCCHINI = 15;        // Chrome 差分   } }

12 种操作类型,各有各的用处:

操作类型           全量/增量    数据来源          适用场景 ──────────────────────────────────────────────────────────── REPLACE          全量        Blob 中的原始数据   小分区、完全不同的块 REPLACE_BZ       全量        bzip2 压缩数据     压缩率高但解压慢 REPLACE_XZ       全量        xz 压缩数据        压缩率最高 SOURCE_COPY      增量        无(直接复制)      新旧完全相同的块 SOURCE_BSDIFF    增量        Blob 中的差分数据   大部分文件修改 PUFFDIFF         增量        puffin 差分数据    ELF 文件优化 LZ4DIFF_BSDIFF   增量        LZ4+bsdiff 差分   压缩分区的差分 LZ4DIFF_PUFFDIFF 增量        LZ4+puffdiff      压缩分区的 ELF ZERO             -           无(清零操作)       需要置零的区域 DISCARD          -           无(TRIM 操作)      SSD/UFS 优化 ZUCCHINI         增量        Chrome 差分数据    Android 13+

增量更新时,SOURCE_COPY 占比通常最高——新旧版本大部分数据是一样的,直接复制就行,连 Blob Data 都不用。这也是为什么增量包比全量包小那么多。

Extent:块级地址

protobuf

message Extent {   optional uint64 start_block = 1;   optional uint64 num_blocks = 2; }

Extent 就是"从第几块开始,连续几块"。分区被切成固定大小的块(通常 4KB),Extent 用块号定位。为什么不用字节偏移?因为块设备本身按块操作,文件系统 I/O 单位也是块,直接用块号省一次转换。

数据结构关系总览

看完各个结构,把它们串起来。从 Payload 二进制文件到最终的块级执行,数据是这样流转的:

DeltaArchiveManifest 包含多个 PartitionUpdate,每个分区包含多个 InstallOperation,每个操作通过 Extent 定址,通过 data_offset 引用 Blob Data。

简单说:一个 Manifest → 多个分区 → 多个操作 → 多个块地址 + 数据引用。

下面这张图更详细地展示了 protobuf 消息的字段层次:

两层签名:为什么要分两层

OTA Payload 有两层签名:

第一层:Metadata Signature ├─ 签名对象:Header + Manifest ├─ 位置:紧接在 Manifest 之后 └─ 作用:防止 Manifest 被篡改  第二层:Payload Signature ├─ 签名对象:Header + Manifest + Blob Data ├─ 位置:文件末尾 └─ 作用:防止 Blob Data 被篡改

为什么要分两层?

OTA 包动不动几百 MB 甚至几 GB,下载到一半断了很常见。断点续传时,已下载的部分需要校验——但如果签名只在文件末尾,你得下完整个包才能验证。

有了 Metadata Signature,下载一开始就能验证 Manifest。Manifest 里有每个 operation 的数据 SHA256,所以每写一块都能单独校验。

这就是"边下边验"。

验证流程:

1. 读 Header → 解析 metadata_signature_size 2. 读 Manifest(manifest_size 字节) 3. 读 Metadata Signature(metadata_signature_size 字节) 4. 公钥验证签名 → 失败就中止 5. 解析 Manifest → 拿到每个 operation 的信息 6. 下载 Blob Data → 每块数据用 SHA256 校验 7. 下载完 → 验证 Payload Signature

源码在 payload_verifier.cc:

cpp

ErrorCode PayloadVerifier::VerifyPayload(     const brillo::Blob& payload,     const brillo::Blob& metadata_signature,     const brillo::Blob& payload_signature) {      brillo::Blob metadata(       payload.begin(),       payload.begin() + kDeltaMagic.size() +        kDeltaPayloadHeaderSize + manifest_size);    if (!VerifySignature(metadata, metadata_signature))     return ErrorCode::kDownloadMetadataSignatureMismatch;    if (!payload_signature.empty()) {     if (!VerifySignature(payload, payload_signature))       return ErrorCode::kDownloadPayloadSignatureMismatch;   }    return ErrorCode::kSuccess; }

为什么用 protobuf 而不是 JSON

Google 选 protobuf,理由很实际:

1.体积小——二进制编码,比 JSON 小 3-10 倍。OTA 包动辄几百 MB,Manifest 本身得尽量小。

2.解析快——不需要字符串解析,直接内存映射。

3.向前兼容——新增字段老版本解析器直接忽略,不会崩。Android 10 加的动态分区信息,老设备照样能解析。

4.schema 严格——`.proto` 文件就是文档,不会有字段名拼错这种低级错误。

代价是可读性差。你不能 cat 一个 payload 文件就看懂 Manifest,得用工具:

bash

dd if=payload.bin bs=1 skip=24 count=<manifest_size> 2>/dev/null | \   protoc --decode=DeltaArchiveManifest update_metadata.proto

小结

Payload 格式看起来就是"Header + protobuf",但每个设计选择都有它的道理:

• 两层签名——Metadata Signature 让断点续传时能提前验证,不用等下完整个包

• 操作抽象——12 种操作类型覆盖全量、增量、清零所有场景

• 块级寻址——Extent 用块号不用字节偏移,和底层 I/O 对齐

• protobuf 选型——二进制编码、向前兼容、schema 严格

每个选择都对应着 OTA 场景下的真实约束。

下一篇聊 ActionProcessor——这条流水线到底怎么运转的,为什么能支持暂停、恢复、取消,为什么一个 Action 失败不会拖垮整个系统。

*源码基于 AOSP system/update_engine,proto 定义在 update_metadata.proto。*

相关学习资料