关于Recovery及OTA升级的一些积累

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、具体是要进入哪个系统,这个是又BootLoader来决定的(因为你基本可以认为他就是开机的第一个程序),他决定进那个就哪个。当然,这个也是由我们来告诉BootLoader的。
3、怎么告诉呢?有两种方式。
第一种,就是按组合键,(就相当于,我们告诉BootLoader,开机检测到有人按了这个组合件就是进recovery,别进安卓。这个当然就可以定制了,例如在机顶盒,我们通常是改成按遥控器,如”连续按上中下键“)。
第二种,就是Android告诉BootLoader下次启动进recovery,然后Android自己再重启。这个就关系到了另一个分区,叫MISC(你基本可以认为他就是存储recovery命令的)。因为BootLoader启动会去读这个MISC分区中的内容,来决定自己进哪个系统。系统固件升级和恢复出厂都属于这种,就是recovery命令有点不一样。

关于Recovery及OTA升级的一些积累_第1张图片

recovery流程

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

流程如下:

1、一般来说,在init.reovery.rc中,就可以看到启动了/sbin/recovery,这就启动了recovery来作为一个service
2、recovery进程简介:
关于Recovery及OTA升级的一些积累_第2张图片

简单分析一下:
  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块中。这个过程可以用一个简单的图来概括,这样更清晰:
关于Recovery及OTA升级的一些积累_第3张图片

