Android8.0.0-r4的OTA升级–差分包升级

  • Post author:
  • Post category:其他





一、

系统更新update.zip包的两种方式


1.


制作一个update.zip

升级包用于升级系统。Android在升级系统时获得update.zip包的方式有两种。一种是离线升级,即手动拷贝升级包到SD卡(或NAND)中,通过settings–>Aboutphone–>SystemUpdate–>选择从SD卡升级。另一种是在线升级,即OTA Install(over the  air)。用户通过在线下载升级包到本地,然后更新。这种方式下的update.zip包一般被下载到系统的/CACHE分区下。


2.


无论将升级包放在什么位置,在使用update.zip

更新时都会重启并进入Recovery模式,然后启动recovery服务(/sbin/recovery)来安装update.zip包。



二、Android系统中三种启动模式


Android系统启动后可能会进入的几种工作模式:


由上图可知Android系统启动后可能进入的模式有以下几种:



(一)


MAGIC KEY(组合键):



即用户在启动后通过按下组合键,进入不同的工作模式,具体有两种:

1、camera + power:若用户在启动刚开始按了camera+power组合键则会进入bootloader模式,并可进一步进入fastboot(快速刷机模式)。

2、home +power :若用户在启动刚开始按了home+power组合键,系统会直接进入Recovery模式。以这种方式进入Recovery模式时系统会进入一个简单的UI(使用了minui)界面,用来提示用户进一步操作。在开发板中提供了一下几种选项操作:

“reboot system now”
“apply update from sdcard”
“wipe data/factory reset”
“wipe cache partition”



(二) 正常启动:



若启动过程中用户没有按下任何组合键,bootloader会读取位于MISC分区的启动控制信息块BCB(BootloaderControl Block)。它是一个结构体,存放着启动命令command。根据不同的命令,系统又可以进入三种不同的启动模式。我们先看一下这个结构体的定义。

/bootable/recovery/bootloader_message/include/bootloader_message/bootloader_message.h

struct bootloader_message {
char command[32];//存放不同的启动命令
char status[32];//update-radio或update-hboot完成存放执行结果
char recovery[768]; //存放/cache/recovery/command中的命令
char stage[32];
char reserved[1184];

};


“恢复”字段以前是1024字节。它只用于存储恢复命令行,因此768字节应该足够。我们保留256个字节来存储阶段字符串(用于多级包)和将来可能的扩展。整个bootloader_message结构等于2048字节


command可能的值有两种,与值为空(即没有命令)一起区分三种启动模式



1. command==”boot-recovery”时,系统会进入Recovery模式。Recovery服务会具体根据/cache/recovery/command中的命令执行相应的操作(例如,升级update.zip或擦除cache,data等)。

2. command==”update-radia”或”update-hboot”时,系统会进入更新firmware(更新bootloader),具体由bootloader完成。

3. command为空时,即没有任何命令,系统会进入正常的启动,最后进入主系统(mainsystem)。这种是最通常的启动流程。

Android系统不同的启动模式的进入是在不同的情形下触发的,从SD卡中升级我们的update.zip时会进入Recovery模式是其中一种,其他的比如:系统崩溃,或则在命令行输入启动命令式也会进入Recovery或其他的启动模式


三、Recovery模式



3.1 Recovery模式中的三个部分

Recovery的工作需要整个软件平台的配合,从通信架构上来看,主要有三个部分。

①MainSystem:即上面提到的正常启动模式(BCB中无命令),是用boot.img启动的系统,Android的正常工作模式。更新时,在这种模式中我们的上层操作就是使用OTA或则从SD卡中升级update.zip包。在重启进入Recovery模式之前,会向BCB中写入命令,以便在重启后告诉bootloader进入Recovery模式。

②Recovery:系统进入Recovery模式后会装载Recovery分区,该分区包含recovery.img(同boot.img相同,包含了标准的内核和根文件系统)。进入该模式后主要是运行Recovery服务(/sbin/recovery)来做相应的操作(重启、升级update.zip、擦除cache分区等)。

