作者: 宋立新
Email:
前言OTA 升级是 Android 系统提供的标准软件升级方式。 它功能强大,提供了完全升级、增量升级模式,可以通过 SD 卡升级,也可以通过网络升级。
这里,我们先研究最简单的情况,通过 SD 卡进行完全升级。
如何执行升级就不多说了,网上有很多资料。(比如,介绍HTC手机如何升级)。我们感兴趣的是它是如何实现的,作为开发者,如何修改它以符合我们的定制化需求。
首先,我们研究一下 ota 升级包的编译过程。
Quick start首先编译出android, 然后执行:
make otapackage
即可获得:out/target/product/{product_name}/ {product_name}-ota-eng.{uid}.zip
将该文件改名为update.zip放到T卡根目录, 即可开始recovery模式下的 OTA 升级。
编译过程研究
主要分两步,第一步, 会准备一个包,其中包含升级需要的内容(原材料),比如,system 目录。
第二步,运行python 脚本 ./build/tools/releasetools/ota_from_target_files,以步骤一准备的ZIP包作为输入,最终生成需要的升级包。
步骤一
编译脚本如下:
(From: build/core/Makefile)
- 1073 # Depending on the various images guarantees that the underlying
- 1074 # directories are up-to-date.
- 1075 $(BUILT_TARGET_FILES_PACKAGE): /
- 1076 $(INSTALLED_BOOTIMAGE_TARGET) /
- 1077 $(INSTALLED_RADIOIMAGE_TARGET) /
- 1078 $(INSTALLED_RECOVERYIMAGE_TARGET) /
- 1079 $(INSTALLED_FACTORYIMAGE_TARGET) /
- 1080 $(INSTALLED_SYSTEMIMAGE) /
- 1081 $(INSTALLED_USERDATAIMAGE_TARGET) /
- 1082 $(INSTALLED_SECROIMAGE_TARGET) /
- 1083 $(INSTALLED_ANDROID_INFO_TXT_TARGET) /
- 1084 $(built_ota_tools) /
- 1085 $(APKCERTS_FILE) /
- 1086 $(HOST_OUT_EXECUTABLES)/fs_config /
- 1087 | $(ACP)
- 1088 @echo "Package target files: $@"
- 1089 $(hide) rm -rf $@ $(zip_root)
- 1090 $(hide) mkdir -p $(dir $@) $(zip_root)
- 1091 @# Components of the recovery image
- 1092 $(hide) mkdir -p $(zip_root)/RECOVERY
- 1093 $(hide) $(call package_files-copy-root, /
- 1094 $(TARGET_RECOVERY_ROOT_OUT),$(zip_root)/RECOVERY/RAMDISK)
- 1095 ifdef INSTALLED_KERNEL_TARGET
- 1096 $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/RECOVERY/kernel
- 1097 $(hide) $(ACP) $(recovery_ramdisk) $(zip_root)/RECOVERY/ramdisk
- 1098 endif
- 1099 ifdef INSTALLED_2NDBOOTLOADER_TARGET
- 1100 $(hide) $(ACP) /
- 1101 $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/RECOVERY/second
- 1102 endif
- 1103 ifdef BOARD_KERNEL_CMDLINE
- 1104 $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/RECOVERY/cmdline
- 1105 endif
- 1106 ifdef BOARD_KERNEL_BASE
- 1107 $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/RECOVERY/base
- 1108 endif
- 1109 @# Components of the factory image
- 1110 $(hide) mkdir -p $(zip_root)/FACTORY
- 1111 $(hide) $(call package_files-copy-root, /
- 1112 $(TARGET_FACTORY_ROOT_OUT),$(zip_root)/FACTORY/RAMDISK)
- 1113 ifdef INSTALLED_KERNEL_TARGET
- 1114 $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/FACTORY/kernel
- 1115 endif
- 1116 ifdef INSTALLED_2NDBOOTLOADER_TARGET
- 1117 $(hide) $(ACP) /
- 1118 $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/FACTORY/second
- 1119 endif
- 1120 ifdef BOARD_KERNEL_CMDLINE
- 1121 $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/FACTORY/cmdline
- 1122 endif
- 1123 ifdef BOARD_KERNEL_BASE
- 1124 $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/FACTORY/base
- 1125 endif
- 1126 @# Components of the boot image
- 1127 $(hide) mkdir -p $(zip_root)/BOOT
- 1128 $(hide) $(call package_files-copy-root, /
- 1129 $(TARGET_ROOT_OUT),$(zip_root)/BOOT/RAMDISK)
- 1130 ifdef INSTALLED_KERNEL_TARGET
- 1131 $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/BOOT/kernel
- 1132 $(hide) $(ACP) $(INSTALLED_RAMDISK_TARGET) $(zip_root)/BOOT/ramdisk
- 1133 endif
- 1134 ifdef INSTALLED_2NDBOOTLOADER_TARGET
- 1135 $(hide) $(ACP) /
- 1136 $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/BOOT/second
- 1137 endif
- 1138 ifdef BOARD_KERNEL_CMDLINE
- 1139 $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/BOOT/cmdline
- 1140 endif
- 1141 ifdef BOARD_KERNEL_BASE
- 1142 $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/BOOT/base
- 1143 endif
- 1144 $(hide) $(foreach t,$(INSTALLED_RADIOIMAGE_TARGET),/
- 1145 mkdir -p $(zip_root)/RADIO; /
- 1146 $(ACP) $(t) $(zip_root)/RADIO/$(notdir $(t));)
- 1147 @# Contents of the system image
- 1148 $(hide) $(call package_files-copy-root, /
- 1149 $(SYSTEMIMAGE_SOURCE_DIR),$(zip_root)/SYSTEM)
- 1150 @# Contents of the data image
- 1151 $(hide) $(call package_files-copy-root, /
- 1152 $(TARGET_OUT_DATA),$(zip_root)/DATA)
- 1153 @# Extra contents of the OTA package
- 1154 $(hide) mkdir -p $(zip_root)/OTA/bin
- 1155 $(hide) $(ACP) $(INSTALLED_ANDROID_INFO_TXT_TARGET) $(zip_root)/OTA/
- 1156 $(hide) $(ACP) $(PRIVATE_OTA_TOOLS) $(zip_root)/OTA/bin/
- 1157 @# Files that do not end up in any images, but are necessary to
- 1158 @# build them.
- 1159 $(hide) mkdir -p $(zip_root)/META
- 1160 $(hide) $(ACP) $(APKCERTS_FILE) $(zip_root)/META/apkcerts.txt
- 1161 $(hide) echo "$(PRODUCT_OTA_PUBLIC_KEYS)" > $(zip_root)/META/otakeys.txt
- 1162 $(hide) echo "$(PRIVATE_RECOVERY_API_VERSION)" > $(zip_root)/META/recovery-api-version.txt
- 1163 $(hide) echo "blocksize $(BOARD_FLASH_BLOCK_SIZE)" > $(zip_root)/META/imagesizes.txt
- 1164 $(hide) echo "boot $(call image-size-from-data-size,$(BOARD_BOOTIMAGE_PARTITION_SIZE))" >> $(zip_root)/META/imagesizes.txt
- 1165 $(hide) echo "recovery $(call image-size-from-data-size,$(BOARD_RECOVERYIMAGE_PARTITION_SIZE))" >> $(zip_root)/META/imagesizes.txt
- 1166 $(hide) echo "system $(call image-size-from-data-size,$(BOARD_SYSTEMIMAGE_PARTITION_SIZE))" >> $(zip_root)/META/imagesizes.txt
- 1167 $(hide) echo "secro $(call image-size-from-data-size,$(BOARD_SECROIMAGE_PARTITION_SIZE))" >> $(zip_root)/META/imagesizes.txt
- 1168 $(hide) echo "userdata $(call image-size-from-data-size,$(BOARD_USERDATAIMAGE_PARTITION_SIZE))" >> $(zip_root)/META/imagesizes.txt
- 1169 $(hide) echo "$(tool_extensions)" > $(zip_root)/META/tool-extensions.txt
- 1170 @# Zip everything up, preserving symlinks
- 1171 $(hide) (cd $(zip_root) && zip -qry ../$(notdir $@) .)
- 1172 @# Run fs_config on all the system files in the zip, and save the output
- 1173 $(hide) zipinfo -1 $@ | awk -F/ 'BEGIN { OFS="/" } /^SYSTEM/// {$$1 = "system"; print}' | $(HOST_OUT_EXECUTABLES)/fs_config > $(zip_root)/META/filesystem_config.txt
- 1174 $(hide) (cd $(zip_root) && zip -q ../$(notdir $@) META/filesystem_config.txt)
可见往里面添加了很多内容。
L1089-1090 , 造一个目录。
L1091-1108,填充 RECOVERY 子目录的内容。用于生成recovery.img。包括:kernel 的image, recovery 根文件系统的 image, recovery 根文件系统的内容:RECOVERY$ tree -L 2├── kernel├── ramdisk└── RAMDISK ├── advanced_meta_init.rc ├── data ├── default.prop ├── dev ├── etc ├── init ├── init.factory.rc ├── init.goldfish.rc ├── init.mt6516.rc ├── init.rc ├── meta_init.rc ├── proc ├── res ├── sbin ├── sys ├── system └── tmpL1109-1125, 填充 FACTORY 子目录的内容, 没有用到,包括:kernel 的imageL1126-1143, 填充 BOOT子目录的内容,用于生成boot.img。和 RECOVERY目录类似,包括:kernel 的image,根文件系统的 image,根文件系统的内容:BOOT$ tree -L 2.├── kernel├── ramdisk└── RAMDISK ├── advanced_meta_init.rc ├── data ├── default.prop ├── dev ├── init ├── init.factory.rc ├── init.goldfish.rc ├── init.mt6516.rc ├── init.rc ├── meta_init.rc ├── proc ├── res -> /system/res ├── sbin ├── sys └── system L1144-1146, 填充 RADIO子目录的内容, 没有用到。L1147-1149, 填充 SYSTEM子目录的内容。 这是升级的主要内容。L1150-1152, 填充 DATA子目录的内容。缺省没有用到。L1153-1156, 填充 OTA/bin子目录的内容,这是OTA升级自己使用的程序。后面会遇到。OTA/bin$ tree.├── applypatch├── applypatch_static├── check_prereq└── updaterL1159-1169, 填充 META子目录的内容,这里包含了OTA脚本需要的一些附加信息。L1170-1171,将所有内容打包。供下一阶段使用。L1173-1174,生成 META/filesystem_config.txt 并将其加入到 zip 包中。该文件保存了 system 目录下各目录、文件的权限及 owner.$ head META/filesystem_config.txtsystem 0 0 755system/usr 0 0 755system/usr/srec 0 0 755system/usr/srec/config 0 0 755system/usr/srec/config/en.us 0 0 755system/usr/srec/config/en.us/grammars 0 0 755system/usr/srec/config/en.us/grammars/phone_type_choice.g2g 0 0 644system/usr/srec/config/en.us/grammars/VoiceDialer.g2g 0 0 644system/usr/srec/config/en.us/grammars/boolean.g2g 0 0 644system/usr/srec/config/en.us/g2p 0 0 755 这里,目录由 zipinfo –l 提供, 而权限则由 fs_config 设定。此程序的源码位于:build/tools/fs_config, 其中fs_config 包含了一个头文件: #include ""这个文件(system/core/include/private/android_filesystem_config.h)以hardcoding 的方式设定了 system 下各目录、文件的权限、属主。比如: { 00440, , , "system/etc/init.goldfish.rc" }, { 00550, , , "system/etc/init.goldfish.sh" }, { 00440, , , "system/etc/init.trout.rc" }, { 00550, , , "system/etc/init.ril" }, 如果需要升级其它内容,比如 bootloader, 则可以在这里加入。 步骤二(From: build/core/Makefile)
- 1186 name := $(TARGET_PRODUCT)
- 1187 ifeq ($(TARGET_BUILD_TYPE),debug)
- 1188 name := $(name)_debug
- 1189 endif
- 1190 name := $(name)-ota-$(FILE_NAME_TAG)
- 1191
- 1192 INTERNAL_OTA_PACKAGE_TARGET := $(PRODUCT_OUT)/$(name).zip
- 1193
- 1194 $(INTERNAL_OTA_PACKAGE_TARGET): KEY_CERT_PAIR := $(DEFAULT_KEY_CERT_PAIR)
- 1195
- 1196 ifeq ($(TARGET_OTA_SCRIPT_MODE),)
- 1197 # default to "auto"
- 1198 $(INTERNAL_OTA_PACKAGE_TARGET): scriptmode := auto
- 1199 else
- 1200 $(INTERNAL_OTA_PACKAGE_TARGET): scriptmode := $(TARGET_OTA_SCRIPT_MODE)
- 1201 endif
- 1202
- 1203 $(INTERNAL_OTA_PACKAGE_TARGET): $(BUILT_TARGET_FILES_PACKAGE) $(OTATOOLS)
- 1204 @echo "Package OTA: $@"
- 1205 $(hide) ./build/tools/releasetools/ota_from_target_files /
- 1206 -m $(scriptmode) /
- 1207 -p $(HOST_OUT) /
- 1208 -k $(KEY_CERT_PAIR) /
- 1209 $(BUILT_TARGET_FILES_PACKAGE) $@
核心是一个python脚本: ota_from_target_files, 它以前一步骤生成的ZIP包作为输入,生成可用于OTA升级的zip包。 具体内容我们后文继续分析。
Android OTA 升级之二:脚本 ota_from_target_files 作者: 宋立新 Email: 前面介绍了ota package 的编译过程,其中最核心的部分就是一个 python 脚本:ota_from_target_files. 现在我们分析这个脚本。 不带任何参数,先看一下它的帮助:
简单翻译一下:
-b 过时,不再使用。
-k 签名用的密钥
-i 生成增量OTA包时用于定义对比包
-w 是否清除 userdata 分区
-n 是否在升级时不检查时间戳,缺省情况下只能基于老的版本升级。
-e 定义额外运行的脚本
-m 定义采用的脚本格式,目前有两种,amend & edify, 其中amend为较老的格式。对应的,升级时会采用不同的解释器。缺省情况下,ota_from_target_files 会同时生成两个脚本。这提供了最大灵活性。
-p 定义脚本用到的一些可执行文件的路径
-s 定义额外运行的脚本的路径
-x 定义额外运行的脚本可能用到的键/值对
-v 老朋友,冗余模式,让脚本打印出执行的命令
-h 老朋友,这个就不用说了吧。
我们调用如下命令生成我们的升级包:
./build/tools/releasetools/ota_from_target_files /
-m auto /
-p out/host/linux-x86 /
-k build/target/product/security/testkey -n /
out/target/product/{product-name}/obj/PACKAGING/target_files_intermediates/{product-name}-target_files-eng.{uid}.zip {output_zip}
再看内容ota_from_target_files为python 脚本,所以如果懂 python, 会更顺利一点。
文件有1000行。分析过程中,我们只是贴代码片段。 完整文件见:
build/tools/releasetools/ota_from_target_files (from Android 2.2)
入口:main
按照python惯例,单独执行的代码执行从__main__开始:
if __name__ == '__main__': try: main(sys.argv[1:]) except common.ExternalError, e: print print " ERROR: %s" % (e,) print sys.exit(1)它调用 main 函数:
- 844 def main(argv):
- 845
- 846 def option_handler(o, a):
- 847 if o in ("-b", "--board_config"):
- 848 pass # deprecated
- 849 elif o in ("-k", "--package_key"):
- 850 OPTIONS.package_key = a
- 851 elif o in ("-i", "--incremental_from"):
- 852 OPTIONS.incremental_source = a
- 853 elif o in ("-w", "--wipe_user_data"):
- 854 OPTIONS.wipe_user_data = True
- 855 elif o in ("-n", "--no_prereq"):
- 856 OPTIONS.omit_prereq = True
- 857 elif o in ("-e", "--extra_script"):
- 858 OPTIONS.extra_script = a
- 859 elif o in ("-m", "--script_mode"):
- 860 OPTIONS.script_mode = a
- 861 elif o in ("--worker_threads"):
- 862 OPTIONS.worker_threads = int(a)
- 863 else:
- 864 return False
- 865 return True
- 866
- 867 args = common.ParseOptions(argv, __doc__,
- 868 extra_opts="b:k:i:d:wne:m:",
- 869 extra_long_opts=["board_config=",
- 870 "package_key=",
- 871 "incremental_from=",
- 872 "wipe_user_data",
- 873 "no_prereq",
- 874 "extra_script=",
- 875 "script_mode=",
- 876 "worker_threads="],
- 877 extra_option_handler=option_handler)
- 878
- 879 if len(args) != 2:
- 880 common.Usage(__doc__)
- 881 sys.exit(1)
- 892 if OPTIONS.device_specific is None:
- 893 # look for the device-specific tools extension location in the input
- 894 try:
- 895 f = open(os.path.join(OPTIONS.input_tmp, "META", "tool-extensions.txt"))
- 896 ds = f.read().strip()
- 897 f.close()
- 898 if ds:
- 899 ds = os.path.normpath(ds)
- 900 print "using device-specific extensions in", ds
- 901 OPTIONS.device_specific = ds
- 902 except IOError, e:
- 903 if e.errno == errno.ENOENT:
- 904 # nothing specified in the file
- 905 pass
- 906 else:
- 907 raise
签名(如果需要的话),处理完毕。
下面我们看主要功能函数:WriteFullOTAPackage。
2011-08-23 14:56:25| 分类: Android OTA升级 | 标签: |字号大中小
def WriteFullOTAPackage(input_zip, output_zip):
if OPTIONS.script_mode == "auto": script = both_generator.BothGenerator(2) elif OPTIONS.script_mode == "amend": script = amend_generator.AmendGenerator() else: # TODO: how to determine this? We don't know what version it will # be installed on top of. For now, we expect the API just won't # change very often. script = edify_generator.EdifyGenerator(2) 首先,我们获得脚本生成器,他们的实现见脚本:edify_generator.py 等。 metadata = {"post-build": GetBuildProp("ro.build.fingerprint", input_zip), "pre-device": GetBuildProp("ro.product.device", input_zip), "post-timestamp": GetBuildProp("ro.build.date.utc", input_zip), } 获得一些环境变量,来自android 环境变量。 Google 一下即知其义。 device_specific = common.DeviceSpecificParams( input_zip=input_zip, input_version=GetRecoveryAPIVersion(input_zip), output_zip=output_zip, script=script, input_tmp=OPTIONS.input_tmp, metadata=metadata) 设备相关参数,不深究。 if not OPTIONS.omit_prereq: ts = GetBuildProp("ro.build.date.utc", input_zip) script.AssertOlderBuild(ts) 如果需要,在脚本中增加一个Assert语句,要求update zip包只能用于升级老的系统。 AppendAssertions(script, input_zip) 如果需要,在脚本中增加一个Assert语句,要求update zip包只能用于同一设备,即目标设备的 ro.product.device必须跟update.zip中的相同。
device_specific.FullOTA_Assertions() Callback, 用于调用设备相关代码。调用时机为即将开始升级。类似还有:FullOTA_InstallEnd IncrementalOTA_Assertions IncrementalOTA_VerifyEnd。 不深究。 script.ShowProgress(0.5, 0) 在升级脚本中加入显示进度的语句, 参数一表示底下的操作(到下一条同类语句或者到末尾)将暂用的时间在总体时间的比例。参数二用于控制显示的速度。比如,50 则表示底下的操作估计50秒内完成,要求进度条显示线程用50秒显示这一部分的进度。0 表示不自动更新,手动控制(使用SetProgress) if OPTIONS.wipe_user_data: script.FormatPartition("userdata") 如果需要,在脚本中增加语句,擦除 userdata 分区。 script.FormatPartition("system") 在脚本中增加语句,擦除 system分区。 script.Mount("MTD", "system", "/system") 在脚本中增加语句,安装 system分区到 /system 目录。 script.UnpackPackageDir("recovery", "/system") script.UnpackPackageDir("system", "/system")在脚本中增加语句,将recovery以及system中的内容拷贝到 /system目录。其中recovery 目录包含一个patch 以及应用该patch 的脚本。 symlinks = CopySystemFiles(input_zip, output_zip) script.MakeSymlinks(symlinks) 386 行从输入 ZIP 包 /system 拷贝文件到输出 ZIP 包 /system。由于这个过程不支持链接文件,所以它将这些文件返回。 于 387 行做继续处理。该行建立这些link 文件。所有的link文件都指向 toolbox boot_img = File("boot.img", common.BuildBootableImage( os.path.join(OPTIONS.input_tmp, "BOOT"))) recovery_img = File("recovery.img", common.BuildBootableImage( os.path.join(OPTIONS.input_tmp, "RECOVERY"))) MakeRecoveryPatch(output_zip, recovery_img, boot_img) 这个复杂,MakeRecoveryPatch 做了两件事:1.在输出 ZIP包中生成一个patch: recovery/recovery-from-boot.p(boot.img和 recovery.img的patch), 它最后会位于:system/recovery-from-boot.p2.在输出 ZIP包中生成一个脚本:recovery/etc/install-recovery.sh , 它最后会位于system/etc/install-recovery.sh.该脚本的内容为:#!/system/bin/shif ! applypatch -c MTD:recovery:2048:6a167ffb86a4a16cb993473ce0726a3067163fc1; then log -t recovery "Installing new recovery image" applypatch MTD:boot:2324480:9a72a20a9c2f958ba586a840ed773cf8f5244183 MTD:recovery f6c2a70c5f2b02b6a49c9f5c5507a45a42e2d389 2564096 9a72a20a9c2f958ba586a840ed773cf8f5244183:/system/recovery-from-boot.pelse log -t recovery "Recovery image already installed"fi Item.GetMetadata(input_zip) 从 META/filesystem_config.txt 中获得 system 目录下的各文件权限信息。 Item.Get("system").SetPermissions(script) 在脚本中增加语句,设置 system 目录下文件的权限及属主等。 common.CheckSize(boot_img.data, "boot.img") 检查 boot.img 文件大小是否超标. common.ZipWriteStr(output_zip, "boot.img", boot_img.data) 将boot.img 放到输出 ZIP 包中。 script.ShowProgress(0.2, 0) script.ShowProgress(0.2, 10) 更行进度条。 script.WriteRawImage("boot", "boot.img") 在脚本中增加语句,将 boot.img 写到 boot 分区。 script.ShowProgress(0.1, 0) 更行进度条。 device_specific.FullOTA_InstallEnd() Callback, 同前。 if OPTIONS.extra_script is not None: script.AppendExtra(OPTIONS.extra_script) 如果有额外脚本,加入。 script.UnmountAll() 在脚本中增加语句,umount 所有分区。 script.AddToZip(input_zip, output_zip) 1)将前面生成的脚本输出到:META-INF/com/google/android/updater-script (对于edify)
- assert(getprop("ro.product.device") == "thedevicename" ||
- getprop("ro.build.product") == "theproductname");
- show_progress(0.500000, 0);
- format("MTD", "system");
- mount("MTD", "system", "/system");
- package_extract_dir("recovery", "/system");
- package_extract_dir("system", "/system");
- symlink("dumpstate", "/system/bin/dumpcrash");
- symlink("toolbox", "/system/bin/cat", "/system/bin/chmod",
- "/system/bin/chown", "/system/bin/cmp", "/system/bin/date",
- "/system/bin/dd", "/system/bin/df", "/system/bin/dmesg",
- "/system/bin/fb2bmp", "/system/bin/getevent", "/system/bin/getprop",
- "/system/bin/hd", "/system/bin/id", "/system/bin/ifconfig",
- "/system/bin/iftop", "/system/bin/insmod", "/system/bin/ioctl",
- "/system/bin/kill", "/system/bin/ln", "/system/bin/log",
- "/system/bin/ls", "/system/bin/lsmod", "/system/bin/mkdir",
- "/system/bin/mount", "/system/bin/mv", "/system/bin/netstat",
- "/system/bin/newfs_msdos", "/system/bin/notify", "/system/bin/printenv",
- "/system/bin/ps", "/system/bin/reboot", "/system/bin/renice",
- "/system/bin/rm", "/system/bin/rmdir", "/system/bin/rmmod",
- "/system/bin/route", "/system/bin/schedtop", "/system/bin/sendevent",
- "/system/bin/setconsole", "/system/bin/setprop", "/system/bin/sleep",
- "/system/bin/smd", "/system/bin/start", "/system/bin/stop",
- "/system/bin/sync", "/system/bin/top", "/system/bin/umount",
- "/system/bin/vmstat", "/system/bin/watchprops",
- "/system/bin/wipe");
- set_perm_recursive(0, 0, 0755, 0644, "/system");
- set_perm_recursive(0, 2000, 0755, 0755, "/system/bin");
- set_perm(0, 3003, 02755, "/system/bin/netcfg");
- set_perm(0, 3004, 02755, "/system/bin/ping");
- set_perm_recursive(1002, 1002, 0755, 0440, "/system/etc/bluez");
- set_perm(0, 0, 0755, "/system/etc/bluez");
- set_perm(1002, 1002, 0440, "/system/etc/dbus.conf");
- set_perm(1014, 2000, 0550, "/system/etc/dhcpcd/dhcpcd-run-hooks");
- set_perm(0, 2000, 0550, "/system/etc/init.goldfish.sh");
- set_perm(0, 0, 0544, "/system/etc/install-recovery.sh");
- set_perm_recursive(0, 0, 0755, 0555, "/system/etc/ppp");
- set_perm_recursive(0, 2000, 0755, 0755, "/system/xbin");
- show_progress(0.200000, 0);
- show_progress(0.200000, 10);
- assert(package_extract_file("boot.img", "/tmp/boot.img"),
- write_raw_image("/tmp/boot.img", "boot"),
- delete("/tmp/boot.img"));
- show_progress(0.100000, 0);
- unmount("/system");
将前面获取的metadata 写入输出包的文件中: META-INF/com/android/metadata
至此,我们就得到了一个update.zip包。可以开始升级了。
思考1) 虽然提供了更新recovery分区的机制,但是没有看到触发该更新的语句。所以,缺省的情况是不会更新recovery分区的。大概是为了安全的原因吧。 但是,有时确实需要更新recovery 分区(比如,设备的硬件配置、分区表等发生改变),这该如何操作呢?
Android OTA 升级之三:生成recovery.img 作者: 宋立新 Email: 得到了ota升级包后,我们就可以用它来升级系统了。Android 手机开机后,会先运行 bootloader。 Bootloader 会根据某些判定条件(比如按某个特殊键)决定是否进入 recovery 模式。Recovery 模式会装载 recovery 分区, 该分区包含recovery.img。recovery.img 包含了标准内核(和boot.img中的内核相同)以及recovery 根文件系统。下面我们看一下它是如何生成的。 (From:
build/core/Makefile) INSTALLED_RECOVERYIMAGE_TARGET 为我们的编译目标: 它依赖很多其它目标: 2.INSTALLED_RAMDISK_TARGET,标准根文件系统 ramdisk.img: 4. recovery_binary,
Recovery可执行程序,源码位于:bootable/recovery 5.
recovery_initrc,recovery模式的init.rc, 位于 bootable/recovery/etc/init.rc 6. recovery_kernel,
recovery 模式的kernel, 同标准内核 9.
recovery_resource_deps, recovery 模式使用的res, 位于:recovery/custom/{product_name}/res, 以及设备自定义部分(我们没用到) 10. RECOVERY_INSTALL_OTA_KEYS,
ota 密钥: 准备recovery目录:out/target/product/{product_name}/recovery 及其子目录: ./root ./root/etc ./root/tmp 从标准根文件系统拷贝所有文件, 删除其res 目录。 和内核一起,生成recovery.img $
tree . ├── advanced_meta_init.rc ├── data ├── default.prop ├── dev ├── etc ├── init ├── init.factory.rc ├── init.goldfish.rc ├── init.quacomm.rc ├── init.rc ├── meta_init.rc ├── proc ├── res │ ├── images │ │ ├── icon_error.png │ │ ├── icon_installing.png │ │ ├── indeterminate1.png │ │ ├── indeterminate2.png │ │ ├── indeterminate3.png │ │ ├── indeterminate4.png │ │ ├── indeterminate5.png │ │ ├── indeterminate6.png │ │ ├── progress_empty.png │ │ └── progress_fill.png │ └── keys ├── sbin │ ├── adbd │ ├── advanced_meta_init │ ├── meta_init │ ├── meta_tst │ └── recovery ├── sys ├── system └── tmp
Android OTA 升级之四:进入根文件系统 作者: 宋立新 Email: 从bootloader 进入Recovery 模式后,首先也是运行Linux内核,该内核跟普通模式没有区别(减轻了BSP开发者的任务)。区别从执行文件系统开始。 Recovery 模式的细节就隐藏在其根文件系统中。 下面,我们就看看进入Recovery 根文件系统都干些啥。 和正常启动一样,内核进入文件系统会执行/init, init 的配置文件就是 /init.rc, 前面文章讲过,这个文件来自:bootable/recovery/etc/init.rc,下面,我们看看它的内容。 可以看到,它很非常简单: 1) 设置几个环境变量。备用。 2) 建立 etc 链接。 3) 造几个目录。备用。 4) Mount /tmp 目录为内存文件系统 tmpfs,后面会用到。 5) Trival 设置,不必关心。 6) 启动 recovery主程序。 7) 如果是eng模式(此时persist.service.adb.enable),启动adb 当然,init主程序还会装载属性配置文件 /default.prop, 它包含了很多系统属性设置,比如,ro.build.*, 等等。 很明显,这里最重要的就是recovery主程序,下面,我们分析它。 Recovery.c 中,作者写了一段注释,对我们理解recovery的实现很有帮助,下面看一下:(我就不翻译了) 将标准输出和标准错误输出重定位到"/tmp/recovery.log",如果是eng模式,就可以通过adb pull
/tmp/recovery.log, 看到当前的log信息,这为我们提供了有效的调试手段。后面还会看到,recovery模式运行完毕后,会将其拷贝到cache分区,以便后续分析。 从misc 分区以及 CACHE:recovery/command 文件中读入参数,写入到argc, argv ,并且,如果有必要,回写入misc分区。这样,如果recovery没有操作成功(比如,升级还没有结束,就拔电池),系统会一直进入recovery模式。提醒用户继续升级,直到成功。 重启。 下面我们分析核心函数 它主要调用函数完成功能。 // If the package contains
an update binary, extract it and run it. static int (const char *, *zip) { const *
binary_entry = (zip, ); if (binary_entry == )
{ return INSTALL_CORRUPT; } char* = "/tmp/update_binary"; (); int = (,
0755); if ( < 0)
{ ("Can't make %s/n", ); return 1; } = (zip, binary_entry, ); ();
if (!)
{ ("Can't copy %s/n", ); return 1; } int pipefd[2]; (pipefd);
// When executing the update
binary contained in the package, the // arguments passed
are: // // - the
version number for this interface // // - an fd to
which the program can write in order to update the // progress
bar. The program can write single-line
commands: // // progress
// fill
up the next // over
// set_progress
commands to manually control the // progress
of this segment of the bar // // set_progress
// // progress
bar within the segment defined by the most // recent
progress command. // // firmware
<"hboot"|"radio"> // arrange
to install the contents of // given
partition on reboot. // // (API
v2: // indicate
taking a file from the OTA package.) // // (API
v3: this command no longer exists.) // // ui_print
// display
// // - the name
of the package zip file. //
注意看这段注释,它解释了以下代码的行为。结合代码,可知: 1) 将会创建新的进程,执行:/tmp/update_binary 2) 同时,会给该进程传入一些参数,其中最重要的就是一个管道fd,供新进程与原进程通信。 3) 新进程诞生后,原进程就变成了一个服务进程,它提供若干UI更新服务: a) progress b) set_progress c) ui_print 这样,新进程就可以通过老进程的UI系统完成显示任务。而其他功能就靠它自己了。 char** = (sizeof(char*) * 5); [0] = ; [1] = (RECOVERY_API_VERSION); //
defined in Android.mk [2] = (10); ([2], "%d", pipefd[1]); [3] =
(char*); [4] = ;
= (); if ( == 0)
{ (pipefd[0]); (, ); (, "E:Can't run %s (%s)/n", , ()); (-1); } (pipefd[1]);
char [1024]; *
from_child = (pipefd[0], "r"); while ((,
sizeof(),
from_child) != )
{ char* = (, " /n"); if ( == )
{ continue; } else if ((, "progress") == 0) { char*
fraction_s = (, "
/n"); char*
seconds_s = (, "
/n");
float
fraction = (fraction_s, ); int = (seconds_s, ,
10);
(fraction * (1-), ); } else if ((, "set_progress") == 0) { char*
fraction_s = (, "
/n"); float
fraction = (fraction_s, ); (fraction); } else if ((, "ui_print") == 0) { char* = (, "/n"); if ()
{ (); } else
{ ("/n"); } } else
{ ("unknown command [%s]/n", ); } } (from_child);
int ; (, &,
0); if (!()
|| () !=
0) { ("Error in %s/n(Status %d)/n", , ()); return
INSTALL_ERROR; }
return
INSTALL_SUCCESS; } 这样,我们又回到了升级包中的文件:META-INF/com/google/android/update-binary,下回继续研究。
Android OTA 升级之五:updater 作者: 宋立新 Email: 可以说,前面分析的OTA升级的各部分代码都是在搭一个舞台,而主角现在终于登场,它就是updater.
Google的代码架构设计非常好,各部分尽量松耦合。前面介绍升级脚本时,可知有两种类型的脚本,amend &
edify. 他们各自对应一个updater. 这里,我们主要关注新的edify的updater. Updater可以作为学习解释器/编译器的同学一个很好的实例,但是我们只关心产品化相关的内容,所以并不去深究lex/yacc相关的东西。 (from:
bootable/recovery/updater/updater.c) // Where in the package we
expect to find the edify script to execute. // (Note it's "updateR-script", not
the older "update-script".) #define "META-INF/com/google/android/updater-script" 这里定义脚本的位置,注释说明本updater支持edify格式的脚本。 int (int ,
char** )
{ // Various things
log information to stdout or stderr more or less // at
random. The log file makes more sense if buffering
is // turned off so
things appear in the right order. (, ); (, ); if ( != 4)
{ (, "unexpected number of arguments (%d)/n", ); return 1; } char* = [1]; if (([0]
!= '1' && [0]
!= '2' && [0]
!= '3') || [1]
!= '/0') { // We support
version 1, 2, or 3. (, "wrong updater binary API; expected 1, 2, or 3;
" "got
%s/n", [1]); return 2; } 获取 version 参数。 // Set up the pipe
for sending commands back to the parent process. int = ([2]); *
cmd_pipe = (, "wb"); (cmd_pipe); 获取命令管道(用于图形显示等,见前篇) // Extract the
script from the package. char* package_data = [3]; za; int ; = (package_data, &za); if ( != 0)
{ (, "failed to open package %s: %s/n", package_data, ()); return
3; }
const *
script_entry = (&za, ); if (script_entry
== )
{ (, "failed to find %s in %s/n", , package_data); return
4; }
char* = (script_entry->uncompLen+1); if (!(&za, script_entry, ,
script_entry->uncompLen)) { (, "failed to read script from
package/n"); return
5; } [script_entry->uncompLen] = '/0';
读入脚本 META-INF/com/google/android/updater-script //
Configure edify's functions.
(); (); RegisterDeviceExtensions(); ();
注册语句处理函数 // Parse
the script.
* ; int = 0; (); int = (&, &); if ( != 0
|| > 0) { (, "%d parse errors/n", ); return
6; }
调用yy* 库函数解析脚本。 //
Evaluate the parsed script.
updater_info; updater_info.cmd_pipe
= cmd_pipe; updater_info.package_zip
= &za; updater_info. = ();
; . =
&updater_info; . = ; . = ;
char* = (&, ); if ( == )
{ if (. == )
{ (, "script aborted (no error
message)/n"); (cmd_pipe, "ui_print script aborted (no error
message)/n"); } else
{ (, "script aborted: %s/n", .); char* = (., "/n"); while ()
{ (cmd_pipe, "ui_print %s/n", ); = (, "/n"); } (cmd_pipe, "ui_print/n"); } (.); return
7; } else { (, "script result was [%s]/n", ); (); } 解释执行脚本。 核心函数是 。它会调用其他callback函数,而这些callback函数又会调用Evaluate去解析不同的脚本片段。从而实现一个简单的解释器。
(&za); ();
return 0; } 还没开始,就结束了。代码非常简单,因为细节隐藏在那些callback函数里。我们看一下。 这些语句控制执行流程。 这些语句执行各种功能。基本上,我们只需要知道用法就可以了。值得注意的是,run_program原语允许我们去执行自定义程序,这应该足够满足我们的个性化需求了。