您的位置:首页 > 移动开发 > Android开发

android recovery

2016-01-03 18:00 609 查看
Android的recovery是我在公司做的最多的,应该也是我在Android中了解的较为深入的一个部分。recovery这部分其实Android本身都已经提供了很完善的一套机制,但是因为公司是做机顶盒的,所以在因为平台订制的关系,recovery这部分还是做了很多修改的。

首先,修改的比较少的是OTT这种盒子,本次也主要讲这种,其实这种和手机区别不大。而类似将DVB 中的loader和Android的recovery整合到一起这种,确实比较不一样。例如在我们公司,整个升级的签名校验订制以及升级所用到的update.zip包中的烧录进程就都是自己一套的。

但是万变不离其中,其实了解了就发现其实也都是那样。

所以简单讲下recovery的相关知识:

如何进入recovery

标准Android的recovery进入方式一般是这几种

”在设置中点击恢复出厂设置“

”开机按组合件进入recovery“,例如 home + power(这种是手机的)

”系统检测到固件更新,下载后要求你重启,这时重启会先进入recovery“

其实这几种在实现上是差不多:

1、首先,我们要明白,在Android中,其实是由两个系统存在的,recovery,其实就是一个小系统,专门用来刷机的。

2、具体是要进入哪个系统,这个是又fastboot来决定的(因为你基本可以认为他就是开机的第一个程序),他决定进那个就哪个。当然,这个也是由我们来告诉fastboot的。

3、怎么告诉呢?有两种方式。

第一种,就是按组合键,(就相当于,我们告诉fastboot,开机检测到有人按了这个组合件就是进recovery,别进安卓。这个当然就可以定制了,例如在机顶盒,我们通常是改成按遥控器,如”连续按上中下键“)。

第二种,就是Android告诉fastboot下次启动进recovery,然后Android自己再重启。这个就关系到了另一个分区,叫MISC(你基本可以认为他就是存储recovery命令的)。因为fastboot启动会去读这个MISC分区中的内容,来决定自己进哪个系统。系统固件升级和恢复出厂都属于这种,就是recovery命令有点不一样。

recovery流程

首先要知道一点,recovery系统是一个类似Linux的变种,使用了Android init的那一套,但是不会进去虚拟机。所以init.rc和property那些Android的东西对于recovery还是一样的。

流程如下:

1、一般来说,在init.reovery.rc中,就可以看到启动了/sbin/recovery,这就启动了recovery来作为一个service

2、recovery进程简介:



1. load_volume_table();这个函数从”/etc/recovery.fstab”读取分区信息

2. get_arg():主要就是获取recovery的命令、参数等等,这样recovery进程才知道自己要做什么,升级包在哪,这些都以一定的结构体保存在MISC分区中,我们成为bootloader_message,也就是下面会说的BCB。

①get_bootloader_message():主要工作是根据分区的文件格式类型(mtd或emmc)从MISC分区中读取BCB数据块到一个临时的变量中。(get和set bootloader_message:

1、/从”/misc”读取分区设置/

2、/mtd类型只读取或者修改MISC_COMMAND_PAGE一页/

3、/emmc类型直接对一个device进行读写操作/)

②然后开始判断Recovery服务是否有带命令行的参数(/sbin/recovery,根据现有的逻辑是没有的),若没有就从BCB中读取 recovery域。如果读取失败则从/cache/recovery/command中读取然后写入BCB临时变量。这样这个BCB的临时变量中的recovery域就被更新了。在将这个BCB的临时变量写回真实的BCB之前,又更新的这个BCB临时变量的command域为“boot-recovery”。这样做的目的是如果在升级失败(比如升级还未结束就断电了)时,系统在重启之后还会进入Recovery模式,直到升级完成。

③在这个BCB临时变量的各个域都更新完成后使用set_bootloader_message()写回到真正的BCB块中。这个过程可以用一个简单的图来概括,这样更清晰:



(get_arg()这个函数中,主要是获取参数,重写recovery命令到BCB。但是,有时从command_file,有时从BCB读取。

看如何从上层进入recovery,从上层重启进入recovery的话,会将recovery命令写入到BCB,将升级包目录写进command_file。

也就是说,command_file是不会有recovery标识的。)

// --> write the arguments we have back into the bootloader control block
// always boot into recovery after this (until finish_recovery() is called)
strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));


(所以说,从BCB读出,在写回,主要就是修改这两句话。这样子,就能保证进入升级。要注意的是,进入升级模式,是在fastboot的过程选择的,而这里是为了保证升级过程中若中断了,下次还是进recovery。

第二种是如果BCB读取失败 还可以从command file中去读取。)