③Bootloader:除了正常的加载启动系统之外,还会通过读取MISC分区(BCB)获得来至Main system和Recovery的消息。



3.2 Recovery模式中的两个通信接口



在Recovery服务中上述的三个实体之间的通信是必不可少的,他们相互之间又有以下两个通信接口。


(一)通过CACHE分区中的三个文件



Recovery通过/cache/recovery/目录下的三个文件与mian system通信。具体如下

①/cache/recovery/command:这个文件保存着Main system传给Recovery的命令行,每一行就是一条命令,支持一下几种的组合。

–send_intent=anystring  //write the text out to recovery/intent     在Recovery结束时在finish_recovery函数中将定义的intent字符串作为参数传进来,并写入到/cache/recovery/intent中

–update_package=root:path  //verify install an OTA package file     Main system将这条命令写入时,代表系统需要升级,在进入Recovery模式后,将该文件中的命令读取并写入BCB中,然后进行相应的更新update.zip包的操作。

–wipe_data    //erase userdata(and cache),then reboot。擦除用户数据。擦除data分区时必须要擦除cache分区。

–wipe_cache   //wipe cache(butnot user data),then reboot。擦除cache分区。

②/cache/recovery/log:Recovery模式在工作中的log打印。在recovery服务运行过程中,stdout以及stderr会重定位到/tmp/recovery.log在recovery退出之前会将其转存到/cache/recovery/log中,供查看。

③/cache/recovery/intent:Recovery传递给Main system的信息。作用不详。


(二)通过BCB(Bootloader Control Block):


BCB是bootloader与Recovery的通信接口,也是Bootloader与Main system之间的通信接口。存储在flash中的MISC分区,占用三个page,其本身就是一个结构体,具体成员以及各成员含义如下:

struct bootloader_message{

char command[32];

char status[32];

char recovery[1024];

};

①command成员:其可能的取值在上文已经分析过了,即当想要在重启进入Recovery模式时,会更新这个成员的值。另外在成功更新后结束Recovery时,会清除这个成员的值,防止重启时再次进入Recovery模式。

②status:在完成相应的更新后,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,这样在进入Main system之前,确保操作被执行。在操作之后进入Main system之前,Recovery又会清空BCB的command域和recovery域,这样确保重启后不再进入Recovery模式。




3.3 如何从Main System重启并进入Recovery模式





以上三个部分是怎样进行通信的:






从Main System

如何进入Recovery模式。先从Main System开始看,当在Main System使用update.zip包进行升级时,系统会重启并进入Recovery模式。在系统重启之前,Main System定会向BCB中的command域写入boot-recovery(粉红色线),用来告知Bootloader重启后进入recovery模式。重启进入Recovery模式后Bootloader会从/cache/recovery/command中读取值并放入到BCB的recovery域。而Main System在重启之前肯定会向/cache/recovery/command中写入Recovery将要进行的操作命令。


update.zip


包来源有两种,一个是OTA

在线下载(一般下载到/CACHE分区),一个是手动拷贝到SD卡中。不论是哪种方式获得update.zip包,在进入Recovery模式前,都未对这个zip包做处理。只是在重启之前将zip包的路径告诉了Recovery服务(通过将–update_package=CACHE:some_filename.zip或–update_package=SDCARD:update.zip命令写入到/cache/recovery/command中)。假设update.zip包已经制作好并拷贝到了SD卡中,并以Settings–>AboutPhone–>SystemUpdate–>Installed From SDCARD方式升级。


四、OTA升级流程


4.1 从System Update到Reboot


当依次选择Settings–>About Phone–>SystemUpdate–>Installed From SDCARD后会弹出一个对话框,提示已有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:在这个函数中Main System做了重启前的准备。继续跟踪下去会发现,在SystemUpdateService.java中的rebootAndUpdate函数中新建了一个线程,在这个线程中最后调用的就是RecoverySystem.installPackage(mContext,mFile),update.zip包也被传递进来了。

