前言

这篇文章大约鸽了一个多月吧,一直懒得写

嗯,它是工具FastbootEnhance的理论基础 我在写这个工具的时候总结而来的

说起Payload,第一感觉就是神秘,因为在引入Payload之前安卓一直采用的是 一个zip里直接装入分区镜像的形式(后来不再使用镜像,而是使用.new.dat,但是至少还是看得出它是啥的),但是,Payload则是用一种神秘的格式把各个分区的数据全部打包进了一个文件,还需要使用各种奇形怪状的dumper才能解开

Payload好像是伴随着A/B分区和update engine引入的,因为一直没有接触A/B分区的设备,也就一直没有了解过这个东西。不过,都2021年了,随着动态分区设备开始大规模普及,Payload.bin更是以format version 2的形式大规模走进了玩机用户的视线

整体结构

其实谷歌已经把它的结构很清楚的写在这里

这个update_metadata.proto是谷歌的Protobuf源文件,而Protobuf则是谷歌推出的一款类的序列化工具。简而言之,就是可以把一个类对象转换成字节流(然后就可以保存起来),并且可以通过这字节流把类对象还原出来。而这个.proto的文件就是这个类对象的描述,使用Protobuf工具编译这个文件,可以得到所需编程语言的源文件(.py / .java / .cs etc.),这些源文件包含了类的定义(又不是js这种怪胎,可以凭空造对象)以及相关的序列化和反序列化方法。因此,只要拿到Protobuf序列化出来的数据以及对应的.proto文件,就可以在任意它支持的语言中还原这个类对象

而Payload.bin本身并不是一个序列化的类,序列化的数据仅仅只是它的一部分(header中的manifest以及signature之类的)

所以 update_metadata.proto 这个文件并不是用来描述Payload.bin的结构的,而只是描述了Payload.bin中的某些部分的数据结构,使得使用反序列化方法能够顺利将这部分数据还原为类对象,然后再通过读取这些类对象的属性来获得所需的值

怎么有种脱裤子放屁的感觉,为什么要序列化而不直接往header里放数据
嗯,确实,因为header里还真的存了一些数据

update_metadata.proto在注释中写到了Payload.bin这个文件的整体结构

// Update file format: An update file contains all the operations needed
// to update a system to a specific version. It can be a full payload which
// can update from any version, or a delta payload which can only update
// from a specific version.
// The update format is represented by this struct pseudocode:
// struct delta_update_file {
//   char magic[4] = "CrAU";
//   uint64 file_format_version;  // payload major version
//   uint64 manifest_size;  // Size of protobuf DeltaArchiveManifest
//
//   // Only present if format_version >= 2:
//   uint32 metadata_signature_size;
//
//   // The DeltaArchiveManifest protobuf serialized, not compressed.
//   char manifest[manifest_size];
//
//   // The signature of the metadata (from the beginning of the payload up to
//   // this location, not including the signature itself). This is a serialized
//   // Signatures message.
//   char metadata_signature_message[metadata_signature_size];
//
//   // Data blobs for files, no specific format. The specific offset
//   // and length of each data blob is recorded in the DeltaArchiveManifest.
//   struct {
//     char data[];
//   } blobs[];
//
//   // The signature of the entire payload, everything up to this location,
//   // except that metadata_signature_message is skipped to simplify signing
//   // process. These two are not signed:
//   uint64 payload_signatures_message_size;
//   // This is a serialized Signatures message.
//   char payload_signatures_message[payload_signatures_message_size];
//
// };

可以看到,除了序列化的数据,header中还独立存储了例如file_format_version和各种size之类的东西
嗯,这些东西也是必要的,否则怎么知道要读多少东西交给Protobuf做反序列化
啊,那为什么还要序列化,manifest也这样直接存储不香么...
难道是Protobuf自带压缩效果?

但是,上面注释中描述的结构并不完全正确,上方描述的payload_signatures_message_sizedata blobs的后面,但是实际操作的时候却发现data blobs的大小并没有被明确告知,即payload_signatures_message_size这个东西的偏移量并不是可以直接得到的。于是我将各分区的数据块大小加起来得到一个可能的偏移量,却读到了一个明显错误的数据...

经过一番探索之后,我发现了payload_signatures_message_size实际上并不在上方注释所描述的位置,而是在manifest

message DeltaArchiveManifest {
......
  optional uint64 signatures_offset = 4;
  optional uint64 signatures_size = 5;
......
}

把里面的signatures_offset读出来发现居然和上面猜的偏移量是一样的。。。

所以,Payload.bin的实际结构大概是这样的

p

Manifest 结构

可以看到,这个Payload啊,最重要的应该就是Manifest了
没有了Manifest,data blobs该怎么用都不知道
那接下来看看Manifest是长什么样的

这个Manifest啊,是个Protobuf
对于现在主流的Version 2,它大概是长这样的
(翻译成类似于cpp的形式)

struct Manifest {

    uint32 block_size [default = 4096];

    uint64 signatures_offset;
    uint64 signatures_size;

