Android(安卓)Update Engine分析(八)升级包制作脚本分析
Android Update Engine分析(八)升级包制作脚本分析
本系列到现在为止共有七篇,分别如下:
- Android Update Engine分析(一)Makefile
- Android Update Engine分析(二)Protobuf和AIDL文件
- Android Update Engine分析(三)客户端进程
- Android Update Engine分析(四)服务端进程
- Android Update Engine分析(五)服务端核心之Action机制
- Android Update Engine分析(六)服务端核心之Action详解
- Android Update Engine分析(七) DownloadAction之FileWriter
前面几篇分别分析了Update Engine的Makefile,客户端demo进程和服务端,基本了解了Update Engine关于升级是如何运作的。而在升级之前,需要先制作升级包。升级包的制作和使用升级包进行升级是两个相反的过程,理解了升级包数据是如何产生的,反过来有利于我们理解Update Engine升级过程中的一些行为。所以我们将从差分包的制作命令开始,跟踪分析整个差分升级包的制作流程,看看升级数据到底是如何生成的。
一直以来,ota_from_target_files
脚本负责Android系统升级包的制作,不论是传统的升级方式还是A/B系统的升级方式。
在A/B系统中,ota_from_target_files
会将升级包制作的流程分解,并将payload文件制作和更新的操作转交给brillo_update_payload
脚本处理,而后者会进一步调用可执行文件delta_generator
去生成或更新用于升级的payload数据。因此,整个升级包的制作分为三个层次,顶层为ota_from_target_files
,接下来是brillo_update_payload
,最底层是delta_generator
。为避免文章太长,本文主要分析脚本ota_from_target_files
和brillo_update_payload
的行为,下一篇将对delta_generator
的代码进行详细分析。
本文涉及的Android代码版本:android‐7.1.1_r23 (NMF27D)
为了方便阅读,以下为本篇目录,只想了解特定内容请点击相应链接跳转:
- 1. 如何制作升级包?
- 2. 脚本
ota_from_target_files
- 2.1 脚本入口
- 2.2
main
函数 - 2.3
WriteABOTAPackageWithBrilloScript
函数 - 2.4 脚本
ota_from_target_files
总结
- 3. 脚本
brillo_update_payload
- 3.1 生成payload文件
cmd_generate()
函数extract_image()
函数extract_image_brillo()
函数- 生成payload文件总结
- 3.2 生成payload数据和metadata数据的哈希值
- 3.3 将payload签名和metadata签名写回payload文件
- 3.4 提取payload文件的properties数据
- 3.5 脚本brillo_update_payload总结
- 3.1 生成payload文件
1. 如何制作升级包?
《Android A/B System OTA分析(四)系统的启动和升级》一文中提到了全量升级包和增量升级包的制作方式,主要有两步:
- 编译系统
- 制作升级包
如果是做全量升级包,则只需要编译一次系统,在此系统的基础上制作升级文件;
如果是做增量升级包,则需要先编译一遍系统保存起来,修改代码,再编译一遍系统。然后工具基于这里的新旧两个系统制作升级包。
以下是我在该篇文章中使用的命令:
## 编译系统#$ source build/envsetup.sh$ lunch bcm7252ssffdr4-userdebug$ mkdir dist_output$ make -j32 dist DIST_DIR=dist_output [...]$ ls -lh dist-output/*target_files*-rw-r--r-- 1 ygu users 566M May 21 14:49 bcm7252ssffdr4-target_files-eng.ygu.zip## 制作全量升级包#$ ./build/tools/releasetools/ota_from_target_files \ dist-output/bcm7252ssffdr4-target_files-eng.ygu.zip \ full-ota.zip# # 制作增量升级包#$./build/tools/releasetools/ota_from_target_files \ -i dist-output/bcm7252ssffdr4-target_files-eng.ygu.zip \ dist-output-new/bcm7252ssffdr4-target_files-eng.ygu.zip \ incremental-ota.zip
2. 脚本ota_from_target_files
假设系统修改前后编译生成的ota包分别叫做old.zip和new.zip,生成的差分升级包叫做update.zip。
差分包制作脚本ota_from_target_files
位于目录:build/tools/releasetools
。
本篇以差分升级包的制作为例,执行命令:
android-7.1.1_r23$ ./build/tools/releasetools/ota_from_target_files \ -i dist/old.zip dist/new.zip \ dist/update.zip
然后跟踪代码执行路径来分析差分包是如何生成的。
2.1 脚本入口
差分包制作命令的入口点在ota_from_target_files.py
脚本的if __name__ == '__main__'
语句:
if __name__ == '__main__': try: common.CloseInheritedPipes() # main函数接收到的参数 sys.argv: [ # './build/tools/releasetools/ota_from_target_files', # '-i', # 'dist/old.zip', # 'dist/new.zip', # 'dist/update.zip' # ] main(sys.argv[1:]) except common.ExternalError as e: print print " ERROR: %s" % (e,) print sys.exit(1) finally: common.Cleanup()
这里会将除脚本名称外的参数传递到函数main(sys.argv[1:])
去执行。
2.2 main
函数
main
函数接收命令行传递过来的参数,并进行处理:
def main(argv): # 这里定义了option参数的处理函数option_handler, 略过函数细节 def option_handler(o, a): ... return True # 对传入的参数argv调用common.ParseOptions进行解析 # 解析后的结果: # args = ['dist/new.zip', 'dist/update.zip'] # OPTIONS.incremental_source = dist/old.zip args = common.ParseOptions(argv, __doc__, extra_opts="b:k:i:d:wne:t:a:2o:", extra_long_opts=[ "board_config=", "package_key=", "incremental_from=", "full_radio", "full_bootloader", "wipe_user_data", "no_prereq", "downgrade", "extra_script=", "worker_threads=", "aslr_mode=", "two_step", "no_signing", "block", "binary=", "oem_settings=", "oem_no_mount", "verify", "no_fallback_to_full", "stash_threshold=", "gen_verify", "log_diff=", "payload_signer=", "payload_signer_args=", ], extra_option_handler=option_handler) if len(args) != 2: common.Usage(__doc__) sys.exit(1) # 没有指定downgrade参数,略过 if OPTIONS.downgrade: # Sanity check to enforce a data wipe. if not OPTIONS.wipe_user_data: raise ValueError("Cannot downgrade without a data wipe") # We should only allow downgrading incrementals (as opposed to full). # Otherwise the device may go back from arbitrary build with this full # OTA package. if OPTIONS.incremental_source is None: raise ValueError("Cannot generate downgradable full OTAs - consider" "using --omit_prereq?") # 加载args[0](即'dist/new.zip')中指定词典文件的键值(key/value)对信息, # LoadInfoDict('dist/new.zip')函数中读取的文件包括: # 1. META/misc_info.txt # 2. BOOT/RAMDISK/etc/recovery.fstab # 3. SYSTEM/build.prop # # Load the dict file from the zip directly to have a peek at the OTA type. # For packages using A/B update, unzipping is not needed. input_zip = zipfile.ZipFile(args[0], "r") OPTIONS.info_dict = common.LoadInfoDict(input_zip) common.ZipClose(input_zip) # 检查是否A/B系统, 读取到的 info_dict['ab_update'] = (str) true ab_update = OPTIONS.info_dict.get("ab_update") == "true" # 当前的A/B系统走这里 if ab_update: # 前面解析参数时得到:OPTIONS.incremental_source = dist/old.zip if OPTIONS.incremental_source is not None: # 从'dist/new.zip'加载的key/value信息作为target_info_dict OPTIONS.target_info_dict = OPTIONS.info_dict # 从'dist/old.zip'加载的key/value信息作为source_info_dict source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r") OPTIONS.source_info_dict = common.LoadInfoDict(source_zip) common.ZipClose(source_zip) # 如果在命令行中添加了'-v'选项,则这里打印获取到的key/value信息 if OPTIONS.verbose: print "--- target info ---" common.DumpInfoDict(OPTIONS.info_dict) if OPTIONS.incremental_source is not None: print "--- source info ---" common.DumpInfoDict(OPTIONS.source_info_dict) # 所有生成A/B系统payload.bin的操作都由这里的调用搞定 # target_file='dist/new.zip' # output_file='dist/update.zip' # source_file='dist/old.zip' WriteABOTAPackageWithBrilloScript( target_file=args[0], output_file=args[1], source_file=OPTIONS.incremental_source) print "done." return
main()
函数的操作比较简单,先解析命令行参数,然后根据参数读取target包的相关词典文件信息,提取key/value键值对。如果提取到的key/value键值对中,“ab_update"对应的信息为"true”,则说明当前是基于A/B系统制作升级包。如果当前是制作差分包,则还需要提取source包的key/value键值对。
关于到底提取了那些key/value信息,可以在命令行添加’-v’选项,这样在执行时会打印所有target和source的键值对:
android-7.1.1_r23$ ./build/tools/releasetools/ota_from_target_files \ -v -i dist/old.zip dist/new.zip \ dist/update.zip
完成键值对的提取后,调用WriteABOTAPackageWithBrilloScript()
函数制作升级包,所以剩余的工作都在这个函数里。
2.3 WriteABOTAPackageWithBrilloScript
函数
def WriteABOTAPackageWithBrilloScript(target_file, output_file, source_file=None): """Generate an Android OTA package that has A/B update payload.""" # 差分包制作命令'ota_from_target_files -i dist/old.zip dist/new.zip dist/update.zip'的传入参数: # target_file='dist/new.zip' # output_file='dist/update.zip' # source_file='dist/old.zip' # # 设置 OPTIONS.package_key # # OPTIONS.package_key选项从命令行参数'-k'或'--package_key'解析得到,所以默认情况下为None # 在META/misc_info.txt中,'default_system_dev_certificate=build/target/product/security/testkey' # # Setup signing keys. if OPTIONS.package_key is None: OPTIONS.package_key = OPTIONS.info_dict.get( "default_system_dev_certificate", "build/target/product/security/testkey") # # 设置 OPTIONS.payload_signer # # OPTIONS.payload_signer选项从命令行参数'--payload_signer'解析得到,所以默认情况下为None # 如果没有设置OPTIONS.payload_signer, 这里构造openssl命令基于package_key生成临时的rsa_key: # 'openssl pkcs8 -in build/target/product/security/testkey.pk8 -inform DER -nocrypt -out /tmp/key-oQvVbH.key' # # A/B updater expects a signing key in RSA format. Gets the key ready for # later use in step 3, unless a payload_signer has been specified. if OPTIONS.payload_signer is None: cmd = ["openssl", "pkcs8", "-in", OPTIONS.package_key + OPTIONS.private_key_suffix, "-inform", "DER", "-nocrypt"] rsa_key = common.MakeTempFile(prefix="key-", suffix=".key") cmd.extend(["-out", rsa_key]) p1 = common.Run(cmd, stdout=subprocess.PIPE) p1.wait() assert p1.returncode == 0, "openssl pkcs8 failed" # 准备临时文件,用于output文件的生成 # Stage the output zip package for package signing. temp_zip_file = tempfile.NamedTemporaryFile() output_zip = zipfile.ZipFile(temp_zip_file, "w", compression=zipfile.ZIP_DEFLATED) # 提取键值对的"oem_fingerprint_properties"信息,默认情况下没有,为None # Metadata to comply with Android OTA package format. oem_props = OPTIONS.info_dict.get("oem_fingerprint_properties", None) oem_dict = None if oem_props: if OPTIONS.oem_source is None: raise common.ExternalError("OEM source required for this build") oem_dict = common.LoadDictionaryFromLines( open(OPTIONS.oem_source).readlines()) # # 构造 metadata # # 从字典信息构建metadata, 我制作升级包时得到的metadata信息为: # 'post-build': 'broadcom/bcm72604usff/bcm72604usff:7.1.1/NMF27D/rg935706151800:userdebug/test-keys', # 'post-build-incremental': 'eng.rg9357.20180615.180010', # 'pre-device': 'bcm72604usff' # 'post-timestamp': '1529056810', # 'ota-type': 'AB', # 'ota-required-cache': '0' # metadata = { "post-build": CalculateFingerprint(oem_props, oem_dict, OPTIONS.info_dict), "post-build-incremental" : GetBuildProp("ro.build.version.incremental", OPTIONS.info_dict), "pre-device": GetOemProperty("ro.product.device", oem_props, oem_dict, OPTIONS.info_dict), "post-timestamp": GetBuildProp("ro.build.date.utc", OPTIONS.info_dict), "ota-required-cache": "0", "ota-type": "AB", } # 制作差分包时,添加相应的pre-build/pre-build-incremental信息: # 'pre-build': 'broadcom/bcm72604usff/bcm72604usff:7.1.1/NMF27D/rg935706151800:userdebug/test-keys' # 'pre-build-incremental': 'eng.rg9357.20180615.180010', # if source_file is not None: metadata["pre-build"] = CalculateFingerprint(oem_props, oem_dict, OPTIONS.source_info_dict) metadata["pre-build-incremental"] = GetBuildProp( "ro.build.version.incremental", OPTIONS.source_info_dict) # # 1. 生成payload文件 # # 构造使用脚本生成payload数据的命令并执行: # brillo_update_payload generate --payload /tmp/payload-YqkYe1.bin \ # --target_image dist/new.zip \ # --source_image dist/old.zip # # 1. Generate payload. payload_file = common.MakeTempFile(prefix="payload-", suffix=".bin") cmd = ["brillo_update_payload", "generate", "--payload", payload_file, "--target_image", target_file] if source_file is not None: cmd.extend(["--source_image", source_file]) p1 = common.Run(cmd, stdout=subprocess.PIPE) p1.wait() assert p1.returncode == 0, "brillo_update_payload generate failed" # # 2. 生成payload和metadata数据的哈希值 # # 构造使用脚本从payload数据生成payload哈希和metadata哈希的命令并执行: # brillo_update_payload hash --unsigned_payload /tmp/payload-YqkYe1.bin \ # --signature_size 256 \ # --metadata_hash_file /tmp/sig-LDz25q.bin \ # --payload_hash_file /tmp/sig-Cdhb80.bin # # 2. Generate hashes of the payload and metadata files. payload_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin") metadata_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin") cmd = ["brillo_update_payload", "hash", "--unsigned_payload", payload_file, "--signature_size", "256", "--metadata_hash_file", metadata_sig_file, "--payload_hash_file", payload_sig_file] p1 = common.Run(cmd, stdout=subprocess.PIPE) p1.wait() assert p1.returncode == 0, "brillo_update_payload hash failed" # # 3. 对payload哈希和metadata哈希数据进行签名 # # 构造用于对payload哈希和metadata哈希签名的文件 # 'openssl pkcs8 -in build/target/product/security/testkey.pk8 -inform DER -nocrypt -out /tmp/key-oQvVbH.key' # # 3. Sign the hashes and insert them back into the payload file. signed_payload_sig_file = common.MakeTempFile(prefix="signed-sig-", suffix=".bin") signed_metadata_sig_file = common.MakeTempFile(prefix="signed-sig-", suffix=".bin") # 3a. 构造openssl命令使用rsa_key对payload哈希进行签名 # openssl pkeyutl -sign -inkey /tmp/key-oQvVbH.key \ # -pkeyopt digest:sha256 \ # -in /tmp/sig-Cdhb80.bin \ # -out /tmp/signed-sig-2UOQ1d.bin # # 3a. Sign the payload hash. if OPTIONS.payload_signer is not None: cmd = [OPTIONS.payload_signer] cmd.extend(OPTIONS.payload_signer_args) else: cmd = ["openssl", "pkeyutl", "-sign", "-inkey", rsa_key, "-pkeyopt", "digest:sha256"] cmd.extend(["-in", payload_sig_file, "-out", signed_payload_sig_file]) p1 = common.Run(cmd, stdout=subprocess.PIPE) p1.wait() assert p1.returncode == 0, "openssl sign payload failed" # 3b. 构造openssl命令使用rsa_key对metadata哈希进行签名 # openssl pkeyutl -sign -inkey /tmp/key-oQvVbH.key \ # -pkeyopt digest:sha256 \ # -in /tmp/sig-LDz25q.bin \ # -out /tmp/signed-sig-08K2oF.bin # # 3b. Sign the metadata hash. if OPTIONS.payload_signer is not None: cmd = [OPTIONS.payload_signer] cmd.extend(OPTIONS.payload_signer_args) else: cmd = ["openssl", "pkeyutl", "-sign", "-inkey", rsa_key, "-pkeyopt", "digest:sha256"] cmd.extend(["-in", metadata_sig_file, "-out", signed_metadata_sig_file]) p1 = common.Run(cmd, stdout=subprocess.PIPE) p1.wait() assert p1.returncode == 0, "openssl sign metadata failed" # 3c. 构造使用脚本将payload签名和metadata签名写回payload数据的命令并执行 # brillo_update_payload sign --unsigned_payload /tmp/payload-YqkYe1.bin \ # --payload /tmp/signed-payload-102BNs.bin \ # --signature_size 256 \ # --metadata_signature_file /tmp/signed-sig-08K2oF.bin \ # --payload_signature_file /tmp/signed-sig-2UOQ1d.bin # # 3c. Insert the signatures back into the payload file. signed_payload_file = common.MakeTempFile(prefix="signed-payload-", suffix=".bin") cmd = ["brillo_update_payload", "sign", "--unsigned_payload", payload_file, "--payload", signed_payload_file, "--signature_size", "256", "--metadata_signature_file", signed_metadata_sig_file, "--payload_signature_file", signed_payload_sig_file] p1 = common.Run(cmd, stdout=subprocess.PIPE) p1.wait() assert p1.returncode == 0, "brillo_update_payload sign failed" # # 4. 提取payload文件的properties数据 # # 构造使用脚本提取payload properties的命令并执行 # brillo_update_payload properties --payload /tmp/signed-payload-102BNs.bin \ # --properties_file /tmp/payload-properties-UoBiUx.txt # # 4. Dump the signed payload properties. properties_file = common.MakeTempFile(prefix="payload-properties-", suffix=".txt") cmd = ["brillo_update_payload", "properties", "--payload", signed_payload_file, "--properties_file", properties_file] p1 = common.Run(cmd, stdout=subprocess.PIPE) p1.wait() assert p1.returncode == 0, "brillo_update_payload properties failed" # # 向properties文件添加其它属性 # # 这里主要是根据OPTIONS.wipe_user_data选项决定是否往properties添加"POWERWASH=1" # OPTIONS.wipe_user_data选项从命令行参数"-w", "--wipe_user_data"解析得到,所以默认情况下为False # if OPTIONS.wipe_user_data: with open(properties_file, "a") as f: f.write("POWERWASH=1\n") metadata["ota-wipe"] = "yes" # # 将payload和properties以及metadata数据写入output文件中 # payload: payload.bin # properties: payload_properties.txt # metadata: META-INF/com/android/metadata # # Add the signed payload file and properties into the zip. common.ZipWrite(output_zip, properties_file, arcname="payload_properties.txt") common.ZipWrite(output_zip, signed_payload_file, arcname="payload.bin", compress_type=zipfile.ZIP_STORED) # # 向META-INF/com/android/metadata写入metadata数据 # 在我测试的平台上写入的数据如下: # $ cat META-INF/com/android/metadata # ota-required-cache=0 # ota-type=AB # post-build=broadcom/bcm72604usff/bcm72604usff:7.1.1/NMF27D/rg935706151800:userdebug/test-keys # post-build-incremental=eng.rg9357.20180615.180010 # post-timestamp=1529056810 # pre-build=broadcom/bcm72604usff/bcm72604usff:7.1.1/NMF27D/rg935706151800:userdebug/test-keys # pre-build-incremental=eng.rg9357.20180615.180010 # pre-device=bcm72604usff # WriteMetadata(metadata, output_zip) # # 将dm-verity相关的care_map数据写入output文件中 # # 我在制作的升级包里面看到的care_map.txt的内容为: # $ cat care_map.txt # /dev/block/by-name/system # # If dm-verity is supported for the device, copy contents of care_map # into A/B OTA package. if OPTIONS.info_dict.get("verity") == "true": target_zip = zipfile.ZipFile(target_file, "r") care_map_path = "META/care_map.txt" namelist = target_zip.namelist() if care_map_path in namelist: care_map_data = target_zip.read(care_map_path) common.ZipWriteStr(output_zip, "care_map.txt", care_map_data) else: print "Warning: cannot find care map file in target_file package" common.ZipClose(target_zip) # # 使用OPTIONS.package_key对output文件进行签名 # # 签名操作分为两步: # 1. 使用openssl命令检查package_key为unencrypted的private key # openssl pkcs8 -in build/target/product/security/testkey.pk8 -inform DER -nocrypt # # 2. 调用signapk.jar对output文件进行签名 # java -Xmx2048m -Djava.library.path=out/host/linux-x86/lib64 \ # -jar out/host/linux-x86/framework/signapk.jar \ # -w build/target/product/security/testkey.x509.pem \ # build/target/product/security/testkey.pk8 /tmp/tmpNuS8M4 dist/update.zip # # Sign the whole package to comply with the Android OTA package format. common.ZipClose(output_zip) SignOutput(temp_zip_file.name, output_file) temp_zip_file.close()
WriteABOTAPackageWithBrilloScript()
函数逻辑非常清晰,也包含了详细的注释,分析时没有太大难度。该函数包含了A/B系统制作升级包的所有步骤,归纳如下:
- 准备制作升级包需要的key
- 用于升级包签名的package_key
- 用于payload数据和metadata数据签名的payload_signer
- 准备升级包的metadata
- 这里的metadata指升级包metadata文件中的数据,而非payload.bin中的metadata数据
- 制作升级包
- 生成payload文件
- 提取payload数据和metadata数据的哈希值
- 对payload哈希和metadata哈希数据使用payload_signer进行签名
- 提取payload文件的properties数据并更新
- 将payload文件, properties文件, metadata文件以及care_map文件写入升级包文件
- 使用package_key对升级包文件进行签名
整个升级包制作的过程中主要使用了两支key,分别由OPTIONS.package_key
和payload_signer
指定。
顾名思义,前者package_key
用于对整个升级包update.zip
进行签名,后者payload_signer
用于对升级数据payload.bin
中的payload
和metadata
签名。
在制作升级包时,这两支key均可以通过命令行参数"-k
"/"--package_key
“或”--payload_signer
"设置。
在没有设置的情况下,二者默认使用系统目录下的"build/target/product/security/testkey"文件作为key的来源。
2.4 脚本ota_from_target_files
总结
脚本ota_from_target_files
的内容大概有2100+行,其中大部分都是制作传统的非A/B系统升级包有关,制作A/B系统的升级包的逻辑非常简单。
执行升级包制作命令时,main()
函数解析命令行参数,然后加载target包并提取相应文件(主要是META/misc_info.txt
和SYSTEM/build.prop
)信息用于创建键值对key/value集合。如果键值对集合中"ab_update"键的值为true,则判断当前为A/B系统制作升级包。随后,将整个升级包的制作都交给函数WriteABOTAPackageWithBrilloScript()
处理。后者包含了A/B系统制作升级包的所有步骤:
- 准备制作升级包需要的key
- 包括对升级数据(payload和metadata)签名的
payload_signer
和升级包自身签名的package_key
。
- 包括对升级数据(payload和metadata)签名的
- 准备升级包的metadata
- 这里的metadata指升级包metadata文件中的数据,而非payload.bin中的metadata数据
- 制作升级包
- 3.1 生成payload文件
- 3.2 提取payload数据和metadata数据的哈希值
- 3.3 对payload哈希和metadata哈希数据使用payload_signer进行签名
- 3.4 提取payload文件的properties数据并更新
- 3.5 将payload文件, properties文件, metadata文件以及care_map文件写入升级包文件
- 使用package_key对升级包文件进行签名
其中,将制作升级包的第3步中对payload数据的处理(3.1, 3.2, 3.4)打包成一些shell命令,交由brillo_update_payload
脚本进行处理。
3. 脚本brillo_update_payload
脚本brillo_update_payload
位于目录system/update_engine/scripts
中,定义了很多可供调用的函数,除去这些函数,整个脚本的主要逻辑就显得比较简单,最重要的部分如下:
case "$COMMAND" in generate) validate_generate cmd_generate ;; hash) validate_hash cmd_hash ;; sign) validate_sign cmd_sign ;; properties) validate_properties cmd_properties ;;esac
这段代码根据$COMMAND
的不同取值,执行不同的操作。每个$COMMAND
对应的操作都会调用两个函数,一个是validate_xxx
,一个是cmd_xxx
。前者主要用于验证命令行是否传递了所需要的参数,后者用于执行对应的操作,所以真正的重点在cmd_xxx
函数。
在升级包的制作过程中,WriteABOTAPackageWithBrilloScript()
函数前后共有4次调用brillo_update_payload
,对应于上面的4种情况,按顺序分别如下:
为了见名知意,已经将调用命令中杂乱无章的临时文件名替换为有意义的文件名。
## 1. 生成payload文件# 使用`brillo_update_payload`脚本生成payload数据:brillo_update_payload generate --payload /tmp/payload_file.bin \ --target_image dist/new.zip \ --source_image dist/old.zip## 2. 生成payload和metadata数据的哈希值# 使用`brillo_update_payload`脚本从payload数据生成payload哈希和metadata哈希:brillo_update_payload hash --unsigned_payload /tmp/payload_file.bin \ --signature_size 256 \ --metadata_hash_file /tmp/metadata_sig_file.bin \ --payload_hash_file /tmp/payload_sig_file.bin## 3. 将payload签名和metadata签名写回payload文件# 使用`brillo_update_payload`脚本将payload签名和metadata签名写回payload文件brillo_update_payload sign --unsigned_payload /tmp/payload_file.bin \ --payload /tmp/signed_payload_file.bin \ --signature_size 256 \ --metadata_signature_file /tmp/signed_metadata_sig_file.bin \ --payload_signature_file /tmp/signed_payload_sig_file.bin## 4. 提取payload文件的properties数据# 构造使用脚本提取payload properties的命令并执行brillo_update_payload properties --payload /tmp/signed_payload_file.bin \ --properties_file /tmp/payload_properties_file.txt
下面对这4个调用的命令逐个分析。
3.1 生成payload文件
使用brillo_update_payload
脚本生成payload数据:
brillo_update_payload generate --payload /tmp/payload_file.bin \ --target_image dist/new.zip \ --source_image dist/old.zip
对于generate
操作,执行case
语句中$COMMAND
为generate
的分支。
在该分支中,validate_generate
函数用于验证命令行是否设置了payload
和target_image
参数,cmd_generate
函数根据传入的参数执行payload.bin的generate
工作。
cmd_generate()
函数
cmd_generate() { # # 设置payload_type # # 根据是否传入source_image参数来确定当前是以delta还是full的方式生成payload # local payload_type="delta" if [[ -z "${FLAGS_source_image}" ]]; then payload_type="full" fi echo "Extracting images for ${payload_type} update." # # 从target/source的zip包中提取boot/system image # 提取到的文件路径存放在 DST_PARTITIONS/SRC_PARTITIONS数组中 # # 关于具体的操作,请参考后随后的extract_image()函数注解 extract_image "${FLAGS_target_image}" DST_PARTITIONS if [[ "${payload_type}" == "delta" ]]; then extract_image "${FLAGS_source_image}" SRC_PARTITIONS fi echo "Generating ${payload_type} update." # 构造指示payload文件的out_file参数,如:-out_file=dist/payload.bin # Common payload args: GENERATOR_ARGS=( -out_file="${FLAGS_payload}" ) local part old_partitions="" new_partitions="" partition_names="" # # 循环操作DST_PARTITIONS[boot,system]分区 # 获取分区名boot, system和对应的target, source包里提取到的image文件名 # partition_names=boot:system # new_partitions=target包中提取的boot和system image的文件名,用':'分隔 # old_partitions=source包中提取的boot和system image的文件名,用':'分隔 # for part in "${!DST_PARTITIONS[@]}"; do # 检查partition_names是否为空 if [[ -n "${partition_names}" ]]; then partition_names+=":" new_partitions+=":" old_partitions+=":" fi partition_names+="${part}" new_partitions+="${DST_PARTITIONS[${part}]}" old_partitions+="${SRC_PARTITIONS[${part}]:-}" done # # 构造包含partition_names和new_partitions的参数 # 如: # -out_file=dist/payload.bin \ # -partition_names=boot:system \ # -new_partitions=/tmp/boot.img.oiHmvn:/tmp/system.raw.ZkArkk # # Target image args: GENERATOR_ARGS+=( -partition_names="${partition_names}" -new_partitions="${new_partitions}" ) # 如果是delta的payload, 添加old_partitions参数和minor_version/zlib_fingerprint # 我测试的zip包中不包含zlib_fingerprint信息。所以这里构造的参数如下: # -out_file=dist/payload.bin \ # -partition_names=boot:system \ # -new_partitions=/tmp/boot.img.oiHmvn:/tmp/system.raw.ZkArkk \ # -old_partitions=/tmp/boot.img.cXD4Dt:/tmp/system.raw.IlRpgW \ # --minor_version=3 # if [[ "${payload_type}" == "delta" ]]; then # Source image args: GENERATOR_ARGS+=( -old_partitions="${old_partitions}" ) if [[ -n "${FORCE_MINOR_VERSION}" ]]; then GENERATOR_ARGS+=( --minor_version="${FORCE_MINOR_VERSION}" ) fi if [[ -n "${ZLIB_FINGERPRINT}" ]]; then GENERATOR_ARGS+=( --zlib_fingerprint="${ZLIB_FINGERPRINT}" ) fi fi # 添加major_version参数 if [[ -n "${FORCE_MAJOR_VERSION}" ]]; then GENERATOR_ARGS+=( --major_version="${FORCE_MAJOR_VERSION}" ) fi # 如果制作脚本有传入metadata_size_file参数,则添加metadata_size_file参数 # 我测试时没有传入metadata_size_file参数 if [[ -n "${FLAGS_metadata_size_file}" ]]; then GENERATOR_ARGS+=( --out_metadata_size_file="${FLAGS_metadata_size_file}" ) fi # 如果有指定POSTINSTALL_CONFIG_FILE,则添加new_postinstall_config_file参数 # 变量POSTINSTALL_CONFIG_FILE默认为空 if [[ -n "${POSTINSTALL_CONFIG_FILE}" ]]; then GENERATOR_ARGS+=( --new_postinstall_config_file="${POSTINSTALL_CONFIG_FILE}" ) fi # # 调用可执行文件delta_generator,传入上面构造的参数GENERATOR_ARGS,用于生成payload.bin # 所以我最后测试时,这里构造并执行的命令如下: # delta_generator -out_file=dist/payload.bin \ # -partition_names=boot:system \ # -new_partitions=/tmp/boot.img.oiHmvn:/tmp/system.raw.ZkArkk \ # -old_partitions=/tmp/boot.img.cXD4Dt:/tmp/system.raw.IlRpgW \ # --minor_version=3 --major_version=2 # echo "Running delta_generator with args: ${GENERATOR_ARGS[@]}" "${GENERATOR}" "${GENERATOR_ARGS[@]}" echo "Done generating ${payload_type} update."}
总结下cmd_generate()
里面的操作:
- 根据执行时是否传入了source_image参数,确定是生成全量(full)还是增量(delta)方式的payload.bin;
- 调用
extract_image()
解压缩提取target/source的zip包中的"IMAGES/{boot,system}.img"文件到临时文件夹; - 根据解压缩的boot和system image的临时文件路径构造generator的参数;
- 调用delta_generator,并传入前面构造的generator参数生成payload.bin;
所以cmd_generator()
将payload.bin的生成再次交给了delta_generator程序:
delta_generator -out_file=dist/payload.bin \ -partition_names=boot:system \ -new_partitions=/tmp/boot.img.oiHmvn:/tmp/system.raw.ZkArkk \ -old_partitions=/tmp/boot.img.cXD4Dt:/tmp/system.raw.IlRpgW \ --minor_version=3 --major_version=2
关于
new_partitions
和old_partitions
参数中的文件名:
/tmp/boot.img.oiHmvn
和/tmp/system.raw.ZkArkk
是从new.zip中提取的boot.img和system.img的临时文件名;/tmp/boot.img.cXD4Dt
和/tmp/system.raw.IlRpgW
是从old.zip中提取的boot.img和system.img的临时文件名;操作中,
/tmp
目录下的文件均为升级包制作过程中生成的临时文件,后续不再对命令中的临时文件名进行解释。
实际上这里称为转发是不准确的,因为在转发前,
brillo_update_payload
会先提取target和source对应zip包(即new.zip和old.zip)中的boot.img和system.img文件,并进行适当的处理。(所谓的处理就是,如果原来是sparse image, 则转换为raw image)
后面会对delta_generator
的操作进行详细分析。
extract_image()
函数
# extract_image ## Detect the format of the |image| file and extract its updatable partitions# into new temporary files. Add the list of partition names and its files to the# associative array passed in |partitions_array|.extract_image() { local image="$1" # # 检查image文件的前4字节确定升级文件类型 # # # 1. Brillo类型的文件是zip包,所以检查zip文件头部的magic header # # Brillo images are zip files. We detect the 4-byte magic header of the zip # file. local magic=$(head --bytes=4 "${image}" | hexdump -e '1/1 "%.2x"') if [[ "${magic}" == "504b0304" ]]; then echo "Detected .zip file, extracting Brillo image." # # 调用extract_image_brillo提取zip包文件 # extract_image_brillo "$@" return fi # # 2. Chrome OS类型的文件是GPT分区, 所以检查头部的cgpt数据 # # Chrome OS images are GPT partitioned disks. We should have the cgpt binary # bundled here and we will use it to extract the partitions, so the GPT # headers must be valid. if cgpt show -q -n "${image}" >/dev/null; then echo "Detected GPT image, extracting Chrome OS image." # # 调用extract_image_cros提取Chrome OS磁盘文件 # extract_image_cros "$@" return fi die "Couldn't detect the image format of ${image}"}
extract_image()
根据传入文件的类型,判断当前待提取文件是Android系统(Brillo)使用的zip包还是Chrome OS系统的磁盘文件,然后调用不同的方法进行处理。对于Android A/B系统使用的zip包,extract_image_brillo()
才是提取image文件的执行者。
extract_image_brillo()
函数
# extract_image_brillo ## Extract the A/B updated partitions from a Brillo target_files zip file into# new temporary files.extract_image_brillo() { # 获取传入参数 local image="$1" local partitions_array="$2" # # 解压缩zip内的META/ab_partitions.txt文件,并提取分区信息 # # android-7.1.1_r23$ cat dist/new/META/ab_partitions.txt # boot # system # local partitions=( "boot" "system" ) local ab_partitions_list # 生成临时文件"ab_partitions_list.XXXXXX" ab_partitions_list=$(create_tempfile "ab_partitions_list.XXXXXX") CLEANUP_FILES+=("${ab_partitions_list}") # 解压缩zip包的"META/ab_partitions.txt"到临时文件 if unzip -p "${image}" "META/ab_partitions.txt" >"${ab_partitions_list}"; then # 检查文件中是否有包含特殊字符串,分区名不应该包含这样的字符串 if grep -v -E '^[a-zA-Z0-9_-]*$' "${ab_partitions_list}" >&2; then die "Invalid partition names found in the partition list." fi # 提取文件内容作为操作的分区 partitions=($(cat "${ab_partitions_list}")) # 检查分区数 if [[ ${#partitions[@]} -eq 0 ]]; then die "The list of partitions is empty. Can't generate a payload." fi else warn "No ab_partitions.txt found. Using default." fi echo "List of A/B partitions: ${partitions[@]}" # All Brillo updaters support major version 2. FORCE_MAJOR_VERSION="2" # # 根据当前处理的zip文件是target包还是source包做不同的处理 # # 如果当前zip是source包 if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then # Source image local ue_config=$(create_tempfile "ue_config.XXXXXX") CLEANUP_FILES+=("${ue_config}") # 提取META/update_engine_config.txt信息到临时文件ue_config.XXXXXX中 if ! unzip -p "${image}" "META/update_engine_config.txt" \ >"${ue_config}"; then warn "No update_engine_config.txt found. Assuming pre-release image, \using payload minor version 2" fi # # 读取文件内PAYLOAD_MINOR_VERSION和PAYLOAD_MAJOR_VERSION的内容 # 测试使用的文件内容如下: # android-7.1.1_r23$ cat dist/new/META/update_engine_config.txt # PAYLOAD_MAJOR_VERSION=2 # PAYLOAD_MINOR_VERSION=3 # # For delta payloads, we use the major and minor version supported by the # old updater. FORCE_MINOR_VERSION=$(read_option_uint "${ue_config}" \ "PAYLOAD_MINOR_VERSION" 2) FORCE_MAJOR_VERSION=$(read_option_uint "${ue_config}" \ "PAYLOAD_MAJOR_VERSION" 2) # 检查PAYLOAD_MINOR_VERSION, # 如果<2,退出,因为Brillo要求delta升级方式至少为3 # Brillo support for deltas started with minor version 3. if [[ "${FORCE_MINOR_VERSION}" -le 2 ]]; then warn "No delta support from minor version ${FORCE_MINOR_VERSION}. \Disabling deltas for this source version." exit ${EX_UNSUPPORTED_DELTA} fi # 检查PAYLOAD_MINOR_VERSION, # 如果>4,则解压缩"META/zlib_fingerprint.txt"到ZLIB_FINGERPRINT if [[ "${FORCE_MINOR_VERSION}" -ge 4 ]]; then ZLIB_FINGERPRINT=$(unzip -p "${image}" "META/zlib_fingerprint.txt") fi else # 当前zip是target包的情况 # Target image local postinstall_config=$(create_tempfile "postinstall_config.XXXXXX") CLEANUP_FILES+=("${postinstall_config}") # 解压缩"META/postinstall_config.txt"到POSTINSTALL_CONFIG_FILE if unzip -p "${image}" "META/postinstall_config.txt" \ >"${postinstall_config}"; then POSTINSTALL_CONFIG_FILE="${postinstall_config}" fi fi local part part_file temp_raw filesize # # 对从"META/ab_partitions.txt"提取到的partition逐个操作 # for part in "${partitions[@]}"; do part_file=$(create_tempfile "${part}.img.XXXXXX") CLEANUP_FILES+=("${part_file}") # 将"IMAGES/{boot,system}.img"释放到{boot,system}.img.xxxx临时文件 unzip -p "${image}" "IMAGES/${part}.img" >"${part_file}" # # 检查{boot,system}.img文件头部的4个字节 # 如果是"3aff26ed", 说明是sparse image,将其转换回raw image # # If the partition is stored as an Android sparse image file, we need to # convert them to a raw image for the update. local magic=$(head --bytes=4 "${part_file}" | hexdump -e '1/1 "%.2x"') if [[ "${magic}" == "3aff26ed" ]]; then temp_raw=$(create_tempfile "${part}.raw.XXXXXX") CLEANUP_FILES+=("${temp_raw}") echo "Converting Android sparse image ${part}.img to RAW." simg2img "${part_file}" "${temp_raw}" # At this point, we can drop the contents of the old part_file file, but # we can't delete the file because it will be deleted in cleanup. true >"${part_file}" part_file="${temp_raw}" fi # delta_generator only supports images multiple of 4 KiB. For target images # we pad the data with zeros if needed, but for source images we truncate # down the data since the last block of the old image could be padded on # disk with unknown data. # # 获取{boot,system}.img的文件大小 # 对于source分区,则将其filesize向下截取到4K边界 # 对于target分区,则将其filesize向上填充到4K边界 # filesize=$(stat -c%s "${part_file}") if [[ $(( filesize % 4096 )) -ne 0 ]]; then if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then echo "Rounding DOWN partition ${part}.img to a multiple of 4 KiB." : $(( filesize = filesize & -4096 )) else echo "Rounding UP partition ${part}.img to a multiple of 4 KiB." : $(( filesize = (filesize + 4095) & -4096 )) fi truncate_file "${part_file}" "${filesize}" fi # # 更新传入参数 partitions_array # 调用时: # 传入 DST_PARTITIONS/SRC_PARTITIONS,用于区分是source还是target; # 调用完: # 传出 DST_PARTITIONS[boot/system]或SRC_PARTITIONS[boot/system] # 用于指示提取的boot.img/system.img的路径 eval "${partitions_array}[\"${part}\"]=\"${part_file}\"" echo "Extracted ${partitions_array}[${part}]: ${filesize} bytes" done}
extract_image_brillo()
函数看起来复制,但其所做的操作却比较简单:
- 从zip包的"META/ab_partitions.txt"文件中提取分区信息;
- 从zip包中解压缩boot/system image文件到临时文件,如果文件是sparse image,则将其转换回raw image;
- 以数组方式返回target/source的zip包提取到的boot/system raw image名字;
一句话,extract_image_brillo()
函数提取zip包中的boot/system image文件用于后续处理。
生成payload文件总结
brillo_update_payload
脚本中,通过函数cmd_generate()
生成payload.bin。
操作上,cmd_generate()
调用extract_image()
提取zip包中IMAGES目录中的boot.img和system.img,如果提取得到的是sparse image格式文件,则还需要进一步使用simg2img工具将其转换为raw image格式。
如果指定了target和source制作增量包,则相应会提取到4个image文件(target和source包各自的boot.img/system.img);如果只指定target制作全量包,则得到2个image文件(boot.img/system.img)。
然后,用提取到的boot.img和system.img的路径构造参数并传递给delta_generator应用程序。
以增量包为例,cmd_generate()
调用delta_generator生成payload.bin的命令为:
delta_generator -out_file=dist/payload.bin \ -partition_names=boot:system \ -new_partitions=/tmp/boot.img.oiHmvn:/tmp/system.raw.ZkArkk \ -old_partitions=/tmp/boot.img.cXD4Dt:/tmp/system.raw.IlRpgW \ --minor_version=3 --major_version=2
所以,最终通过delta_generator去生成payload.bin文件。
3.2 生成payload数据和metadata数据的哈希值
使用brillo_update_payload
脚本从payload数据生成payload哈希和metadata哈希:
brillo_update_payload hash --unsigned_payload /tmp/payload_file.bin \ --signature_size 256 \ --metadata_hash_file /tmp/metadata_sig_file.bin \ --payload_hash_file /tmp/payload_sig_file.bin
对于hash
操作,执行case
语句中$COMMAND
为hash
的分支。
在该分支中,validate_hash()
函数用于验证命令行是否设置了signature_size
/unsigned_payload
/payload_hash_file
/metadata_hash_file
等参数,然后cmd_hash()
函数将这些参数传递给delta_generator
去生成payload和metadata的哈希。
代码非常简单,甚至不需要注释:
cmd_hash() { "${GENERATOR}" \ -in_file="${FLAGS_unsigned_payload}" \ -signature_size="${FLAGS_signature_size}" \ -out_hash_file="${FLAGS_payload_hash_file}" \ -out_metadata_hash_file="${FLAGS_metadata_hash_file}" echo "Done generating hash."}
以增量包为例,cmd_hash()
调用delta_generator生成payload和metadata的哈希命令为:
delta_generator -in_file=/tmp/payload_file.bin \ -signature_size=256 \ -out_hash_file=/tmp/payload_sig_file.bin \ -out_metadata_hash_file=/tmp/metadata_sig_file.bin
好吧,最终还是通过delta_generator去生成payload和metadata的哈希。
3.3 将payload签名和metadata签名写回payload文件
使用brillo_update_payload
脚本将payload签名和metadata签名写回payload文件
brillo_update_payload sign --unsigned_payload /tmp/payload_file.bin \ --payload /tmp/signed_payload_file.bin \ --signature_size 256 \ --metadata_signature_file /tmp/signed_metadata_sig_file.bin \ --payload_signature_file /tmp/signed_payload_sig_file.bin
对于sign
操作,执行case
语句中$COMMAND
为sign
的分支。
在该分支中,validate_sign()
函数用于验证命令行是否设置了signature_size
/unsigned_payload
/payload
/payload_signature_file
/metadata_signature_file
等参数,然后cmd_sign()
函数将这些参数传递给delta_generator
去生成包含签名的payload.bin文件。
这里的代码也非常简单,不需要注释:
cmd_sign() { GENERATOR_ARGS=( -in_file="${FLAGS_unsigned_payload}" -signature_size="${FLAGS_signature_size}" -signature_file="${FLAGS_payload_signature_file}" -metadata_signature_file="${FLAGS_metadata_signature_file}" -out_file="${FLAGS_payload}" ) if [[ -n "${FLAGS_metadata_size_file}" ]]; then GENERATOR_ARGS+=( --out_metadata_size_file="${FLAGS_metadata_size_file}" ) fi "${GENERATOR}" "${GENERATOR_ARGS[@]}" echo "Done signing payload."}
以增量包为例,cmd_sign()
调用delta_generator将payload签名和metadata签名写回payload文件的命令为:
delta_generator -in_file=/tmp/payload_file.bin \ -signature_size=256 \ -signature_file=/tmp/signed_payload_file.bin \ -metadata_signature_file=/tmp/signed_metadata_sig_file.bin \ -out_file=/tmp/signed_payload_file.bin
虽然这里叫写回,其实并不是在原来的payload.bin文件上操作,而是使用
payload.bin
,signed_payload_file.bin
和signed_metadata_sig_file.bin
生成了一个新的signed_payload_file.bin
文件。
好吧,最终也还是通过delta_generator去合并上payload哈希和metadata哈希。
3.4 提取payload文件的properties数据
构造使用脚本提取payload properties的命令并执行
brillo_update_payload properties --payload /tmp/signed_payload_file.bin \ --properties_file /tmp/payload_properties_file.txt
对于properties
操作,执行case
语句中$COMMAND
为properties
的分支。
在该分支中,validate_properties()
函数用于验证命令行是否设置了payload
/properties_file
等参数,然后cmd_properties()
函数将这些参数传递给delta_generator
去提取payload文件的properties数据。
代码如下:
cmd_properties() { "${GENERATOR}" \ -in_file="${FLAGS_payload}" \ -properties_file="${FLAGS_properties_file}"}
以增量包为例,cmd_properties()
调用delta_generator提取payload文件的properties数据的命令为:
delta_generator -in_file=/tmp/signed_payload_file.bin \ -properties_file=/tmp/payload_properties_file.txt
所以,delta_generator也负责提取payload文件的properties数据的操作。
3.5 脚本brillo_update_payload总结
在生成payload.bin时,需要4步操作:
- 生成payload文件
- 生成payload和metadata数据的哈希值
- 将payload签名和metadata签名写回payload文件
- 提取payload文件的properties数据
而brillo_update_payload
脚本将这4步操作中的命令转发给delta_generator
。分别如下:
- 生成payload文件
delta_generator -out_file=dist/payload.bin \ -partition_names=boot:system \ -new_partitions=/tmp/boot.img.oiHmvn:/tmp/system.raw.ZkArkk \ -old_partitions=/tmp/boot.img.cXD4Dt:/tmp/system.raw.IlRpgW \ --minor_version=3 --major_version=2
- 生成payload和metadata数据的哈希值
delta_generator -in_file=/tmp/payload_file.bin \ -signature_size=256 \ -out_hash_file=/tmp/payload_sig_file.bin \ -out_metadata_hash_file=/tmp/metadata_sig_file.bin
- 将payload签名和metadata签名写回payload文件
delta_generator -in_file=/tmp/payload_file.bin \ -signature_size=256 \ -signature_file=/tmp/signed_payload_file.bin \ -metadata_signature_file=/tmp/signed_metadata_sig_file.bin \ -out_file=/tmp/signed_payload_file.bin
- 提取payload文件的properties数据
delta_generator -in_file=/tmp/signed_payload_file.bin \ -properties_file=/tmp/payload_properties_file.txt
然后余下的操作都在delta_generator
里面了,下一篇将会对delta_generator
代码中的这4个操作进行详细分析。
4. 联系和福利
-
个人微信公众号“洛奇看世界”,一个大龄码农的救赎之路。
- 公众号回复关键词“Android电子书”,获取超过150本Android相关的电子书和文档。电子书包含了Android开发相关的方方面面,从此你再也不需要到处找Android开发的电子书了。
- 公众号回复关键词“个人微信”,获取个人微信联系方式。我组建了一个Android OTA的讨论组,联系我,说明Android OTA,拉你进组一起讨论。
更多相关文章
- 一款常用的 Squid 日志分析工具
- GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
- “罗永浩抖音首秀”销售数据的可视化大屏是怎么做出来的呢?
- Nginx系列教程(三)| 一文带你读懂Nginx的负载均衡
- RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
- Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
- 不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...
- android sdk 超时 解决办法
- Android入门开发之SD卡读写操作