④RecoverySystem类:RecoverySystem类的源码所在文件路径为:gingerbread0919/frameworks/base/core/java/android/os/RecoverySystem.java。我们关心的是installPackage(Context context,FilepackageFile)函数。这个函数首先根据我们传过来的包文件,获取这个包文件的绝对路径filename。然后将其拼成arg=“–update_package=”+filename。它最终会被写入到BCB中。这个就是重启进入Recovery模式后,Recovery服务要进行的操作。它被传递到函数bootCommand(context,arg)。

⑤bootCommand():在这个函数中才是Main System在重启前真正做的准备。主要做了以下事情,首先创建/cache/recovery/目录,删除这个目录下的command和log(可能不存在)文件在sqlite数据库中的备份。然后将上面④步中的arg命令写入到/cache/recovery/command文件中。下一步就是真正重启了。接下来看一下在重启函数reboot中所做的事情。

⑥pm.reboot():重启之前先获得了PowerManager(电源管理)并进一步获得其系统服务。然后调用了pm.reboot(“recovery”)函数。他就是/bionic/libc/bionic/reboot.cpp中的reboot函数。这个函数实际上是一个系统调用,即__reboot(LINUX_REBOOT_MAGIC1,LINUX_REBOOT_MAGIC2,mode,NULL);从这个函数我们可以看出前两个参数就代表了我们的组合键,mode就是我们传过来的“recovery”。再进一步跟踪就到了汇编代码了,我们无法直接查看它的具体实现细节。但可以肯定的是这个函数只将“recovery”参数传递过去了,之后将“boot-recovery”写入到了MISC分区的BCB数据块的command域中。这样在重启之后Bootloader才知道要进入Recovery模式。

在这里我们无法肯定Main System在重启之前对BCB的recovery域是否进行了操作。其实在重启前是否更新BCB的recovery域是不重要的,因为进入Recovery服务后,Recovery会自动去/cache/recovery/command中读取要进行的操作然后写入到BCB的recovery域中。

至此,Main System就开始重启并进入Recovery模式。在这之前Main System做的最实质的就是两件事,

一 、是将“boot-recovery”写入BCB的command域,

二、是将–update_package=/cache/update.zip”或则“–update_package=/sdcard/update.zip”写入/cache/recovery/command文件中。


4.2 从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服务中将要完成升级工作。


4.3 Recovery服务流程细节

Recovery服务毫无疑问是Recovery启动模式中最核心的部分。它完成Recovery模式所有的工作。Recovery程序对应的源码文件位于:/bootable/recovery/recovery.cpp。



一、 Recovery

的三类服务:

Recovery的服务内容主要有三类:




FACTORY RESET,恢复出厂设置。




OTA INSTALL,即update.zip包升级。




ENCRYPTED FILE SYSTEM ENABLE/DISABLE,使能/关闭加密文件系统。



二、Recovery

服务的通用流程:




以OTAINSTALL

的流程为例具体分析。并从相关函数的调用过程图:




顺着流程图分析,从recovery.cpp

的main函数开始:




1.


ui_init()


:Recovery

服务使用了一个基于framebuffer的简单ui(miniui)系统。这个函数对其进行了简单的初始化。在Recovery服务的过程中主要用于显示一个背景图片(正在安装或安装失败)和一个进度条(用于显示进度)。另外还启动了两个线程,一个用于处理进度条的显示(progress_thread),另一个用于响应用户的按键(input_thread)。




2.


get_arg()


:这个函数主要做了上图中get_arg()

往右往下直到parse arg/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包最为核心的部分。




5.


if(wipe_data/wipe_cache)


:这一步判断实际是两步,在源码中是先判断是否擦除data

