Android的recovery是我在公司做的最多的,应该也是我在Android中了解的较为深入的一个部分。recovery这部分其实Android本身都已经提供了很完善的一套机制,但是因为公司是做机顶盒的,所以在因为平台订制的关系,recovery这部分还是做了很多修改的。
首先,修改的比较少的是OTT这种盒子,本次也主要讲这种,其实这种和手机区别不大。而类似将DVB 中的loader和Android的recovery整合到一起这种,确实比较不一样。例如在我们公司,整个升级的签名校验订制以及升级所用到的update.zip包中的烧录进程就都是自己一套的。
但是万变不离其中,其实了解了就发现其实也都是那样。
所以简单讲下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系统是一个类似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分区。(这种就所谓的恢复出厂设置)
上面已经说过,这个基本是整个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) ff 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.
利用上面几个固定的字节对应的固定的值,可以进行指纹校验,这是第一步和第二步的校验
第三步校验 50 4b 05 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