标签:
前段时间学习了AndroidRecovery模式及OTA升级过程,为加深理解和防止以后遗忘,所以写这篇文档进行一个总结和梳理,以便日后查阅回顾。文档主要包括两部分,第一部分为OTA升级包的制作过程分析,第二部分为Recovery模式下OTA升级包安装过程的分析,其中包括Recovery模式分析及服务流程。
《Recovery 开发指导》
《Android系统Recovery工作原理之使用update.zip升级过程分析》
《Android系统启动过程分析》
所谓OTA(Over-the-AirTechnology)是指手机终端通过无线网络下载远程服务器上的升级包,对系统或应用进行升级的技术。有关网络部分不做过多讨论,本文重点放在系统升级这一概念上。
图1 某android手机存储设备结构图
以PC机进行类比,假设计算机操作系统装在C盘,当加电启动时,引导程序会将C盘的系统程序装入内存并运行,而系统升级或重装系统,则是将C盘中原来的系统文件部分或全部重写。对于手机及其上的ANDROID系统而言,同样如此,需要一个存储系统文件的“硬盘”。
图1 是某款手机的存储设备结构图,其存储区(红色框图部分)分为四部分:SRAM、Nand Flash、SDRAM及外设地址空间。其中Nand Flash中存储着全部系统数据(通过专门的烧写工具将编译后的映象文件download到Nand Flash中,工具由IC厂商提供),包括boot.img、system.img、recovery.img等,因此Nand Flash即是上文所说的手机上的“硬盘”。图1最右部分(图中绿色框图部分)是Nand Flash存储区更详细的划分,我们将主要关注system分区(蓝色框图),因为OTA升级主要是对这部分系统数据的重写(当然boot分区也可升级)。除此之外,蓝黑色区域标示的misc分区也应值得注意,它在OTA升级中发挥着重要的作用。
OK,一言以蔽之,所谓OTA就是将升级包(zip压缩包)写入到系统存储区,因此我们需要考虑两个问题:1、升级包是如何生成的?2、升级包是如何写入到system分区的?
2.2.1、 update.zip包的目录结构
|----boot.img
|----system/
|----recovery/
`|----recovery-from-boot.p
`|----etc/
`|----install-recovery.sh
|---META-INF/
`|CERT.RSA
`|CERT.SF
`|MANIFEST.MF
`|----com/
`|----google/
`|----android/
`|----update-binary
`|----updater-script
`|----android/
`|----metadata
2.2.2、 update.zip包目录结构详解
以上是我们用命令makeotapackage 制作的update.zip包的标准目录结构。(和实际的略有不同)
1、boot.img是更新boot分区所需要的文件。这个boot.img主要包括kernel+ramdisk,包括应用会用到的一些库等等。可以将Android源码编译out/target/product/ xxxx /system/中的所有文件拷贝到这个目录来代替。
2、system/目录的内容在升级后会放在系统的system分区。主要用来更新系统的一些应用或则应用会用到的一些库等等。可以将Android源码编译out/target/product/xxxx/system/中的所有文件拷贝到这个目录来代替。
3、recovery/目录中的recovery-from-boot.p是boot.img和recovery.img的补丁(patch),主要用来更新recovery分区,其中etc/目录下的install-recovery.sh是执行更新的脚本。
4、update-binary是一个二进制文件,相当于一个脚本解释器,能够识别updater-script中描述的操作。它是Android源码编译后生成的out/target/product/xxxx/system bin/updater文件,可将updater重命名为update-binary得到。该文件在具体的更新包中的名字由源码中bootable/recovery/install.c中的宏ASSUMED_UPDATE_BINARY_NAME的值而定。
5、updater-script:此文件是一个脚本文件,具体描述了更新过程。我们可以根据具体情况编写该脚本来适应我们的具体需求。该文件的命名由源码中bootable/recovery/updater/updater.c文件中的宏SCRIPT_NAME的值而定。
6、 metadata文件是描述设备信息及环境变量的元数据。主要包括一些编译选项,签名公钥,时间戳以及设备型号等。
7、我们还可以在包中添加userdata目录,来更新系统中的用户数据部分。这部分内容在更新后会存放在系统的/data目录下。
8、update.zip包的签名:update.zip更新包在制作完成后需要对其签名,否则在升级时会出现认证失败的错误提示。而且签名要使用和目标板一致的加密公钥。加密公钥及加密需要的三个文件在Android源码编译后生成的具体路径为:
out/host/linux-x86/framework/signapk.jar
build/target/product/security/testkey.x509.pem
build/target/product/security/testkey.pk8。
我们用命令makeotapackage制作生成的update.zip包是已签过名的,如果自己做update.zip包时必须手动对其签名。具体的加密方法:
$ java –jar gingerbread/out/host/linux/framework/signapk.jar –wgingerbread/build/target/product/security/testkey.x509.pem gingerbread/build/target/product/security/testkey.pk8update.zip update_signed.zip
以上命令在update.zip包所在的路径下执行,其中signapk.jartestkey.x509.pem以及testkey.pk8文件的引用使用绝对路径。update.zip 是我们已经打好的包,update_signed.zip包是命令执行完生成的已经签过名的包。
9、MANIFEST.MF:这个manifest文件定义了与包的组成结构相关的数据。类似Android应用的mainfest.xml文件。
10、CERT.RSA:与签名文件相关联的签名程序块文件,它存储了用于签名JAR文件的公共签名。
11、CERT.SF:这是JAR文件的签名文件,其中前缀CERT代表签名者。
另外,在具体升级时,对update.zip包检查时大致会分三步:①检验SF文件与RSA文件是否匹配。②检验MANIFEST.MF与签名文件中的digest是否一致。③检验包中的文件与MANIFEST中所描述的是否一致。
12、在做的MTK平台的项目(如8670、9976),需要增加与项目强相关的适配文件(scatter.txt、SEC_VER.txt、type.txt),scatter.txt分散加载文件,将可执行映像文件分散加载到不同的内存段(文件内容:指定不同内存段的起始地址)。
type.txt是build升级包过程生成的,里面的值1代表FullOTA,0代表DiffOTA,android的上层的update流程中会check这个值。
scatter.txt也是build升级包过程生成的,里面的内容来自于/mediatek/misc/ota_scatter.txt。具体code可见脚本ota_from_target_files。而mediatek/misc/ota_scatter.txt是在build full ota时会产生。该文件主要用于在升级的时候check升级前后parition layout是否有改变。
SEC_VER.TXT是在编译时从alps\mediatek\custom\$PROJECT\security\recovery下copy过来的,用于在打开SUPPORT_SBOOT_UPDATE之后会使用,具体可以参考[FAQ05739] SD或者OTA升级secutiry device和non-security device的区别。
在./mk new时,会执行ptgen,执行ptgen会依赖OTA_SCATTER_FILE,见mediatek/build/libs/pregen.mk:218,然后再build/tools/makeMtk.mk中的142 以及 692会生成ota_scatter.txt。
升级包有整包与差分包之分。顾名思义,所谓整包即包含整个system分区中的数据文件;而差分包则仅包含两个版本之间改动的部分。利用整包升级好比对电脑进行重作系统,格式化系统分区,并将新系统数据写入分区;而利用差分包升级不会格式化system分区,只是对其中部分存储段的内容进行重写。除升级包之外,制作过程中还会涉及到另一种zip包,代码中称之为8675-cota-target_files-xxx.zip,我称之为差分资源包。首先阐述下整包的制作过程。
系统经过整编后,执行makeotapackage命令,即可完成整包的制作,而此命令可分为两个阶段进行。首先执行./build/core/Makefile中的代码:
#-----------------------------------------------------------------
# OTA update package
name := $(TARGET_PRODUCT)
ifeq ($(TARGET_BUILD_TYPE),debug)
name :=$(name)_debug
endif
name := $(name)-ota-$(FILE_NAME_TAG)
INTERNAL_OTA_PACKAGE_TARGET:= $(PRODUCT_OUT)/$(name).zip
$(INTERNAL_OTA_PACKAGE_TARGET): KEY_CERT_PAIR :=$(DEFAULT_KEY_CERT_PAIR)
$(INTERNAL_OTA_PACKAGE_TARGET):$(BUILT_TARGET_FILES_PACKAGE) $(OTATOOLS)
@echo"Package OTA: $@"
$(hide) ./build/tools/releasetools/ota_from_target_files-v \
-n \
-p$(HOST_OUT) \
-k$(KEY_CERT_PAIR) \
$(ota_extra_flag) \
$(BUILT_TARGET_FILES_PACKAGE) $@
.PHONY: otapackage
otapackage: $(INTERNAL_OTA_PACKAGE_TARGET)
#-----------------------------------------------------------------
代码段1 make otapackage目标代码
如上代码是Makefile文件中目标otapackage的执行代码 。首先,make otapackage命令会执行Makefile(./build/core/Makefile)中otapackage的目标代码(如代码1所示)。由代码可知,otapackage目标的执行只依赖于$(INTERNAL_OTA_PACKAGE_TARGET),而不存在任何规则(根据Makefile语法,规则必须以TAB键开始,而目标otapackage的定义之后却是变量name的声明,因此不存在规则),因此只需要关注目标$(INTERNAL_OTA_PACKAGE_TARGET)的生成。显然,此目标的生成依赖于目标文件:$(BUILT_TARGET_FILES_PACKAGE)和$(OTATOOLS),且其执行的命令为./build/tools/releasetools/ota_from_target_files。也就是说,make otapackage所完成的功能全是通过这两个目标文件和执行的命令完成的,我们将分别对这三个关键点进行分析。
a) $(OTATOOLS)
目标文件OTATOOLS的编译规则如下所示
1. OTATOOLS := $(HOST_OUT_EXECUTABLES)/minigzip \
2. $(HOST_OUT_EXECUTABLES)/mkbootfs \
3. $(HOST_OUT_EXECUTABLES)/mkbootimg \
4. $(HOST_OUT_EXECUTABLES)/fs_config \
5. $(HOST_OUT_EXECUTABLES)/mkyaffs2image \
6. $(HOST_OUT_EXECUTABLES)/zipalign \
7. $(HOST_OUT_EXECUTABLES)/aapt \
8. $(HOST_OUT_EXECUTABLES)/bsdiff \
9. $(HOST_OUT_EXECUTABLES)/imgdiff \
10. $(HOST_OUT_JAVA_LIBRARIES)/dumpkey.jar \
11. $(HOST_OUT_JAVA_LIBRARIES)/signapk.jar \
12. $(HOST_OUT_EXECUTABLES)/mkuserimg.sh \
13. $(HOST_OUT_EXECUTABLES)/genext2fs \
14. $(HOST_OUT_EXECUTABLES)/tune2fs \
15. $(HOST_OUT_EXECUTABLES)/e2fsck \
16. $(HOST_OUT_EXECUTABLES)/make_ext4fs
17.
18. .PHONY: otatools
19. otatools: $(OTATOOLS)
可以看出变量OTATOOLS为系统中一系列文件的集合。那么这些文件又有什么用处呢? 事实上,这些文件用于压缩(minigzip:用于gzip文件;make_ext4fs:将文件转换为ext4类型;mkyaffs2image:用于yaffs文件系统;......)、解压缩、差分(bsdiff,imgdiff)、签名(singapk.jar)等功能,结合代码段1可得到如下结论:目标$(INTERNAL_OTA_PACKAGE_TARGET)的执行依赖于这一系列系统工具--仅此而已。也就是说,目标文件$(OTATOOLS)仅仅指定了命令执行所需要的工具,并未执行任何操作。
注:变量$(HOST_OUT_EXECUTABLES)指代的是out/host/linux-x86/bin目录,而变量$(HOST_OUT_JAVA_LIBRARIES)/表示的是out/host/linux-x86/framework目录,这意味着我们可以从此目录下找到上述工具,并为我们所用。
b) $(BUILT_TARGET_FILES_PACKAGE)
目标OTATOOLS指明了执行makeotapackage命令所需要的系统工具,而目标$(BUILT_TARGE_FILES_PACKAGE)的生成则完成了两件事:重新打包system.img文件和生成差分资源包。$(BUILT_TARGE_FILES_PACKAGE)的编译规则如下所示:
1. # -----------------------------------------------------------------
2. # A zip of the directories that map to the target filesystem.
3. # This zip can be used to create an OTA package or filesystem image
4. # as a post-build step.
5. #
6. name := $(TARGET_PRODUCT)
7. ifeq ($(TARGET_BUILD_TYPE),debug)
8. name := $(name)_debug
9. endif
10.name := $(name)-target_files-$(FILE_NAME_TAG)
11.
12.intermediates := $(call intermediates-dir-for,PACKAGING,target_files)
13.BUILT_TARGET_FILES_PACKAGE := $(intermediates)/$(name).zip
14.$(BUILT_TARGET_FILES_PACKAGE): intermediates := $(intermediates)
15.$(BUILT_TARGET_FILES_PACKAGE): \
16. zip_root := $(intermediates)/$(name)
17.
18.# $(1): Directory to copy
19.# $(2): Location to copy it to
20.# The "ls -A" is to prevent "acp s/* d" from failing if s is empty.
21.define package_files-copy-root
22. if [ -d "$(strip $(1))" -a "$$(ls -A $(1))" ]; then \
23. mkdir -p $(2) && \
24. $(ACP) -rd $(strip $(1))/* $(2); \
25. fi
26.endef
27.
28.built_ota_tools := \
29. $(call intermediates-dir-for,EXECUTABLES,applypatch)/applypatch \
30. $(call intermediates-dir-for,EXECUTABLES,applypatch_static)/applypatch_static \
31. $(call intermediates-dir-for,EXECUTABLES,check_prereq)/check_prereq \
32. $(call intermediates-dir-for,EXECUTABLES,sqlite3)/sqlite3 \
33. $(call intermediates-dir-for,EXECUTABLES,updater)/updater
34.$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_OTA_TOOLS := $(built_ota_tools)
35.
36.$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_RECOVERY_API_VERSION := $(RECOVERY_API_VERSION)
37.
38.ifeq ($(TARGET_RELEASETOOLS_EXTENSIONS),)
39.# default to common dir for device vendor
40.$(BUILT_TARGET_FILES_PACKAGE): tool_extensions := $(TARGET_DEVICE_DIR)/../common
41.else
42.$(BUILT_TARGET_FILES_PACKAGE): tool_extensions := $(TARGET_RELEASETOOLS_EXTENSIONS)
43.endif
44.
45.# Depending on the various images guarantees that the underlying
46.# directories are up-to-date.
47.
48.ifeq ($(TARGET_USERIMAGES_USE_EXT4),true)
49.$(BUILT_TARGET_FILES_PACKAGE): $(INSTALLED_CACHEIMAGE_TARGET)
50.endif
51.
52.$(BUILT_TARGET_FILES_PACKAGE): \
53. $(INSTALLED_BOOTIMAGE_TARGET) \
54. $(INSTALLED_RADIOIMAGE_TARGET) \
55. $(INSTALLED_RECOVERYIMAGE_TARGET) \
56. $(INSTALLED_FACTORYIMAGE_TARGET) \
57. $(INSTALLED_SYSTEMIMAGE) \
58. $(INSTALLED_CACHEIMAGE_TARGET) \
59. $(INSTALLED_USERDATAIMAGE_TARGET) \
60. $(INSTALLED_SECROIMAGE_TARGET) \
61. $(INSTALLED_ANDROID_INFO_TXT_TARGET) \
62. $(built_ota_tools) \
63. $(APKCERTS_FILE) \
64. $(HOST_OUT_EXECUTABLES)/fs_config \
65. | $(ACP)
66. @echo "Package target files: $@"
67. $(hide) rm -rf $@ $(zip_root)
68. $(hide) mkdir -p $(dir $@) $(zip_root)
69. @# Components of the recovery image
70. $(hide) mkdir -p $(zip_root)/RECOVERY
71. $(hide) $(call package_files-copy-root, \
72. $(TARGET_RECOVERY_ROOT_OUT),$(zip_root)/RECOVERY/RAMDISK)
73.ifdef INSTALLED_KERNEL_TARGET
74. $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/RECOVERY/kernel
75. $(hide) $(ACP) $(recovery_ramdisk) $(zip_root)/RECOVERY/ramdisk
76.endif
77.ifdef INSTALLED_2NDBOOTLOADER_TARGET
78. $(hide) $(ACP) \
79. $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/RECOVERY/second
80.endif
81.ifdef BOARD_KERNEL_CMDLINE
82. $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/RECOVERY/cmdline
83.endif
84.ifdef BOARD_KERNEL_BASE
85. $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/RECOVERY/base
86.endif
87. @# Components of the factory image
88. $(hide) mkdir -p $(zip_root)/FACTORY
89. $(hide) $(call package_files-copy-root, \
90. $(TARGET_FACTORY_ROOT_OUT),$(zip_root)/FACTORY/RAMDISK)
91.ifdef INSTALLED_KERNEL_TARGET
92. $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/FACTORY/kernel
93.endif
94.ifdef BOARD_KERNEL_PAGESIZE
95. $(hide) echo "$(BOARD_KERNEL_PAGESIZE)" > $(zip_root)/RECOVERY/pagesize
96.endif
97.ifdef INSTALLED_2NDBOOTLOADER_TARGET
98. $(hide) $(ACP) \
99. $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/FACTORY/second
100.endif
101.ifdef BOARD_KERNEL_CMDLINE
102. $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/FACTORY/cmdline
103.endif
104.ifdef BOARD_KERNEL_BASE
105. $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/FACTORY/base
106.endif
107. @# Components of the boot image
108. $(hide) mkdir -p $(zip_root)/BOOT
109. $(hide) $(call package_files-copy-root, \
110. $(TARGET_ROOT_OUT),$(zip_root)/BOOT/RAMDISK)
111.ifdef INSTALLED_KERNEL_TARGET
112. $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/BOOT/kernel
113. $(hide) $(ACP) $(INSTALLED_RAMDISK_TARGET) $(zip_root)/BOOT/ramdisk
114.endif
115.ifdef INSTALLED_2NDBOOTLOADER_TARGET
116. $(hide) $(ACP) \
117. $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/BOOT/second
118.endif
119.ifdef BOARD_KERNEL_CMDLINE
120. $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/BOOT/cmdline
121.endif
122.ifdef BOARD_KERNEL_BASE
123. $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/BOOT/base
124.endif
125.ifdef BOARD_KERNEL_PAGESIZE
126. $(hide) echo "$(BOARD_KERNEL_PAGESIZE)" > $(zip_root)/BOOT/pagesize
127.endif
128.#wschen
129.ifneq "" "$(CUSTOM_BUILD_VERNO)"
130. $(hide) echo "$(CUSTOM_BUILD_VERNO)" > $(zip_root)/BOOT/board
131.endif
132.
133.#[eton begin]: added by LiuDekuan for u-boot update
134. $(hide) $(ACP) $(PRODUCT_OUT)/uboot_eyang77_ics2.bin $(zip_root)/uboot.bin
135.#[eton end]
136.
137. $(hide) $(foreach t,$(INSTALLED_RADIOIMAGE_TARGET),\
138. mkdir -p $(zip_root)/RADIO; \
139. $(ACP) $(t) $(zip_root)/RADIO/$(notdir $(t));)
140. @# Contents of the system image
141. $(hide) $(call package_files-copy-root, \
142. $(SYSTEMIMAGE_SOURCE_DIR),$(zip_root)/SYSTEM)
143. @# Contents of the data image
144. $(hide) $(call package_files-copy-root, \
145. $(TARGET_OUT_DATA),$(zip_root)/DATA)
146. @# Extra contents of the OTA package
147. $(hide) mkdir -p $(zip_root)/OTA/bin
148. $(hide) $(ACP) $(INSTALLED_ANDROID_INFO_TXT_TARGET) $(zip_root)/OTA/
149. $(hide) $(ACP) $(PRIVATE_OTA_TOOLS) $(zip_root)/OTA/bin/
150. @# Security information of the OTA package
151. @echo "[SEC OTA] Adding Security information to OTA package"
152. @echo "[SEC OTA] path : mediatek/custom/$(MTK_PROJECT)/security/recovery/SEC_VER.txt"
153. $(hide) $(ACP) mediatek/custom/$(MTK_PROJECT)/security/recovery/SEC_VER.txt $(zip_root)/OTA/
154. @# Files that do not end up in any images, but are necessary to
155. @# build them.
156. $(hide) mkdir -p $(zip_root)/META
157. $(hide) $(ACP) $(APKCERTS_FILE) $(zip_root)/META/apkcerts.txt
158. $(hide) echo "$(PRODUCT_OTA_PUBLIC_KEYS)" > $(zip_root)/META/otakeys.txt
159. $(hide) echo "recovery_api_version=$(PRIVATE_RECOVERY_API_VERSION)" > $(zip_root)/META/misc_info.txt
160.ifdef BOARD_FLASH_BLOCK_SIZE
161. $(hide) echo "blocksize=$(BOARD_FLASH_BLOCK_SIZE)" >> $(zip_root)/META/misc_info.txt
162.endif
163.ifdef BOARD_BOOTIMAGE_PARTITION_SIZE
164. $(hide) echo "boot_size=$(BOARD_BOOTIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt
165.endif
166.ifdef BOARD_RECOVERYIMAGE_PARTITION_SIZE
167. $(hide) echo "recovery_size=$(BOARD_RECOVERYIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt
168.endif
169.ifdef BOARD_SYSTEMIMAGE_PARTITION_SIZE
170. $(hide) echo "system_size=$(BOARD_SYSTEMIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt
171.endif
172.ifdef BOARD_SECROIMAGE_PARTITION_SIZE
173. $(hide) echo "secro_size=$(BOARD_SECROIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt
174.endif
175.ifdef BOARD_CACHEIMAGE_PARTITION_SIZE
176. $(hide) echo "cache_size=$(BOARD_CACHEIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt
177.endif
178.ifdef BOARD_USERDATAIMAGE_PARTITION_SIZE
179. $(hide) echo "userdata_size=$(BOARD_USERDATAIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt
180.endif
181. $(hide) echo "tool_extensions=$(tool_extensions)" >> $(zip_root)/META/misc_info.txt
182.ifdef mkyaffs2_extra_flags
183. $(hide) echo "mkyaffs2_extra_flags=$(mkyaffs2_extra_flags)" >> $(zip_root)/META/misc_info.txt
184.endif
185.ifdef INTERNAL_USERIMAGES_SPARSE_EXT_FLAG
186. $(hide) echo "extfs_sparse_flag=$(INTERNAL_USERIMAGES_SPARSE_EXT_FLAG)" >> $(zip_root)/META/misc_info.txt
187.endif
188. $(hide) echo "default_system_dev_certificate=$(DEFAULT_KEY_CERT_PAIR)" >> $(zip_root)/META/misc_info.txt
189.ifdef PRODUCT_EXTRA_RECOVERY_KEYS
190. $(hide) echo "extra_recovery_keys=$(PRODUCT_EXTRA_RECOVERY_KEYS)" >> $(zip_root)/META/misc_info.txt
191.endif
192. @# Zip everything up, preserving symlinks
193. $(hide) (cd $(zip_root) && zip -qry ../$(notdir $@) .)
194. @# Run fs_config on all the system, boot ramdisk, and recovery ramdisk files in the zip, and save the output
195. $(hide) zipinfo -1 $@ | awk ‘BEGIN { FS="SYSTEM/" } /^SYSTEM\// {print "system/" $$2}‘ | $(HOST_OUT_EXECUTABLES)/fs_config > $(zip_root)/META/filesystem_config.txt
196. $(hide) zipinfo -1 $@ | awk ‘BEGIN { FS="BOOT/RAMDISK/" } /^BOOT\/RAMDISK\// {print $$2}‘ | $(HOST_OUT_EXECUTABLES)/fs_config > $(zip_root)/META/boot_filesystem_config.txt
197. $(hide) zipinfo -1 $@ | awk ‘BEGIN { FS="RECOVERY/RAMDISK/" } /^RECOVERY\/RAMDISK\// {print $$2}‘ | $(HOST_OUT_EXECUTABLES)/fs_config > $(zip_root)/META/recovery_filesystem_config.txt
198. $(hide) (cd $(zip_root) && zip -q ../$(notdir $@) META/*filesystem_config.txt)
199.target-files-package: $(BUILT_TARGET_FILES_PACKAGE)
200.ifneq ($(TARGET_PRODUCT),sdk)
201.ifeq ($(filter generic%,$(TARGET_DEVICE)),)
202.ifneq ($(TARGET_NO_KERNEL),true)
203.ifneq ($(recovery_fstab),)
system.img文件的重新打包是通过$(BUILT_TARGE_FILES_PACKAGE)的依赖条件$(INSTALLED_SYSTEMIMAGE)目标文件的编译来完成的,而$(BUILT_TARGE_FILES_PACKAGE)所有的执行命令(代码第66行至最后)都只为完成一件事,生成差分资源包所对应的目录并将其打包为ZIP包。具体的操作包括:
· 创建$(zip_root)目录(代码第66~68行),$(zip_root)即out/target/product/<product-name>/obj/PACKAGING/target_files_from_intermedias/<product-name>-target_files-<version-name>;
· 创建/$(zip_root)/RECOVERY目录并将COPY相关文件(代码69~86);
· 创建/$(zip_root)/FACTORY目录并将COPY相关文件(代码87~106);
· 创建/$(zip_root)/BOOT目录并将COPY相关文件(代码107~131);
· 创建其他目录并COPY文件(代码132~191);
· 将$(zip_root)目录压缩为资源差分包(代码192~198)等。
经过目标文件$(BUILT_TARGE_FILES_PACKAGE)的执行后,system.img已经被重新打包,且差分资源包也已经生成,剩下的工作就是将差分资源包(即target-files zipfile,下文将统一使用“差分资源包”这一概念)传递给ota_ from _target _files代码,由它来生成OTA整包。
总之,前述代码的作用就在于将系统资源(包括system、recovery、boot等目录)重新打包,生成差分资源包。我们可以看下差分资源包中的文件结构,如下:
图2 target-fileszipfile目录结构
其中,OTA目录值得关注,因为在此目录下存在着一个至关重要的文件:OTA/bin/updater(后文会有详述)。生成的差分资源包被传递给./build/tools/releasetools/
ota_from_target_files执行第二阶段的操作:制作升级包。
图3./build/tools/releasetools目录下的文件
图3是./build/tools/releasetools目录下所包含的文件,这组文件是Google提供的用来制作升级包的代码工具,核心文件为:ota_from_target_files和img_from_target_files。其中,前者用来制作recovery模式下的升级包;后者则用来制作fastboot下的升级包(fastboot貌似是一种更底层的刷机操作,不太懂)。其他文件则是为此二者提供服务的,比如,common.py中包含有制作升级包所需操作的代码,各种工具类,参数处理/META文件处理/image生成/signcertification/patch file 操作等等;edify_generator.py则用于生成recovery模式下升级的脚本文件:<升级包>.zip/META-INF/com/google/android/updater-script。
文件ota_from_target_files是本文所关注的重点,其中定义了两个主要的方法:WriteFullOTAPackage和WriteIncrementalOTAPackage。前者用于生成整包,后者用来生成差分包。接着上文,当Makefile调用ota_from_target_files,并将差分资源包传递进来时,会执行WriteFullOTAPackage。此方法完成的工作包括:(1)将system目录,boot.img等文件添加到整包中;(2)生成升级包中的脚本文件:<升级包>.zip/META-INF/com/google/android/updater-script;(3)将上文提到的可执行文件:OTA/bin/updater添加到升级包中:META-INF/com/google/android/updater-binary。摘取部分代码片段如下:
1. script.FormatPartition("/system")
2. script. FormatPartition ("/system")
3. script.UnpackPackageDir("recovery", "/system")
4. script.UnpackPackageDir("system", "/system")
5. (symlinks, retouch_files) = CopySystemFiles(input_zip, output_zip)
6. script.MakeSymlinks(symlinks)
7. if OPTIONS.aslr_mode:
8. script.RetouchBinaries(retouch_files)
9. else:
10. script.UndoRetouchBinaries(retouch_files)
代码2 WriteFullOTAPackage代码片段
其中的script为edify_generator对象,其FormatPartition、UnpackPackageDir等方法分别是向脚本文件update-script中写入格式化分区、解压包等指令
1. def AddToZip(self, input_zip, output_zip, input_path=None):
2. """Write the accumulated script to the output_zip file. input_zip
3. is used as the source for the ‘updater‘ binary needed to run
4. script. If input_path is not None, it will be used as a local
5. path for the binary instead of input_zip."""
6.
7. self.UnmountAll()
8. common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
9. "\n".join(self.script) + "\n")
10. if input_path is None:
11. data = input_zip.read("OTA/bin/updater")
12. else:
13. data = open(os.path.join(input_path, "updater")).read()
14. common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
15. data, perms=0755)
代码段3 edify_generator中的AddToZip方法
WriteFullOTAPackage执行的最后会调用此方法。将资源差分包中OTA/bin/updater文件copy到升级包中META-INF/com/google/android/update-binary。此文件是OTA升级的关键,其将在recovery模式下被执行,用来将代码段2中生成的指令转换为相应的函数去执行,从而完成对系统数据的重写。
生成差分包调用的是文件./build/tools/releasetools/ota_from_target_files中的WriteIncrementalOTA方法,调用时需要将两个版本的差分资源包作为参数传进来,形如:./build/tools/releasetools/ota_from_target_files–n –i ota_v1.zip ota_v2.zip update.zip
其中,参数n表示忽略时间戳;i表示生成增量包(即差分包);ota_v1.zip与ota_v2.zip分别代表前后两个版本的差分资源包;而update.zip则表示最终生成的差分包。WriteIncrementalOTA函数会计算输入的两个差分资源包中版本的差异,并将其写入到差分包中;同时,将updater及生成脚本文件udpate-script添加到升级包中。
制作完升级包后,之后便是将其写入到相应存储区中,这部分工作是在recovery模式下完成的。在这先简单描述一下这个过程。recovery模式下通过创建一个新的进程读取并执行脚本文件META-INF/com/google/android/updater-script。见如下代码:
1. const char** args = (const char**)malloc(sizeof(char*) * 5);
2. args[0] = binary;
3. args[1] = EXPAND(RECOVERY_API_VERSION); // defined in Android.mk
4. char* temp = (char*)malloc(10);
5. sprintf(temp, "%d", pipefd[1]);
6. args[2] = temp;
7. args[3] = (char*)path;
8. args[4] = NULL;
9.
10. pid_t pid = fork();
11. if (pid == 0) {
12. close(pipefd[0]);
13. execv(binary, (char* const*)args);
14. _exit(-1);
15. }
16. close(pipefd[1]);
代码段4创建新进程安装升级包
分析代码之前,首先介绍linux中函数fork与execv的用法。
pid_t fork( void)用于创建新的进程,fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
int execv(const char *progname, char *constargv[])
execv会停止执行当前的进程,并且以progname应用进程替换被停止执行的进程,进程ID没有改变。progname:被执行的应用程序。argv: 传递给应用程序的参数列表, 注意,这个数组的第一个参数应该是应用程序名字本身,并且最后一个参数应该为NULL,不参将多个参数合并为一个参数放入数组。
代码4见于bootable/recovery/install.c的try_update_binary函数中,是OTA升级的核心代码之一。通过对fork及execv函数的介绍可知,代码4创建了一个新的进程并在新进程中运行升级包中的META-INF/com/google/android/updater-binary文件(参数binary已在此前赋值),此文件将按照META-INF/com/google/android/updater-script中的指令将升级包里的数据写入到存储区中。OK,我们来看下META-INF/com/google/android/updater-binary文件的来历。
在源代码的./bootable/recovery/updater目录下,存在着如下几个文件:
通过查看Android.mk代码可知,文件install.c、updater.c将会被编译为可执行文件updater存放到目录out/target/product/<product-name>/obj/EXECUTABLES/updater_intermediates/中;而在生成差分资源包(target-fileszipfile)时,会将此文件添加到压缩包中。
1. built_ota_tools := \
2. $(call intermediates-dir-for,EXECUTABLES,applypatch)/applypatch \
3. $(call intermediates-dir-for,EXECUTABLES,applypatch_static)/applypatch_static \
4. $(call intermediates-dir-for,EXECUTABLES,check_prereq)/check_prereq \
5. $(call intermediates-dir-for,EXECUTABLES,sqlite3)/sqlite3 \
6. $(call intermediates-dir-for,EXECUTABLES,updater)/updater
7. $(hide) mkdir -p $(zip_root)/OTA/bin
8. $(hide) $(ACP) $(INSTALLED_ANDROID_INFO_TXT_TARGET) $(zip_root)/OTA/
9. $(hide) $(ACP) $(PRIVATE_OTA_TOOLS) $(zip_root)/OTA/bin/
代码段5 Makefile中定义的变量built_ota_tools
如代码段5,Makefile中定义了执行OTA所需要的一组工具(built_ota_tools),其中便包括由图4中文件编译而成的文件updater;而在生成差分资源包时,会将这组工具拷贝到差分资源包的OTA/bin目录中(见代码段6);在生成升级包时(无论是执行WriteFullOTAPackage还是WriteIncrementalOTAPackage),最后都会调用edify_generator的AddToZip方法,将updater添加到升级包中(更名为"META-INF/com/google/android/update-binary");最终在recovery模式下被执行,这便是其来龙去脉。而关于updater的执行,也大致的描述下吧。
由前文可知,updater主要由bootable/recovery/updater目录下的install.c和updater.c编译而成,主函数位于updater.c。其中,在install.c中定义了读写系统存储区的操作函数(这才是重写系统数据的真正代码)并将这些函数与updater-script中的指令映射起来。而在updater.c会首先装载install.c定义的函数,之后便解析升级脚本updater-script,执行其对应的操作命令。与此同时,执行updater的进程还会与父进程通信,通知父进程进行UI的相关操作(代码见bootable/recovery/install.c中的try_update_binary函数)。
所谓 Recovery 是智能手机的一种特殊工作模式,有点类似于 Windows 下的 DOS 工具箱,系统进入到这种模式下时,可以通过按键选择相应的操作菜单,从而实现相应的功能,比如 android 系统数据区的快速格式化(即恢复出厂设置);通过 SD 卡进行OTA系统升级及固件(firmware)升级等。我们公司的手机一般进入 recovery 模式的方法是电源键加音量上键。后面会详细分析系统进入Recovery模式的过程以及在Recovery模式下进行OTA升级的过程。
设置模块中进行恢复出厂设置,OTA升级,patch升级及firmware升级等操作,系统一共做了两件事:
1. 往 /cache/recovery/command 文件中写入命令字段;
2. 重启系统
重启系统后必须进入Recovery模式,接下来分析进入Recovery模式的流程。
手机开机后,硬件系统上电,首先是完成一系列的初始化过程,如 CPU、串口、中断、timer、DDR 等硬件设备,然后接着加载 bootloader,为后面内核的加载作好准备。在一些系统启动必要的初始完成之后,系统会通过检测三个条件来判断要进入何种工作模式,流程如图。这一部分代码的源文件在\bootable\bootloader\lk\app\aboot\aboot.c 文件的aboot_init()函数中:
从源代码中可以看出,开机时按住HOME键或音量上键(不同手机对此会有修改),会进入Recovery模式;按着返回键或音量下键会进入fastboot模式。如果没有组合键(代码中成为magic key)按下,则会检测SMEM中reboot_mode变量的值,代码如下:
reboot_mode可取的值为RECOVERY_MODE和FASTBOOT_MODE的宏定义,分别为:
reboot_mode的取值由check_reboot_mode()函数返回,接下来我们看该函数在\bootable\bootloader\lk\target\msm8660_surf\init.c中的定义:
可以看到该函数先读取地址restart_reason_addr处的值,最后返回該值,restart_reason_addr地址定义为0x2A05F65C,从此处读完值后会向改地址写入0x00,即将其内容擦除,这样做是为了防止下次进入时又进入Recovery模式。
如果restart_reason_addr处没有值被读到,则会继续读取MISC分区的BCB段进行判断,调用的函数为recovery_init()。这里解释一下BCB(BootloaderControl Block),BCB 是 bootloader 与 Recovery 的通信接口,也是 Bootloader 与 Main system 之间的通信接口。存储在flash 中的 MISC 分区,占用三个 page,其本身就是一个结构体,具体成员以及各成员含义如下:
command 字段:该字段的值会在 Android 系统需要进入 recovery 模式的时候被 Android 更新。另外在固件更新成功时,也会被更新,以进入 recovery 模式来做一些收尾的清理工作。在更新成功后结束 Recovery时,会清除这个字段的值,防止重启时再次进入 Recovery 模式。
status 字段:在完成"update-radio" 或者 "update-hboot"更新后,bootloader会将执行结果写入到这个字段。
recovery 字段:仅可以被 Main System 写入,用来向 recovery 发送消息。该文件的内容格式为:
recovery\n
<recovery command>\n
<recovery command>
该文件存储的是一个字符串,必须以“recovery\n”开头,否则这个字段的所有内容域会被忽略。“recovery\n”之后的部分,是/cache/recovery/command 文件支持的命令。可以将其理解为 Recovery 操作过程中对命令操作的备份。 Recovery会先读取 BCB,然后读取/cache/recovery/command,然后将二者重新写回 BCB,这样在进入 Mainsystem 之前,确保操作被执行。在操作之后进入 Main system 之前,Recovery 又会清空 BCB 的 command 域和 recovery 域,这样确保重启后不再进入 Recovery 模式。
解释完BCB字段的内容,我们再回过头来,调用recovery_init()的代码如下:
该函数的定义在bootloader\lk\app\aboot\recovery.c中。
recovery_init()函数会先通过get_recovery_message(&msg)函数把BCB段的内容读取到recovery_message结构体中,再读取其command字段,如果字段是boot-recovery,则进入recovery模式;如果是update-radio,则进入固件升级流程。get_recovery_message(&msg)函数的代码如下。
系统判断进入哪种工作模式的流程如下图所示。如果以上条件皆不满足,则进入正常启动序列,系统会加载 boot.img 文件,然后加载 kernel,在内核加载完成之后,会根据内核的传递参数寻找 android 的第一个用户态进程,即 init 进程,该进程根据 init.rc以及 init.$(hardware).rc 脚本文件来启动 android 的必要的服务,直到完成 android 系统的启动。
当进入 recovery 模式时,系统加载的是recovery.img 文件,该文件内容与 boot.img 类似,也包含了标准的内核和根文件系统。但是 recovery.img 为了具有恢复系统的能力,比普通的 boot.img 目录结构中:
1、多了/res/images 目录,在这个目录下的图片是恢复时我们看到的背景画面。
2、多了/sbin/recovery 二进制程序,这个就是恢复用的程序。
3、/sbin/adbd 不一样,recovery 模式下的 adbd 不支持 shell。
4、初始化程序(init)和初始化配置文件(init.rc)都不一样。这就是系统没有进入图形界面而进入了类似文本界面,并可以通过简单的组合键进行恢复的原因。与正常启动系统类似,也是启动内核,然后启动文件系统。在进入文件系统后会执行/init,init 的配置文件就是 /init.rc。这个配置文件位于bootable/recovery/etc/init.rc。查看这个文件我们可以看到它做的事情很简单:
1) 设置环境变量。
2) 建立 etc 连接。
3) 新建目录,备用。
4) 挂载文件系统。
5) 启动 recovery(/sbin/recovery)服务。
6) 启动 adbd 服务(用于调试)。
上文所提到的fastboot 模式,即命令或 SD 卡烧写模式,不加载内核及文件系统,此处可以进行工厂模式的烧写。
综上所述,有三种进入recovery 模式的方法,分别是开机时按组合键,写 SMEM 中的 reboot_mode变量值,以及写位于 MISC 分区的 BCB 中的 command 字段。
Recovery 的工作需要整个软件平台的配合,从通信架构上来看,主要有以下三个部分:
1. Main System:即上面提到的正常启动模式(BCB 中无命令),是用 boot.img 启动的系统, Android的正常工作模式。更新时,在这种模式中我们的上层操作就是使用 OTA 或则从 SD 卡中升级 update.zip升级包。
2. Recovery:系统进入 Recovery 模式后会装载 Recovery 分区,该分区包含 recovery.img (同 boot.img相同,包含了标准的内核和根文件系统)。进入该模式后主要是运行 Recovery 服务(/sbin/recovery)来做相应的操作。
3. Bootloader:除了正常的加载启动系统之外,还会通过读取 MISC 分区(BCB)获得来自 Main System和 Recovery 的消息。
这三个实体之间的通信是必不可少的,他们相互之间有如下两个通信接口:一个是通过 CACHE 分区中的三个文件(command、log、intent);另一个是前面提到的MISC分区的BCB段。
Recovery 的服务内容主要有三类:
①FACTORYRESET,恢复出厂设置。
②OTA INSTALL,即我们的update.zip 包升级。
③ENCRYPTEDFILE SYSTEM ENABLE/DISABLE,使能/关闭加密文件系统。
本文主要关心OTA升级的流程,所以下面的内容主要解释从上层应用点击进行OTA升级到重启进入Recovery模式进行升级包安装的过程。
我们只看从MainSystem如何进入Recovery模式,其他的通信暂不讨论。先从Main System开始看,当我们在Main System使用update.zip包进行升级时,系统会重启并进入Recovery模式。在系统重启之前,我们可以看到,Main System一定会向BCB中的command域写入boot-recovery(粉红色线),用来告知Bootloader重启后进入recovery模式。这一步是必须的。至于Main System是否向recovery域写入值我们在源码中不能肯定这一点。即便如此,重启进入Recovery模式后Bootloader会从/cache/recovery/command中读取值并放入到BCB的recovery域。而MainSystem在重启之前肯定会向/cache/recovery/command中写入Recovery将要进行的操作命令。
? 从SystemUpdate到Reboot
假设我们进入系统更新应用后,已下载完OTA包到SD卡,会弹出一个对话框,提示已有update.zip包是否现在更新,我们从这个地方跟踪。这个对话框的源码是SystemUpdateInstallDialog.java。
① 在mNowButton按钮的监听事件里,会调用mService.rebootAndUpdate(new File(mFile))。这个mService就是SystemUpdateService的实例。这个类所在的源码文件是SystemUpdateService.java。这个函数的参数是一个文件。它肯定就是我们的update.zip包了。我们可以证实一下这个猜想。
②mFile的值:在SystemUpdateInstallDialog.java中的ServiceConnection中我们可以看到这个mFile的值有两个来源。
来源一:
mFile的一个来源是这个是否立即更新提示框接受的上一个Activity以“file”标记传来的值。这个Activity就是SystemUpdate.java。它是一个PreferenceActivity类型的。在其onPreferenceChange函数中定义了向下一个Activity传送的值,这个值是根据我们不同的选择而定的。如果我们在之前选择了从SD卡安装,则这个传下去的“file”值为“/sdcard/update.zip”。如果选择了从NAND安装,则对应的值为“/nand/update.zip”。
来源二:
另个一来源是从mService.getInstallFile()获得。我们进一步跟踪就可发现上面这个函数获得的值就是“/cache”+mUpdateFileURL.getFile();这就是OTA在线下载后对应的文件路径。不论参数mFile的来源如何,我们可以发现在mNowButton按钮的监听事件里是将整个文件,也就是我们的update.zip包作为参数往rebootAndUpdate()中传递的。
③rebootAndUpdate:在这个函数中MainSystem做了重启前的准备。继续跟踪下去会发现,在SystemUpdateService.java中的rebootAndUpdate函数中新建了一个线程,在这个线程中最后调用的就是RecoverySystem.installPackage(mContext,mFile),我们的update.zip包也被传递进来了。
④RecoverySystem类:RecoverySystem类的源码所在文件路径为: *****/frameworks/base/core/java/android/os/RecoverySystem.java。我们关心的是installPackage(Contextcontext,FilepackageFile)函数。这个函数首先根据我们传过来的包文件,获取这个包文件的绝对路径filename。然后将其拼成arg=“--update_package=”+filename。它最终会被写入到BCB中。这个就是重启进入Recovery模式后,Recovery服务要进行的操作。它被传递到函数bootCommand(context,arg)。
⑤bootCommand():在这个函数中才是MainSystem在重启前真正做的准备。主要做了以下事情,首先创建/cache/recovery/目录,删除这个目录下的command和log(可能不存在)文件在sqlite数据库中的备份。然后将上面④步中的arg命令写入到/cache/recovery/command文件中。下一步就是真正重启了。接下来看一下在重启函数reboot中所做的事情。
⑥pm.reboot():重启之前先获得了PowerManager(电源管理)并进一步获得其系统服务。然后调用了pm.reboot(“recovery”)函数。该函数最后找到是E:\MTK6592(Original)\alps\system\core\libcutils\android_reboot.c中的reboot函数。这个函数实际上是一个系统调用,即__reboot(LINUX_REBOOT_MAGIC1,LINUX_REBOOT_MAGIC2,LINUX_REBOOT_CMD_RESTART2, arg);从这个函数我们可以看出前两个参数就代表了我们的组合键,LINUX_REBOOT_CMD_RESTART2就是我们传过来的“recovery”。再进一步跟踪就到了汇编代码了,我们无法直接查看它的具体实现细节。但可以肯定的是这个函数只将“recovery”参数传递过去了,之后将“boot-recovery”写入到了MISC分区的BCB数据块的command域中。这样在重启之后Bootloader才知道要进入Recovery模式。
在这里我们无法肯定MainSystem在重启之前对BCB的recovery域是否进行了操作。其实在重启前是否更新BCB的recovery域是不重要的,因为进入Recovery服务后,Recovery会自动去/cache/recovery/command中读取要进行的操作然后写入到BCB的recovery域中。
至此,MainSystem就开始重启并进入Recovery模式。在这之前Main System做的最实质的就是两件事,一是将“boot-recovery”写入BCB的command域,二是将--update_package=/cache/update.zip”或则“--update_package=/sdcard/update.zip”写入/cache/recovery/command文件中。下面的部分就开始重启并进入Recovery服务了。
? 从reboot到Recovery服务
这个过程我们在上文(对照第一个图)已经讲过了。从Bootloader开始如果没有组合键按下,就从MISC分区读取BCB块的command域(在主系统时已经将“boot-recovery”写入)。然后就以Recovery模式开始启动。与正常启动不同的是Recovery模式下加载的镜像是recovery.img。这个镜像同boot.img类似,也包含了标准的内核和根文件系统。其后就与正常的启动系统类似,也是启动内核,然后启动文件系统。在进入文件系统后会执行/init,init的配置文件就是/init.rc。这个配置文件来自bootable/recovery/etc/init.rc。查看这个文件我们可以看到它做的事情很简单:
①设置环境变量。
②建立etc连接。
③新建目录,备用。
④挂载/tmp为内存文件系统tmpfs
⑤启动recovery(/sbin/recovery)服务。
⑥启动adbd服务(用于调试)。
这里最重要的就是当然就recovery服务了。在Recovery服务中将要完成我们的升级工作。
从/bootable/recovery/recovery.c的代码注释中我们可以看到Recovery的服务内容主要有三类:
① FACTORY RESET,恢复出厂设置。
② OTA INSTALL,即我们的update.zip包升级。
③ ENCRYPTED FILE SYSTEMENABLE/DISABLE,使能/关闭加密文件系统。
具体的每一类服务的大概工作流程,注释中都有,下文中会详细说下OTA INSTALL的工作流程。这三类服务的大概的流程都是通用的,只是不同操作体现与不同的操作细节。下面我们看Recovery服务的通用流程。
本文中会以OTA INSTALL的流程为例具体分析,相关函数的调用过程如下图所示。我们顺着流程图分析,从recovery.c的main函数开始:
1. ui_init():Recovery服务使用了一个基于framebuffer的简单ui(miniui)系统。这个函数对其进行了简单的初始化。在Recovery服务的过程中主要用于显示一个背景图片(正在安装或安装失败)和一个进度条(用于显示进度)。另外还启动了两个线程,一个用于处理进度条的显示(progress_thread),另一个用于响应用户的按键(input_thread)。
2. get_arg():这个函数主要做了上图中get_arg()往右往下直到parsearg/v的工作。我们对照着流程一个一个看。
①get_bootloader_message():主要工作是根据分区的文件格式类型(mtd或emmc)从MISC分区中读取BCB数据块到一个临时的变量中。
②然后开始判断Recovery服务是否有带命令行的参数(/sbin/recovery,根据现有的逻辑是没有的),若没有就从BCB中读取recovery域。如果读取失败则从/cache/recovery/command中读取然后。这样这个BCB的临时变量中的recovery域就被更新了。在将这个BCB的临时变量写回真实的BCB之前,又更新的这个BCB临时变量的command域为“boot-recovery”。这样做的目的是如果在升级失败(比如升级还未结束就断电了)时,系统在重启之后还会进入Recovery模式,直到升级完成。
③在这个BCB临时变量的各个域都更新完成后使用set_bootloader_message()写回到真正的BCB块中。
这个过程可以用一个简单的图来概括,这样更清晰:
3. parserargc/argv:解析我们获得参数。注册所解析的命令(register_update_command),在下面的操作中会根据这一步解析的值进行一步步的判断,然后进行相应的操作。
4. if(update_package):判断update_package是否有值,若有就表示需要升级更新包,此时就会调用install_package()(即图中红色的第二个阶段)。在这一步中将要完成安装实际的升级包。这是最为复杂,也是升级update.zip包最为核心的部分。我们在下一节详细分析这一过程。为从宏观上理解Recovery服务的框架,我们将这一步先略过,假设已经安装完成了。我们接着往下走,看安装完成后Recovery怎样一步步结束服务,并重启到新的主系统的。
5. if(wipe_data/wipe_cache):这一步判断实际是两步,在源码中是先判断是否擦除data分区(用户数据部分)的,然后再判断是否擦除cache分区。值得注意的是在擦除data分区的时候必须连带擦除cache分区。在只擦除cache分区的情形下可以不擦除data分区。
6. maybe_install_firmware_update():如果升级包中包含/radio/hbootfirmware的更新,则会调用这个函数。查看源码发现,在注释中(OTA INSTALL)有这一个流程。但是main函数中并没有显示调用这个函数。目前尚未发现到底是在什么地方处理。但是其流程还是向上面的图示一样。即,① 先向BCB中写入“boot-recovery”和“—wipe_cache”之后将cache分区格式化,然后将firmwareimage 写入原始的cache分区中。②将命令“update-radio/hboot”和“—wipe_cache”写入BCB中,然后开始重新安装firmware并刷新firmware。③之后又会进入图示中的末尾,即finish_recovery()。
7. prompt_and_wait():这个函数是在一个判断中被调用的。其意义是如果安装失败(update.zip包错误或验证签名失败),则等待用户的输入处理(如通过组合键reboot等)。
8. finish_recovery():这是Recovery关闭并进入MainSystem的必经之路。其大体流程如下:
① 将intent(字符串)的内容作为参数传进finish_recovery中。如果有intent需要告知Main System,则将其写入/cache/recovery/intent中。这个intent的作用尚不知有何用。
② 将内存文件系统中的Recovery服务的日志(/tmp/recovery.log)拷贝到cache(/cache/recovery/log)分区中,以便告知重启后的Main System发生过什么。
③ 擦除MISC分区中的BCB数据块的内容,以便系统重启后不在进入Recovery模式而是进入更新后的主系统。
④ 删除/cache/recovery/command文件。这一步也是很重要的,因为重启后Bootloader会自动检索这个文件,如果未删除的话又会进入Recovery模式。原理在上面已经讲的很清楚了。
9. reboot():这是一个系统调用。在这一步Recovery完成其服务重启并进入Main System。这次重启和在主系统中重启进入Recovery模式调用的函数是一样的,但是其方向是不一样的。所以参数也就不一样。查看源码发现,其重启模式是RB_AUTOBOOT。这是一个系统的宏。
至此,我们对Recovery服务的整个流程框架已有了大概的认识。下面就是升级update.zip包时特有的也是Recovery服务中关于安装升级包最核心的第二个阶段。即我们图例中的红色2的那个分支。
安装升级包所调用的函数为install_package(),源码位于/bootable/recovery/install.cpp。该函数调用really_install_package(path,wipe_cache),该函数的流程为:
根据源代码和上面的流程图总结有如下步骤:
①ensure_path_mount():先判断所传的update.zip包路径所在的分区是否已经挂载。如果没有则先挂载。
②load_keys():加载公钥源文件,路径位于/res/keys。这个文件在Recovery镜像的根文件系统中。
③verify_file():对升级包update.zip包进行签名验证。
④mzOpenZipArchive():打开升级包,并将相关的信息拷贝到一个临时的ZipArchinve变量中。这一步并未对我们的update.zip包解压。
⑤try_update_binary():在这个函数中才是对我们的update.zip升级的地方。这个函数一开始先根据我们上一步获得的zip包信息,以及升级包的绝对路径将update_binary文件拷贝到内存文件系统的/tmp/update_binary中。以便后面使用。
⑥pipe():创建管道,用于下面的子进程和父进程之间的通信。
⑦fork():创建子进程。其中的子进程主要负责执行binary(execv(binary,args),即执行我们的安装命令脚本),父进程负责接受子进程发送的命令去更新ui显示(显示当前的进度)。子父进程间通信依靠管道。
⑧其中,在创建子进程后,父进程有两个作用。一是通过管道接受子进程发送的命令来更新UI显示。二是等待子进程退出并返回INSTALLSUCCESS。其中子进程在解析执行安装脚本的同时所发送的命令有以下几种:
progress <frac> <secs>:根据第二个参数secs(秒)来设置进度条。
set_progress <frac>:直接设置进度条,frac取值在0.0到0.1之间。
firmware<”hboot”|”radio”><filename>:升级firmware时使用,在API V3中不再使用。
ui_print <string>:在屏幕上显示字符串,即打印更新过程。
execv(binary,args)的作用就是去执行binary程序,这个程序的实质就是去解析update.zip包中的updater-script脚本中的命令并执行。由此,Recovery服务就进入了实际安装update.zip包的过程。
上述的子进程所执行的程序binary实际上就是update.zip包中的update-binary。实际上Recovery服务在做这一部分工作的时候是先将包中update-binary拷贝到内存文件系统中的/tmp/update_binary,然后再执行的。升级包中update-binary的在升级包制作那一小节中已有说明。
通过install.c源码来分析下update-binary程序的执行过程:
①函数参数以及版本的检查:当前updater binary API所支持的版本号有1,2,3这三个。
②获取管道并打开:在执行此程序的过程中向该管道写入命令,用于通知其父进程根据命令去更新UI显示。
③读取updater-script脚本:从update.zip包中将updater-script脚本读到一块动态内存中,供后面执行。
④Configureedify’s functions:注册脚本中的语句处理函数,即识别脚本中命令的函数。主要有以下几类 RegisterBuiltins():注册程序中控制流程的语句,如ifelse、assert、abort、stdout等。RegisterInstallFunctions():实际安装过程中安装所需的功能函数,比如mount、format、set_progress、set_perm等等。RegisterDeviceExtensions():与设备相关的额外添加項,在源码中并没有任何实现。 FinishRegistration():结束注册。
⑤Parsethescript:调用yy*库函数解析脚本,并将解析后的内容存放到一个Expr类型的python类中。主要函数是yy_scan_string()和yyparse()。
⑥执行脚本:核心函数是Evaluate(),它会调用其他的callback函数,而这些callback函数又会去调用Evaluate去解析不同的脚本片段,从而实现一个简单的脚本解释器。
⑦错误信息提示:最后就是根据Evaluate()执行后的返回值,给出一些打印信息。这一执行过程非常简单,最主要的函数就是Evaluate。它负责最终执行解析的脚本命令。而安装过程中的命令就是updater-script。
常用修改权限的命令:
Set_perm 0 0 0600 ×××(只有所有者有读和写的权限)
Set_perm 0 0 0644 ×××(所有者有读和写的权限,组用户只有读的权限)
Set_perm 0 0 0700 ×××(只有所有者有读和写以及执行的权限)
Set_perm 0 0 0666 ×××(每个人都有读和写的权限)
Set_perm 0 0 0777 ×××(每个人都有读和写以及执行的权限)
1.copy_dir
语法:copy_dir <src-dir><dst-dir> [<times**p>]
<src-dir>表示原文件夹,<dst-dir>表示目的文件夹,[<times**p>]表示时间戳
作 用:将<src-dir>文件夹中的内容复制到<dst-dir>文件夹中。<dst-dir>文件夹中的原始内容 将会保存不变,除非<src-dir>文件夹中有相同的内容,这样<dst-dir>中的内容将被覆盖
举例:copy_dir PACKAGE:system SYSTEM:(将升级包中的system文件夹复制到手机中)
2.format
语法:format <root>
<root>表示要格式化的分区
作用:格式化一个分区
举例:format SYSTEM:(将手机/system分区完全格式化)
注意:格式化之后的数据是不可以恢复的
3.delete
语法:delete <file1> [... <fileN>]
<file1> [... <fileN>]表示要格式化的文件,可以是多个文件用空格隔开
作用:删除文件1,2到n
举例:delete SYSTEM:app/Calculator.apk(删除手机systen文件夹中app中的Calculator.apk文件)
4.delete_recursive
语法:delete_recursive <file-or-dir1> [... <file-or-dirN>]
<file-or-dir1> [... <file-or-dirN>]表示要删除的文件或文件夹,可以使多个,中间用空格隔开
作用:删除文件或者目录,删除目录时会将目录中的所有内容全部删除
举例:delete_recursive DATA:dalvik-cache(删除/data/dalvik-cache文件夹下的所有内容)
5.run_program
语法:run_program <program-file> [<args> ...]
<program-file>表示要运行的程序,[<args> ...]表示运行程序所加的参数
作用:运行终端程序
举例:run_program PACKAGE:install_busybox.sh(执行升级包中的install_busybox.sh脚本)
6.set_perm
语法:set_perm <uid> <gid> <mode> <path> [...<pathN>]
<uid>表示用户名称,<gid>表示用户组名称,<mode>,表示权限模式,<path>[... <pathN>]表示文件路径,可以使多个,用空格隔开
作用:设置单个文件或目录的所有者和权限,像linux中的chmod、chown或chgrp命令一样,只是集中在了一个命令当中
举 例:set_perm 0 2000 0550 SYSTEM:etc/init.goldfish.sh(设置手机system中的etc/init.goldfish.sh的用户为root,用户组为shell,所有者以及所属用户组成员可以进行读取和执行操作,其他用户无操作权限)
7.set_perm_recursive
语法:set_perm_recursive <uid> <gid> <dir-mode><file-mode> <path> [... <pathN>]
<uid> 表示用户,<gid>表示用户组,<dir-mode>表示文件夹的权限,<file-mode>表示文件的权限,<path> [... <pathN>]表示文件夹的路径,可以多个,用空格分开
作用:设置文件夹及文件夹中的文件的所有者和用户组
举 例:set_perm_recursive 0 0 0755 0644 SYSTEM:app(设置手机system/app文件夹及其中文件的用户为root,用户组为root,app文件夹权限为所有者可以进行读、写、执行操作,其他用户可以进行读取和执行操作,其中的文件的权限为所有者可以进行读写操作,其他用户可以进行读取操作)
8.show_progress
语法:show_progress <fraction> <duration>
<表示一个小部分> <表示一个小部分的持续时间>
作用:为下面进行的程序操作显示进度条,进度条会根据<duration>进行前进,当操作时间是确定的时候会更快
举例:show_progress 0.1 0(显示进度条当操作完成后前进10%)
9.symlink
语法:symlink <link-target> <link-path>
<link-target>表示链接到的目标,<link-path>表示快捷方式的路径
作 用:相当于linux中的ln命令,将<link-target>在<link-path>处创建一个软链 接,<link-target>的格式应为绝对路径(或许相对路径也可以),<link-path>为“根目录:路径”的形式
举例:symlink /system/bin/su SYSTEM:xbin/su(在手机中system中的xbin中建立一个/system/bin/su的快捷方式)
10.assert
语法:assert <boolexpr>
作用:此命令用来判断表达式boolexpr的正确与否,当表达式错误时程序终止执行※此作用有待验证
11.package_extract_file/dir语法:package_extract_file(file/dir,file/dir)
作用:提取包中文件/路径
举例:package_extract_dir("system", "/system");
package_extract_file("system/bin/modelid_cfg.sh","/tmp/modelid_cfg.sh");
12.write_radio_image
语法:write_radio_image<src-image>
作用:将基带部分的镜像写入手机,<src-image>表示镜像文件
举例:write_radio_imagePACKAGE:radio.img
13.write_hboot_image
语法:write_hboot_image<src-image>
作用:将系统bootloader镜像写入手机,<src-image>表示镜像位置,此命令在直到在所有的程序安装结束之后才会起作用
举例:write_hboot_imagePACKAGE:hboot.img
14.write_raw_image语法:write_raw_image<src-image> <dest-root>
作用:将boot.img写入手机,里面包含了内核和ram盘
举例:write_raw_image PACKAGE:boot.img BOOT:
15.函数名称: apply_patch
函数语法: apply_patch(srcfile, tgtfile, tgtsha1,tgtsize, sha1_1, patch_1, ..., sha1_x, patch1_x)
参数详解:srcfile-------------------字符串,要打补丁的源文件(要读入的文件)
Tgtfile-------------------字符串,补丁文件要写入的目标文件
tgtsha1-----------------字符串,写入补丁文件的目标文件的sha1哈希值
sha1_x------------------字符串,要写入目标文件的补丁数据的sha1哈希值patch1_x----------------字符串,实际上应用到目标文件的补丁
作用解释: 这个函数是用来打补丁到文件。
16.函数名称: apply_patch_check
函数语法: apply_patch_check(file, sha1_1, ..., sha1_x)
参数详解:file----------------------字符串,要检查的文件
sha1_x------------------要检查的哈希值
作用解释: 检查文件是否已经被打补丁,或者能不能被打补丁。需要检查“applypatch_check ”函数调用的源代码。
17.函数名称: apply_patch_space
函数语法: apply_patch_space(bytes)
参数详解:bytes-------------------检查的字节的数字
作用解释: 检查缓存来确定是否有足够的空间来写入补丁文件并返回一些数据。
18.函数名称: read_file
函数语法: read_file(filename)
参数详解: filename----------------字符串,要读取内容的文件名
作用解释: 这个函数返回文件的内容
19.函数名称: sha1_check
函数语法: sha1_check(data) 或 sha1_check(data, sha1_hex, ..., sha1_hexN)
参数详解:data------要计算sha1哈希值的文件的内容-必须是只读文件格式;
sha1_hexN------文件数据要匹配的特定的十六进制sha1_hex哈希值字符串
作用解释: 如果只指定data参数,这个函数返回data参数的十六进制sha1_hex哈希值字符串。其他参数用来确认你检查的文件是不是列表中的哈希值的一个,它返回匹配的哈希值,或者在没有匹配任何哈希值时返回空。
OTA升级包中有两个非常重要的脚本,分别是:
META-INF/com/google/android/updater-script
recovery/etc/install-recovery.sh
升级来源文件有如下三个:
boot.img
/system
recovery/recovery-from-boot.p
另一个很重要的文件是/etc/recovery.fstab,内容由EMMC分区方案确定。
-------- /etc/recovery.fstab -----------
/boot emmc /dev/block/mmcblk0p1
/sdcard vfat /dev/block/mmcblk0p4
/recovery emmc /dev/block/mmcblk0p2
/system ext4 /dev/block/mmcblk0p5
/cache ext4 /dev/block/mmcblk0p6
/data ext4 /dev/block/mmcblk0p7
/misc emmc /dev/block/mmcblk0p9
--------------------------------------------
otgpackage编译脚本会根据这个文件填充updater-script,后面可以看到。这个文件存在于recovery分区中,进入recovery模式后,可以访问到它。进入recovery模式的方式多种多样,但每种方式都需要bootloader的配合。进入recovery模式后会对升级包进行验证,过程不表,失败退出。进入recovery流程后,主要关心updater-script的工作。
首先是updater-script,代码中可以很容易分析出他的工作流程,如下:
---------updater-script ----------------
.... //省略若干
format("ext4","EMMC", "/dev/block/mmcblk0p5", "0");
mount("ext4","EMMC", "/dev/block/mmcblk0p5", "/system"); //挂载system分区。这里有"/dev/block/mmcblk0p5"和"/system"的对应关系,来源于前文提到的recovery.fstab。
package_extract_dir("recovery","/system");//将zip包中的recovery目录解压到系统/system目录,将来升级recovery分区时使用(install-recovery.sh,recovery-from-boot.p)
package_extract_dir("system","/system"); //将zip包中的system目录解压到系统/system目录,完成system分区的升级
...... //省略若干
symlink("mksh","/system/bin/sh");
symlink("toolbox","/system/bin/cat", ....); //创建软链接,省略若干
retouch_binaries("/system/lib/libbluedroid.so",.....); //各种动态库,省略若干
set_perm_recursive(0,0, 0755, 0644, "/system");
...... //修改权限,省略若干
show_progress(0.200000,0); //显示升级进度
...... //修改权限,省略若干
package_extract_file("boot.img","/dev/block/mmcblk0p1"); //将boot.img解压到相应block设备,完成boot分区的升级。boot分区包含了kernel + ramdisk
show_progress(0.100000,0);
unmount("/system"); //卸载system分区
---------------------------------------------
system分区和boot升级完成,接下来重启,进入正常系统。正常启动的系统init.rc中定义了一个用于烧写recovery分区的服务,也就是执行install-recovery.sh,每次启动都要执行一次。
----- /init.rc------
...
service flash_recovery /system/etc/install-recovery.sh
class main
oneshot
...
--------------------
install-recovery.sh是recovery模式中updater-script解压出来的,内容如下:
-------/system/etc/install-recovery.sh ----
#!/system/bin/sh
log -t recovery "Before sha1....Simba...."
if ! applypatch-c EMMC:/dev/block/mmcblk0p2:4642816:c125924fef5a1351c9041ac9e1d6fd1f9738ff77;then
log -t recovery "Installing new recoveryimage__From Simba..."
applypatchEMMC:/dev/block/mmcblk0p1:3870720:aee24fadd281e9e2bd4883ee9962a86fc345dcabEMMC:/dev/block/mmcblk0p2 c125924fef5a1351c9041ac9e1d6fd1f9738ff77 4642816aee24fadd281e9e2bd4883ee9962a86fc345dcab:/system/recovery-from-boot.p
else
log -t recovery "Recovery image alreadyinstalled__From Simba..."
fi
-------------------------------------------
执行 make otapackage命令时,编译脚本比较boot.img和recovery.img得出patch文件recovery-from-boot.p。recovery-from-boot.p也是在recovery模式中updater-script解压到system目录的。install-recovery.sh脚本就是使用这个patch加上boot分区,更新recovery分区。应用patch前,install-recovery.sh会计算当前recovery分区的sha1。若计算结果与脚本中记录的相同(c125924fef5a1351c9041ac9e1d6fd1f9738ff77),说明已经更新过了,不再操作。这样就完成了/system目录,boot分区(kernel + ramdisk),recovery分区(kernel +ramdisk-recovery)的升级。
以上是标准的Android升级流程,我们自己添加的分区可以参考以上几种方式实现。自定义的分区采用何种升级方式需要细细考量,关系到升级包的内容结构和签名过程。
本文档参考了CSDN上和参考文献中关于Recovery模式及OTA升级的博客和文档,按照自己的理解思路重新梳理,可能会有很多理解的偏差,欢迎大家批评指正。基本的思路就是从OTA包的制作到下载后点击升级如何进入Recovery模式以及在Recovery模式下是怎样实现OTA包的安装升级的。
问题现象:在进行 OTA 升级测试时,下载成功了升级包,在点击立即更新后,手机一直处于提示“正在更新中”,没能重启进行升级。
问题分析:经过分析发现,因为OTA 应用不具备系统权限。导致其无法在目录/cache/recovery 中创建command 文件并在该文件中写入命令,从而导致 OTA 应用无法通过这种预定的方式重启机器并进入recovery 模式,无法实现正常 OTA 升级。
解决方案:通过在 init.rc 文件中增加 mkdircache/recovery 命令,使该目录默认具备写权限,确保 OTA应用可以正常进行系统升级。
问题现象: 下载完升级包后,进入 recovery 模式进行升级时, 系统提示升级失败,手机无法成功升级。
问题分析:通过分析日志,升级失败系在对系统文件进行校验时无法通过校验。跟踪编译流程,发现生成的版本文件和用于生成 OTA 升级包的目录文件不一致。根本原因是在生成版本文件后的编译目标文件的过程中,许多模块重新进行了编译。从而导致版本文件和目标文件中存在有着差异的文件。从而导致升级因校验失败而无法正常升级。
解决方案:针对这种情况,在编译完目标文件后重新打包生成版本文件,就可以解决两者不一致的问题。
问题现象:差分包签名校验失败,报错提示:signature verification failed,Installation aborted。
解决方案:(有三种情况):
1. 差分包签名和版本中签名不一致。开发流版本使用 google 原生签名,故差分包也必须使用
google 原生签名。集成流和发布流版本使用公司签名,故差分包也必须使用公司签名。
2. 差分包导入到 sd 卡时,有时会出现导入失败,原因是从命令提示符中看到已经导入成功,实际上差分包的部分数据还在缓存中,没有完全导入 SD 卡,所以会出现 SD 卡的数据不完整而校验失败,解决方法:将升级包(update.zip 包)导入 SD 卡后,需要执行 adb shell sync。
3. 在制作差分包过程中,差分包的压缩文件损坏,CRC 校验失败。验证方法:将差分包解压,此时会提示解压失败,正常的差分包应该是能正常解压的。
问题现象:升级过程中失败,报错提示:assert failed: getprop("ro.product.device")
问题分析:由于升级过程中需要校验ro.product.device,若新版本中修改了该属性值,则使用前向版本升级时,由于 ro.product.device 不一致,则将会导致升级认为机器手机类型不同而升级失败。
解决方案:将assert(getprop("ro.product.device")的脚本语句屏蔽。
问题现象:版本号不对应,报错提示:assertfailed: file_getprop("/system/build.prop", "ro.build.fingerprint")
问题分析:由于差分包是基于前后两个版本进行差分后升级,若使用的源版本不对应,便会导致差分包不匹配而升级失败。
解决方案: 进入系统设置,查看手机版本是否与差分包的ro.build.fingerprint 对应,重新使用正确的版本进行升级。
问题现象:版本的文件被手动修改,报错提示:script aborted: assert failed: apply_patch_check
问题分析: 可能开发人员或中试人员对源版本获取了root 权限,对手机中的文件进行了修改,而升级中刚好会升级这些文件,便会出现升级被改动文件失败的情况。
解决方案: 获取手机版本中 system 目录所有文件和用于制作差分包的源版本包中的文件进行比对,找出该文件为何被修改的原因。如果是版本集成问题,需要重新编译版本。
问题现象:cache 分区空间不足,报错提示:scriptaborted: assert failed: apply_patch_space
问题分析:由于差分包升级过程中是需要将需差分包的文件放置在 cache 分区下,若需差分的最大文件容量大于 cache 分区的最大容量,则会导致无法放置而升级失败。
解决方案:查看差分包中updater-script 脚本中的以下语句:assert(apply_patch_space(number)),通过计算 cache 分区容量<number>,则是原因版本中某个被修改的文件很大,该大文件一般是版本中的 iso影像,因此在项目中若产品量产后,是不允许修改 iso 影像的。
问题现象:内核升级失败,报错提示:scriptaborted: assert failed: apply_patch("EMMC:…
问题分析:多种情况下都可能导致内核升级失败:
1. 由于版本中若修改了内核的起始地址,将会导致制作出来的差分包在校验内核时 sha 校验失败。
2. 在制作差分包时,若需要升级modem 文件,其正确顺序为先做 AP 侧的差分包和整包,然后把要升级的 MP 侧文件放进去,再签名。若顺序反了:如先放置 MP 侧文件,再制作 AP 侧的差分包和整包,这种也会导致升级内核失败。
解决方案:对于第一种情况,则对内核不能使用差分的形式,而要使用整体的形式进行升级,即将对内核的 apply_patch 语句去除,而使用以下方法。emmc 文件系统:package_extract_file("boot.img","/dev/block/mmcblk0p8")或 MTD 文件系统:assert(package_extract_file("boot.img","/tmp/boot.img"),write_raw_image("/tmp/boot.img","boot"),delete("/tmp/boot.img"));
问题现象:升级 boot.img 时,拔电池重启后,会一直进入 recovery 模式,并且不能正常升级。
问题分析:由于差分包升级过程中是需要校验的,恢复到一半的时候断电,会导致差分包与源文件对不上号而导致升级失败。
解决方案:升级中提示用户不能拔电池,或者使用整包升级而不是差分升级包。
标签:
原文地址:http://blog.csdn.net/teliduxing1029/article/details/51536560