分区(用户数据部分)的,然后再判断是否擦除cache分区。值得注意的是在擦除data分区的时候必须连带擦除cache分区。在只擦除cache分区的情形下可以不擦除data分区。




6.


maybe_install_firmware_update()


:如果升级包中包含/radio/hboot firmware

的更新,则会调用这个函数。

①先向BCB中写入“boot-recovery”和“—wipe_cache”之后将cache分区格式化,然后将firmware image 写入原始的cache分区中。

②将命令“update-radio/hboot”和“—wipe_cache”写入BCB中,然后开始重新安装firmware并刷新firmware。

③之后又会进入图示中的末尾,即finish_recovery()。




7.


prompt_and_wait()


:这个函数是在一个判断中被调用的。其意义是如果安装失败(update.zip

包错误或验证签名失败),则等待用户的输入处理(如通过组合键reboot等)。




8.


finish_recovery()


:这是Recovery

关闭并进入Main System的必经之路。其大体流程如下:











将intent

(字符串)的内容作为参数传进finish_recovery中。如果有intent需要告知Main System,则将其写入/cache/recovery/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服务的核心install_package


(升级update.zip特有)




和Recovery

服务中的wipe_data、wipe_cache不同,install_package()是升级update.zip特有的一部分,也是最核心的部分。






这一部分的源码文件位于:/bootable/recovery/install.cpp




流程:







ensure_path_mount()


:先判断所传的update.zip

包路径所在的分区是否已经挂载。如果没有则先挂载。







load_keys()


:加载公钥源文件,路径位于/res/keys

。这个文件在Recovery镜像的根文件系统中。







verify_file()


:对升级包update.zip

包进行签名验证。







mzOpenZipArchive()


:打开升级包,并将相关的信息拷贝到一个临时的ZipArchinve

变量







try_update_binary()


:在这个函数中才是对update.zip

升级的地方。这个函数一开始先根据上一步获得的zip包信息,以及升级包的绝对路径将update_binary文件拷贝到内存文件系统的/tmp/update_binary中。以便后面使用。







pipe()


:创建管道,用于下面的子进程和父进程之间的通信。







fork()


:创建子进程。其中的子进程主要负责执行binary

(execv(binary,args),即执行安装命令脚本),父进程负责接受子进程发送的命令去更新ui显示(显示当前的进度)。子父进程间通信依靠管道。







其中,在创建子进程后,父进程有两个作用。一是通过管道接受子进程发送的命令来更新UI

显示。二是等待子进程退出并返回INSTALL SUCCESS。其中子进程在解析执行安装脚本的同时所发送的命令有以下几种:

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包的过程。



四、update_binary的执行过程

Recovery服务在做这一部分工作的时候是先将包中update-binary拷贝到内存文件系统中的/tmp/update_binary,然后再执行的。update_binary程序的源码位于/bootable/recovery/updater/updater.cpp:

执行过程:

①函数参数以及版本的检查:当前updater binary API所支持的版本号有1,2,3这三个。

②获取管道并打开:在执行此程序的过程中向该管道写入命令,用于通知其父进程根据命令去更新UI显示。

③读取updater-script脚本:从update.zip包中将updater-script脚本读到一块动态内存中,供后面执行。

④Configure edify’s functions:注册脚本中的语句处理函数,即识别脚本中命令的函数。主要有以下几类

RegisterBuiltins():注册程序中控制流程的语句,如ifelse、assert、abort、stdout等。

RegisterInstallFunctions():实际安装过程中安装所需的功能函数,比如mount、format、set_progress、set_perm等等。

RegisterDeviceExtensions():与设备相关的额外添加項,在源码中并没有任何实现。

FinishRegistration():结束注册

⑤Parsethe script:调用yy*库函数解析脚本,并将解析后的内容存放到一个Expr类型的python类中。主要函数是yy_scan_string()和yyparse()。