接下来就是判断从上面流程获取的recovery命令及参数了

3. if(update_package):判断update_package是否有值,若有就表示需要升级更新包,此时就会调用 install_package()。在这一步中将要完成安装实际的升级包。这是最为复杂,也是升级update.zip包最为核心的部分。(这种就是所谓的固件升级)

4. if(wipe_data/wipe_cache):这一步判断实际是两步,在源码中是先判断是否擦除data分区(用户数据部分)的,然后再判断是否擦除cache分区。值得注意的是在擦除data分区的时候必须连带擦除cache分区。在只擦除cache分区的情形下可以不擦除data分区。(这种就所谓的恢复出厂设置)

finish_recovery():这是Recovery关闭并进入Main System的必经之路。其大体流程如下:



① 将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模式。原理在上面已经讲的很清楚了。

install_package()

上面已经说过,这个基本是整个recovery最复杂的也是最核心的部分,就是他完成刷机(固件升级)。详细说下:



①ensure_path_mount():先判断所传的update.zip包路径所在的分区是否已经挂载。如果没有则先挂载。

②load_keys():加载公钥源文件,路径位于/res/keys。(下面讲)

③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显示。

二是等待子进程退出并返回INSTALL SUCCESS。

其中子进程在解析执行安装脚本execv(binary,args)的作用就是去执行binary程序,这个程序的实质就是去解析update.zip包中的 updater-script脚本中的命令并执行。由此,Recovery服务就进入了实际安装update.zip包的过程。

实际上,上面已经说完了主要流程,其实也比较简单,所以接下来做一点细节的补充:

细节补充

Install_package()中load_keys和verify_file

/返回key和key的个数,key的位置在 “/res/keys”/

1.RSAPublicKey* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);

key的结构如下:

*{key->len,key->n0inv,{key->n[i]},{key->rr[i]}}

*或者v2 {key->len,key->n0inv,{key->n[i]},{key->rr[i]}}

*example”{64,0xc926ad21,{1795090719,…,-695002876},{-857949815,…,1175080310}}”

“v2 {64,0xc926ad21,{1795090719,…,-695002876},{-857949815,…,1175080310}}” /

Key的版本不同的话,幂分别是3和65537