    struct ImageInfo {
        //以下内容我实测一个都没读出来。。全是空的
        string board;
        string key;
        string channel;
        string version;
        string build_channel;
        string build_version;
    } old_image_info(仅ota更新,非完整包), new_image_info;

    // 表示OTA支持的操作,完整包应为0
    uint32 minor_version [default = 0];

    
    //一个分区对应一个
    struct PartitionUpdate {
        //分区名
        string partition_name;

        //以下几个用于更新前预处理
        //似乎都没怎么看到过
        bool run_postinstall;
        string postinstall_path;
        string filesystem_type;

        //应该是下方new_partition_info的签名?
        struct Signature {
            uint32 version [deprecated];
            bytes data;
            fixed32 unpadded_signature_size;
        } new_partition_signature[];

        //可以在此处得到新分区的大小和校验值
        struct PartitionInfo {
            uint64 size;
            bytes hash;
        } old_partition_info, new_partition_info;

        //新分区的安装操作,是个数组,一个分区有多个安装操作,多次安装
        struct InstallOperation {
            //本次安装操作的类型
            enum Type {
                REPLACE,  //直接使用data blobs中的数据替换
                REPLACE_BZ,  //bzip解压后再替换
                MOVE [deprecated],  //OTA的操作,移动数据
                BSDIFF [deprecated],  //OTA的操作,一种拆分算法

                //下面两个也是OTA增量时的操作
                SOURCE_COPY,
                SOURCE_BSDIFF,

                REPLACE_XZ = 8; //xz解压缩后替换数据

                ZERO = 6;  //写0
                DISCARD = 7;  //OTA操作,忽略?
                BROTLI_BSDIFF = 10;  //OTA操作?压缩过的bsdiff?

                PUFFDIFF = 9;  //OTA操作,另一种差量算法?
            } type;

            //当前操作的数据
            //相较于data blobs头部的偏移量
            uint64 data_offset;
            //当前操作的数据长度
            uint64 data_length;

            //一些OTA操作在读取数据时的范围
            //指的是设备里的已有分区内容
            struct Extent {
                uint64 start_block;
                uint64 num_blocks;
            } src_extents[];
            //以及长度
            uint64 src_length;

            //安装的目的地
            struct Extent {
                uint64 start_block;
                uint64 num_blocks;
            } dst_extents[];
            //安装时的数据长度(解压后总长)
            uint64 dst_length;

            //data blobs数据的校验值
            bytes data_sha256_hash;

            //OTA操作时,分区里已有源数据的校验值
            bytes src_sha256_hash;
        } operations[];

        //分区预处理是否可以跳过
        bool postinstall_optional;

        //剩下的大概就是一些校验相关的吧
        struct Extent {
            uint64 start_block;
            uint64 num_blocks;
        }  hash_tree_data_extent, hash_tree_extent;

        string hash_tree_algorithm;

        bytes hash_tree_salt;

        struct Extent {
            uint64 start_block;
            uint64 num_blocks;
        } fec_data_extent, fec_extent;

        uint32 fec_roots [default = 2];
    } partitions[];

    //当前系统的timestamp大于这个值时,视为降级安装
    int64 max_timestamp;

    //动态分区元数据
    struct DynamicPartitionMetadata {
        struct DynamicPartitionGroup {
            //组的名字
            string name;
            //组的最大大小
            uint64 size;
            //属于这个组的分区名
            string partition_names[];
        } groups[];
        //是否启动分区快照
        bool snapshot_enabled = 2;
    } dynamic_partition_metadata;
}

可以看到,结构还是相当复杂的,而且有许多属性是可选且分Manifest类型出现的(完整包/OTA包)
也许这就是为什么不写死在header里吧

对于完整包来说,安装更新只需要

foreach paritions
    foreach operations
        install()

然后install()的时候,根据data_offsetdata_length从data blobs里的指定位置读取数据
根据InstallOperationtype来确定如何处理数据(比如xz解压缩)
再把数据丢到dst指定的区块即可

至于拆分包,就更加复杂了,但是,也没法做实验来测试。。。

还有那个动态分区元数据
对应到device tree中是这个东西
https://github.com/LineageOS/android_device_oneplus_sm8250-common/blob/51943f7a173545b6f9d832ca804fcfefd619e23d/BoardConfigCommon.mk#L151
https://github.com/LineageOS/android_device_oneplus_instantnoodle/blob/7d42a8d7915a8c42da87885ae83c5be11ef5cea3/BoardConfig.mk#L45

从lpdump的结果来看,它真的只是一点点元数据,既不会影响分区的内容,也不会影响分区的排布,似乎仅仅只是用来限制几个分区的整体大小的(或者方便在删除分区时绑在一起?)。。。即使没有它们,默认的default分区组也会把一切安排的好好的。。

总结

这个Payload啊,看起来很神秘,但其实也没有那么神秘
不过往里面塞了个Protobuf,导致无论是打包还是解包,都需要调用谷歌的Protobuf库,实在是有亿点点麻烦
谁知道他到底是为了压缩数据,还是推广自家技术呢?