⑥执行脚本:核心函数是Evaluate(),它会调用其他的callback函数,而这些callback函数又会去调用Evaluate去解析不同的脚本片段,从而实现一个简单的脚本解释器。

⑦错误信息提示:最后就是根据Evaluate()执行后的返回值,给出一些打印信息。

这一执行过程非常简单,最主要的函数就是Evaluate。它负责最终执行解析的脚本命令。而安装过程中的命令就是updater-script。



五、update-script脚本

一、update-script脚本语法简介:

1.assert(condition):如果condition参数的计算结果为False,则停止脚本执行,否则继续执行脚本。

2.show_progress(frac,sec):frac表示进度完成的数值,sec表示整个过程的总秒数。主要用与显示UI上的进度条。

3.format(fs_type,partition_type,location):fs_type,文件系统类型,取值一般为“yaffs2”或“ext4”。Partition_type,分区类型,一般取值为“MTD”或则“EMMC”。主要用于格式化为指定的文件系统。事例如下:format(”yaffs2”,”MTD”,”system”)。

4.mount(fs_type,partition_type,location,mount_point):前两个参数同上,location要挂载的设备,mount_point挂载点。作用:挂载一个文件系统到指定的挂载点。

5.package_extract_dir(src_path,destination_path):src_path,要提取的目录,destination_path目标目录。作用:从升级包内,提取目录到指定的位置。示例:package_extract_dir(“system”,”/system”)。

6.symlink(target,src1,src2,……,srcN):target,字符串类型,是符号连接的目标。SrcX代表要创建的符号连接的目标点。示例:symlink(“toolbox”,”/system/bin/ps”),建立指向toolbox符号连接/system/bin/ps,值得注意的是,在建立新的符号连接之前,要断开已经存在的符号连接。

7.set_perm(uid,gid,mode,file1,file2,……,fileN):作用是设置单个文件或则一系列文件的权限,最少要指定一个文件。

8.set_perm_recursive(uid,gid,mode,dir1,dir2,……,dirN):作用同上,但是这里同时改变的是一个或多个目录及其文件的权限。

9.package_extract_file(srcfile_path,desfile_paht):srcfile_path,要提取的文件,desfile_path,提取文件的目标位置。示例:package_extract_file(“boot.img”,”/tmp/boot.img”)将升级包中的boot.img文件拷贝到内存文件系统的/tmp下。

10.write_raw_image(src-image,partition):src-image源镜像文件,partition,目标分区。作用:将镜像写入目标分区。示例:write_raw_image(“/tmp/boot.img”,”boot”)将boot.img镜像写入到系统的boot分区。

11.getprop(key):通过指定key的值来获取对应的属性信息。示例:getprop(“ro.product.device”)获取ro.product.device的属性值。

二、updater-script脚本执行流程分析:

make otapackage生成的升级脚本的执行过程:

①比较时间戳:如果升级包较旧则终止脚本的执行。

②匹配设备信息:如果和当前的设备信息不一致,则停止脚本的执行。

③显示进度条:如果以上两步匹配则开始显示升级进度条。

④格式化system分区并挂载。

⑤提取包中的recovery以及system目录下的内容到系统的/system下。

⑥为/system/bin/下的命令文件建立符号连接。

⑦设置/system/下目录以及文件的属性。

⑧将包中的boot.img提取到/tmp/boot.img。

⑨将/tmp/boot.img镜像文件写入到boot分区。

⑩完成后卸载/system。

以上就是updater-script脚本中的语法,及其执行的具体过程。通过分析其执行流程,我们可以发现在执行过程中,并未将升级包另外解压到一个地方,而是需要什么提取什么。值得主要的是在提取recovery和system目录中的内容时,一并放在了/system/下。在操作的过程中,并未删除或改变update.zip包中的任何内容。在实际的更新完成后,我们的update.zip包确实还存在于原来的位置。



版权声明:本文为nwpushuai原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。