(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的必经之路。其大体流程如下:
关于Recovery及OTA升级的一些积累_第4张图片
① 将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最复杂的也是最核心的部分,就是他完成刷机(固件升级)。详细说下:
关于Recovery及OTA升级的一些积累_第5张图片

①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 (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.

利用上面几个固定的字节对应的固定的值,可以进行指纹校验,这是第一步和第二步的校验
第三步校验4b 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

之前讲过Android中recovery的基本知识。
在工作中,需要做的经常是对标准recovery做一些定制化,所以这篇文章,记录下这段时间的一些心得:

1、增量升级:
在源码根目录下自行make otapackage 会生成升级包,两次编译后包用下面工具:
./build/tools/releasetools/ota_from_target_files -n -i <旧包> <新包> <差分包名> ,可以制作增量升级包。这里必须用中间生成的包才行。
改进方法:
前面说的可以自己写一个简单的Linux脚本,把修改后的升级脚本和文件进行打包签名,这样可以比源码中直接make otapackage效率要高一些,同时也更灵活。
有时你这个版本只是多了一两个APK,就可以在脚本用mount挂在system分区->package_extract_file直接将APK解压到制定的目录。

2、添加一些脚本命令:
例如,现在这个版本是想要减掉上一个版本的一个system/app/下的一个APK,如果我们可以自己给脚本解析器增加一个delete的命令。
例如,增加设置命令的接口来给fastboot发送命令,让recovery系统去告诉fastboot去完成一些只能在fastboot中完成的工作。遇到两次需要这样做:(1)当时,有一个工作是要求在recovery中增加重新划分分区的功能,因为这个工作只能在fastboot中完成,所以我就是这样做的。
(2)还有一次是恢复出厂设置后,有些fastboot中的env需要重新设置,才能算是真正的恢复出厂,就也是让recovery去告诉fastboot重新设置下env。

3、修改升级时的画面:
这个基本每次都要做的,如果只是使用安卓原本的recovery来做的话,就只要去源码下bootable/recovery/res/images把图片换换,然后修改下位置和一些简单的细节就可以了。
不过了解下它的实现也好:

Recovery UI 在recovery源代码recovery.cpp中main有
    Device* device = make_device();       
    ui = device->GetUI();
    gCurrentUI = ui;
 
    ui->Init();
    ui->SetLocale(locale);
    ui->SetBackground(RecoveryUI::NONE);
if(show_text) ui->ShowText(true);

(1)首先新建了一个Device类的对象, Device类封装了一些操作,包括UI的操作
(2)调用Device类的GetUI()返回一个DefaultUI对象,recovery中涉及到三个UI类,三个类之间为继承关系,分别为DefaultUI、 ScreenRecoveryUI、RecoveryUI
(3)调用DefaultUI类的Init(), DefaultUI类没有Init()方法,因此将调用它的父类ScreenRecoveryUI的Init()
(4)同理,调用ScreenRecoveryUI类的SetLocale()来标识几个比较特别的区域
(5)同理,调用ScreenRecoveryUI类的SetBackground()设置初始状态的背景图
(6)显示recovery的主界面,即一个选择菜单
graphics.c给出一些接口,这些接口会调用Pixelflinger的源代码给出的接口,以下是部分接口。
Pixelflinger库来进行渲染。 附上minui部分接口的说明,供参考

int gr_init(void);            /* 初始化图形显示,主要是打开设备、分配内存、初始化一些参数 */ 
void gr_exit(void);           /* 注销图形显示,关闭设备并释放内存 */   
int gr_fb_width(void);        /* 获取屏幕的宽度 */ 
int gr_fb_height(void);       /* 获取屏幕的高度 */ 
gr_pixel *gr_fb_data(void);   /* 获取显示数据缓存的地址 */ 
void gr_flip(void);           /* 刷新显示内容 */ 
void gr_fb_blank(bool blank); /* 清屏 */ 
void gr_color(unsignedcharr, unsignedcharg, unsignedcharb, unsignedchara); /* 设置颜色 */ 
void gr_fill(intx,inty,intw,inth); /* 填充矩形区域,参数分别代表起始坐标、矩形区域大小 */ 
int gr_text(intx,inty,constchar*s); /* 显示字符串 */ 
int gr_measure(constchar*s);            /* 获取字符串在默认字库中占用的像素长度 */ 
void gr_font_size(int*x,int*y);        /* 获取当前字库一个字符所占的长宽 */  
void gr_blit(gr_surface source,intsx,intsy,intw,inth,intdx,intdy); /* 填充由source指定的图片 */ 
unsigned int gr_get_width(gr_surface surface);  /* 获取图片宽度 */ 
Unsigned int gr_get_height(gr_surface surface); /* 获取图片高度 */ 
/* 根据图片创建显示资源数据,name为图片在mk文件指定的相对路径 */ 
int res_create_surface(constchar* name, gr_surface* pSurface); 
void res_free_surface(gr_surface surface);      /* 释放资源数据 */

screen_ui.cpp给出了设置的流程,跟踪这个代码可以知道显示的方法,要注意的是,显示文字的界面必须在showtext为TRUE的时候才会显示,所以用此方法来实现进程界面和选择界面的变换。

4、字体修改
字体比较麻烦吧,我记得我修改字体的时候,觉得很麻烦,不清楚有没有比较好的方法。
我的方法:先在graphics.c文件中修改字体头文件。然后:
(1)在recovery/miniui中有制作头文件的源码mkfont.c,在/recovery/font中有字体图片,需用gimp工具得到mkfont.c编译所要的结构体。注意gimp输出.c文件时,全部选项都不要选。
(2)制作的字体文件.h存在的不足是底色和字体色的问题。修改mkfont.c文件让其相反输出即可。
(3)在graphics.c文件中的static void gr_init_font(void)//字体函数是对字体的初始化,在这里是判断字体头文件中的字体,根据阈值0x80来选择透明度,源码默认是255,所以无论怎么调色,最后都是黑色
(4)对颜色的修改就要先修改第3点所述部分,再在int gr_text(int x, int y, const char *s)增加想要的字体颜色即可 ,如:gr_color(255,255,255,252);

5、语言
有时,机器给不同国家,recovery也就要求显示不同语言,那就用这个函数:
SetLocale, 该函数根据locale判断所用的字体是否属于阿拉伯语系,阿拉伯语的书写习惯是从右到左,如果是阿拉伯语系的话,就设置一个标志,后面根据这个标志决定从右到左显示文字或进度条。 SetLocale的参数locale赋值逻辑是这样的,先从command文件中读取, command文件中设置locale的命令如”–locale=zh_CN“,如果没有传入locale,初始化过程中会尝试从/cache/recovery/last_locale中读取locale, 如果该文件也没有,则locale不会被赋值,就默认用English.
这个其实也是在setting中设置的,会设置为env保持在fastboot中。(我用的方案是这样,不清楚是不是安卓原本的)

6、recovery备份分区
7、增加备份与备份恢复
8、恢复最初升级包的两种方案
9、硬件信息与软件信息

你可能感兴趣的:(android,Recovery,OTA)