/*对zip包数据进行校验。对zip包的签名部分进行摘要计算(sha),再利用key对摘要

2.err = verify_file(path, loadedKeys, numKeys);

Zip包结构

1:主要数据,已经经过签名

2:end-of-central-directory 包括comment_size + EOCD_HEADER_SIZE

其中(eocd[0] = 0x50 eocd[1] = 0x4b eocd[2] = 0x05 eocd[3] = 0x06)(用于指纹校验)

RSA块:经秘钥加密,可用于签名校验。

3:footer:(2-byte signature start) ffff ff (2-byte comment size)

其中comment_size = footer[4] + (footer[5] << 8);

eocd_size = comment_size + EOCD_HEADER_SIZE;

signature_start = footer[0] + (footer[1] << 8);signature_start - FOOTER_SIZE这个大小用来存放RSA block.(这算一步小校验)

signed_len:except for the comment length field (2 bytes) and the comment data.

利用上面几个固定的字节对应的固定的值,可以进行指纹校验,这是第一步和第二步的校验

第三步校验5050 4b 0505 06 若出现在正确的位置后面的话,则”EOCD marker occurs after start of EOCD

第四步校验就是对/zip包的前部分,SHA_update(&ctx, buffer, size);摘要计算/

过程SHA_init(&ctx);

SHA_update(&ctx, buffer, size);//一次会处理4096字节

const uint8_t* sha1 = SHA_final(&ctx);

得到摘要结果

/利用公钥 对摘要进行校验,上一步得到的/

RSA_verify()

Zip包的后面是RSA区和6个字节的脚信息,RSA区是明文用私钥加密后的数据,机顶盒中会有一个公钥。先对前面的升级数据进行SHA1,然后用公钥对RSA区数据进行解密。

解密后的数据的前半部进行 pkcs1.5 padding bytes.校验。

解密后的数据的后半部和SHA1后的数据进行比较,完成校验。(20个字节)

updater-script脚本部分函数说明

升级脚本文件updater-script的内容可根据自己需要进行修改。对脚本中的部分函数进行简要说明:

z ui_print(char *str)

功能:打印信息。

参数:str指针指向要打印的信息地址。

z show_progress (char *sec,char *total)

功能:显示进度条。

参数:

− sec:多少秒更新一次进度条,一般为1。

− total:升级所耗时间(根据升级包大小来确定)。

z format(char fs_type, char *partition_type,char *location,char fs_size, char *mount_point)

功能:格式化分区。

参数:

− fs_type:文件系统类型(“ubifs”or“ext4”,“raw”)。

¾ NAND Flash器件:裸分区:raw;文件系统分区:ubifs。

¾ eMMC器件:裸分区:不支持;文件系统分区:ext4。

− partition_type:器件类型(“MTD”or“EMMC”)。

− location:分区名或者分区对应的设备节点。

¾ NAND Flash器件,分区名:system。

¾ eMMC器件分区,对应的设备节点: /dev/block/platform/hi_mci.1/byname/syste。

− fs_size:0表示擦除整个分区。

− mount_point:分区挂载点。

z package_extract_file(char *package_path, char *destination_path)

功能:从zip包中提取单个文件。

参数:

− package_path:解压的文件。

− destination_path:解压到的目标路径。

z write_raw_image (char *file, char *partition)

功能:将单个文件写入分区。

参数:

− file:欲写入的文件。

− partition:欲写入的分区。

z mount(char *fs_type, char *partition_type, char *location, char *mount_point)

功能:挂载特定分区到某目录下

参数:

− fs_type:文件系统类型(“ubifs”or “ext4”)。

− partition_type:器件类型(“MTD”or “EMMC”)。

− location:分区名字或者分区对应的设备节点。

¾ NAND Flash器件,分区名:system

¾ eMMC器件分区,对应的设备节点:

/dev/block/platform/hi_mci.1/by-name/system

− mount_point:挂载点。

z unmount(char *mount_point)

功能:卸载分区。

参数:

− mount_point:分区挂载点。

z package_extract_dir(char *package_path, char *destination_path)

功能:直接提取一文件夹并直接解压到相应目录。

参数:

− package_path:zip压缩包里面要提取的文件夹名。

− destination_path:解压到的目录。

z symlink(char *name, char *argv[])

功能:将argv* 指向的内容全部链接到name文件。

参数:

− name:想要链接到的文件名。

− argv:想要链接的文件。

z set_perm_recursive (int uid, int gid, int dir_mode, int file_mode,char *path)

功能:修改目录权限及目录内文件的权限。

参数:

− uid:用户id。

− gid:组 id。

− dir_mode:目录权限。

− file_mode:目录内文件权限。

− path:目录路径。

z partchange(char *partition_type, char *new_partition)

功能:依据传入的分区信息,在内核中建立新的分区

参数:

− partition_type:器件类型(“MTD”or “EMMC”)

− new_partiton:分区信息

− EMMC器件:关键字是:dev/block/mmcblk0

− MTD器件:关键字是:hinand

partchange函数不支持spi器件,支持Nand Flash,eMMC器件

z setmisc(char *partition_type, char *location)

功能:写misc标记位

− partition_type:器件类型(“MTD”or“EMMC”)

− location:分区名字或者分区对应的设备节点。

¾ NAND Flash器件,分区名:misc

¾ eMMC器件分区,对应的设备节点:

/dev/block/platform/hi_mci.1/by-name/misc

增量升级及升级包的制作

很多时候,我们要升级的固件和上一个版本差的只是一两个APK或者是多了一些库文件,这个时候,如果我们再升级这个system分区,即升级整个system.img就做了很多务必要的工作,而且耗费的流量太大。

从上面的升级脚本看到,其实完全是可以将某个文件\目录按照指定的属性添加到指定的目录下的,同时也可以删除掉某个指定的文件\目录。

这个就是增量升级。

本来,在Android源码中

./build/tools/releasetools/ota_from_target_files -n -i <旧包> <新包> <差分包名> 是可以制作OTA增量升级包的,但是一般,不会这么干,因为这种做法太蠢了。

那怎么做呢,从上面的一大堆话中,其实可以知道升级就是按照按照升级脚本来的。

所以,升级包(rom包)制作方式:

1、改一个自己需要的升级脚本,可以试增量升级,也可以是整个镜像升级。(当然脚本还是放在哪个目录下,然后update-binary也得支持这些脚本命令才行)

2、然后把要的东西(APK,库,镜像)和升级脚本打包成一个update.zip,在用源码中的key给这个升级包进行签名,然后就做成一个可以用的升级包了。(当然了,手机刷机常用的rom包,其实也是一样的,不过这个时候就是升级整个system.img,或者根据需要再升级某些指定的分区。)

怎么签名:

java -jar out/host/linux-x86/framework/signapk.jar -w build/target/product/security/testkey.x509.pem build/target/product/security/testkey.pk8 ~/export/update_signed.zip ~/export/updatesigned.zip
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: