您的位置:首页 > 其它

安卓手机Recovery概述和原理分析

2019-04-30 10:35 246 查看
版权声明:本文为博主原创文章,未经博主允许不得转载 https://blog.csdn.net/wteruiycbqqvwt/article/details/89704239

安卓手机Recovery概述

1、Recovery是用户想要刷机的过程中经常会遇到的一个词。那么什么是Recovery?Recovery模式又是什么意思?手机怎么进入Recovery模式?

2、Recovery的字面意思是恢复、复原。对于手机来说,Recovery就是安卓的手机提供的一种可以对手机内部的数据或系统进行修改的模式,类似于windows PE或DOS。在手机进入Recovery后,可以将手机恢复到出厂设置、升级手机的系统、对手机进行刷机等等。

手机怎么进入Recovery模式?

不同的安卓手机进入Recovery模式的情况略有不同,主要有以下几种:

1、将手机关机后,同时按住音量上键和电源键,按住3秒,即可进入 Recovery模式;

2、将手机关机后,同时按住音量下键和电源键,按住3秒,即可进入 Recovery模式;

3、将手机关机后,同时按住音量上键、Home键和电源键 ,待手机震动后松开电源键(音量+、HOME键继续保持按住)才能进入。

如果以上方法都不行,请上网搜索自己的机型,查找具体办法。

进入Recovery模式后,手机上的显示类似下图(不同的手机差别很大)

3、Recovery模式英文翻译在手机进入Recovery模式后,使用音量上下键,可以移动光标,按电源键表示确认。目前大多数的手机Recovery都是英文,那么这些英文具体是什么意思呢?

下面给出翻译:

主界面reboot system now:重启手机(刷机完毕选择此项就能重新启动系统)

apply SDcard:update.zip:安装手机内存卡中的update.zip升级包(一般卡刷包的名字就是这个,选择这项实际上就是卡刷升级)

wipe data/factory reset:清除用户数据并恢复出厂设置(刷机前必须执行的选项)

wipe cache partition:清除系统缓存(刷机前需要执行这个。系统出问题也可尝试此选项,一般能够解决)

install zip from SDcard:安装手机SD卡上的升级包(可以执行任意名称的zip升级包,不限制升级包名称)

backup and restore:备份和还原系统

mounts and storage:挂载和存储选项(进入挂载和存储选项二级菜单,见下方)

advanced:高级设置

4、备份还原

Backup:备份当前系统

Restore:还原上一个系统

Advanced Restore:高级还原选项(用户可以选择之前备份的系统,然后进行恢复)

挂载和存储选项

mount /system:挂载system文件夹(基本用不到)

mount /data:挂载data文件夹(基本用不到)

unmount /cache:取消cache文件夹挂载(基本用不到)

unmount /sdcard:取消内存卡挂载(基本用不到)

mount /sd-ext:挂载内存卡Ext分区(基本用不到)

format boot:格式化boot(刷机前最好执行一下)

format system:格式化system(刷机前最好执行一下)

format data:格式化data(刷机前最好执行一下)

format cache:格式化cache(刷机前最好执行一下)

format sdcard:格式化手机SD卡(小心!会清除手机SD上的所有数据!!)

format sd-ext:格式化内存卡Ext分区(小心!会清除手机SD上的Ext分区数据!!)

mount USB storage:开启Recovery模式下的USB大容量存储功能(也就是说可以在Recovery下对内存卡进行读写操作)

高级设置

Reboot Recovery:重启Recovery(重启手机并再次进入Recovery)

Wipe Dalvik Cache:清空虚拟机缓存。

 

小结:recovery的使用原理就和电脑上的GHOST软件一样,起到系统备份与还原作用。当然也可以升级内核,刷补丁等。recovery的编写是和系统基带联系在一起的,不同的基带版本不能乱刷。

recovery原理分析

本文依据android2.3源码只分析Recovery相关原理,不针对代码走读,现在Android版本已经4.x.x但是recovery的基本原理不变。

一、Recovery是如何构成的

  说recovery的构成并不贴切,应该说recovery.img的构成,它是由boot_img_hdr + zImage + recovery-ramdisk构成。boot_img_hd是个结构体它描述了很多重要的信息。

[code]struct boot_img_hdr
{
unsigned char magic[BOOT_MAGIC_SIZE];
unsigned kernel_size;  /* size in bytes */
unsigned kernel_addr;  /* physical load addr */
unsigned ramdisk_size; /* size in bytes */
unsigned ramdisk_addr; /* physical load addr */
unsigned second_size;  /* size in bytes */
unsigned second_addr;  /* physical load addr */
unsigned tags_addr;    /* physical addr for kernel tags */
unsigned page_size;    /* flash page size we assume */
unsigned unused[2];    /* future expansion: should be 0 */
unsigned char name[BOOT_NAME_SIZE]; /* asciiz product name */
unsigned char cmdline[BOOT_ARGS_SIZE];
unsigned id[8]; /* timestamp / checksum / sha1 / etc */
};

其中kernel_size表示zImage的实际大小;kernel_addr表示zImage载入内存的物理地址,这个地址也是bootloader跳转到内核的地址;ramdisk_size表示ramdisk(此处就是指recovery-ramdisk)的实际大小;ramdisk_addr是ramdisk加载到内存的物理地址,之后kernel会解压并把它挂在成根文件系统,我们的中枢神经-init.rc就隐藏于内;second_size,second_addr做扩展用一般都不会使用;tags_addr是传参数用的物理内存地址,它作用是把bootloader中的参数传递给kernel;page_size是flash(eg. nandflash)的一个页大小,一般为2K,这通常情况取决于你使用Flash芯片的页大小;cmdline就是command line它可以由bootloader传递也可以在.config(kernel)中配置。zImage是我们熟悉的内核镜像,由kernel编译生成。recovery-ramdisk是由mkbootfs、gzip打包生成的命令如下:

[code]mkbootfs ramdiskdir | gzip > recovery-ramdisk.gz

我们可以通过解压recovery.img来获取真正的recovery可执行文件,操作如下:

[code]./split_bootimg.pl recovery.img
mkdir out
cd out
gunzip -c ../recovery.img-ramdisk.gz | cpio -i

/sbin目录下的recovery就是可执行文件了,还原可参照如下操作:

[code]find . | cpio -o -H newc | gzip > ../recovery.img-ramdisk.gz
mkbootimg --kernel recovery.img-kernel --ramdisk recovery.img-ramdisk.gz -o new-recovery.img

注: 上述使用到的工具下载(android-img-tools

        recovery.img与boot.img在结构上是一样的。

        现在很多平台的打包方式是不一样的,它们的做法是编译时把kernel镜像和根文件系统打包在一起合称为zImage。

        本文提到的bootloader指blob,下同。

二、Recovery是如何启动的

       请看如下流程图:

这个流程图只是大概描述bootloader中选择启动部分的流程。当设备上电或Reboot进入bootloader时会检测此时是否有特殊键被按下也就是流程图中的KeyPress,例如某手机开机时同时按下照相键+音量键就会进入recovery。有人会问如何检测按键被按下?很简单就是读键盘控制寄存器的值,如果你是GPIO按键就读GPIO的寄存器。如果没有键被按下,bootloader会读取misc分区中bootloader_message结构信息。

[code]struct bootloader_message {
char command[32];
char status[32];
char recovery[1024];
};

(这里顺便提一下:如果你的存储芯片是NandFlash bootloader_message是存放在Misc分区Block 0的第二个页上,如果是MMC那么就是这个分区的开始位置。在Android兴起之初NandFlash很流行,但由于MMC总线接口简单统一、大容量、易于更换大有替换NandFlash之势)再看bootloader_message中的command如果其内容是"boot-recovery"那么bootloader会进入recovery;

bootloader_message中的recovery是放解析的参数,它是告诉Recovery系统需执行什么样的动作,

如:"recovery\n--update_package=CACHE:update.zip",这样Recovery系统就会明白它将要更新AndroidOS系统,升级包是/cache/update.zip。

同样还有"recovery\n--wipe_date" 清除用户数据后重启,通常用它做恢复出厂设置功能;

再有"recovery\n--wipe_cache"清除cachec分区内容后重启,cache分区一般放临时文件用;当然你也可以自定义实现你想要的功能。

那么这个bootloader_message是谁写入的呢?当你需要OTA升级的时候,系统会到指定地址下载升级包放入Cache分区,然后把bootloader_message信息写入Misc分区然后重启,还有当你需要恢复出厂设置时它也会进行同样的操作。

bootloader加载recovery.img时先读取img头信息(上文提到的boot_img_hdr)把zImage和recovery-ramdisk加载至制定内存地址,设置参数等操作后,跳转至kernel在内存中的地址(就是kernel_addr),至此kernel被加载启动。挂载根文件后执行init,init会解析init.rc,大家注意init.rc中有如下一条:

[code]service recovery /sbin/recovery

执行到这条时我们的recovery就算启动了。

题外话:我们可以发现recovery可执行文件是静态编译的,之所以这样是因为recovery模式中没有共享库还有缺动态链接库加载器(/system/bin/linker)。

三、Recovery是如何工作的

上图主要描述recovery执行的主要功能的主体框架

    1、UpdateXXX.zip   

    我们知道recovery升级时需要一个特殊的UpdateXXX.zip包,解压出来可以发现META-INF是一定会有的,在看其里面的内容:

[code]|-- CERT.RSA
|-- CERT.SF
|-- com
|   `-- google
|       `-- android
|           |-- update-binary
|           |-- updater-script
|           `-- update-script
`-- MANIFEST.MF

其中 CERT.RSA、CERT.SF、MANIFEST.MF是做包时产生的签名文件如下脚本可以生成:

[code]java -jar signapk.jar xxx.pem xxx.pk8  xxx-unsigned.zip  updatexxx-signed.zip

其中密钥文件xxx.pem xxx.pk8 在 build/target/product/security/下就可以找到,signapk.jar是由build/tools/signapk/编译生成的。当然各个厂家为了不混淆自家的升级包文件可能会定制自己的密钥文件。

接下来我们来看看update-binary、updater-script、update-script这三个文件。

其中update-script是Amend脚本,在Android1.5之前使用的现在已经不再支持,它是由Recovery直接解析执行的。

现在使用的updater-script是Edify脚本,是由update-binary解析执行的。后者的好处在于,它完全独立于Recovery系统,在recovery中fork出一个进程来执行update-binary的,而且用户可以扩展供执行的解析命令,这样灵活性很高,可以做的事情越多。

2、升级时recovery是如何更新系统的?

    细心的朋友可能早就发现了,boot.img和system.img虽然都叫xxx.img其实他们有本质的不同。boot.img不带有文件系统的信息,通俗的话说就是"裸数据",直接写入分区就好;而system.img是带有文件信息的,所带有的文件信息最终取决于你的存储芯片和你所使用的文件系统,比如使用的存储芯片是NandFlash,那么文件系统可能是Yaffs(2)、Jffs(2)等等,很大一部分是Yaffs2,那system.img是带有Yaffs2的文件信息(包括文件类型、文件chunk数、chunkID、Yaffs2的OOB信息等等),如果使用的存储芯片是MMC,那么文件系统可能是ext2、ext3、ext4、fat32等等,很多使用的是ext系列。那么update.zip需要升级boot.img和system.img怎么实现呢?我们先看如下脚本(以NandFlash-Yaffs2为例)

[code]format("MTD", "system");
mount("MTD", "system", "/system");
package_extract_dir("system", "/system");

package_extract_file("boot.img", "/tmp/boot.img"),
write_raw_image("/tmp/boot.img", "boot");

我们可以发现对更新boot.img、system.img是不同的脚本。

更新system时先格式化system分区,大家看到MTD并不是格式化成MTD文件系统,MTD是Nandflash驱动的管理层,Yaffs2文件系统是构建MTD层之上,MTD起到呈上启下的作用,另像分区信息、设备节点等都有MTD层来管理。如果查看代码就会发现format类型是MTD时就会把这个分区格式化成Yaffs2的。我们可以通过cat /proc/mtd来查看分区信息。格式化完成之后我们会把system分区挂载到某一目录下,然后把update.zip中的system文件下所有文件写到刚挂载的目录下。

而对于boot.img的更新是直接把update.zip中的boot.img写到设备中也就是nandflash芯片中。

那么两者同样是写有什么不同呢?直接写设备节点是直接由mtd接口来完成的,nandflash只在涉及到的页的OOB区添加ECC校验信息。但是如果是带有文件系统的写首先需要通过Yaffs文件系统的文件接口加上OOB的一些信息,然后再通过MTD接口来完成,nandflash在涉及到的页的OOB区添加ECC和文件系统的一些信息如:chunkid、chunksize等信息。

    3、几条很好用的脚本命令

    getprop   获取系统属性,eg:getprop("ro.flag")=="ok"                                            一般和其他脚本联合使用

    assert      脚本中的断言,eg:assert(getprop("ro.flag") == "ok");                             如果失败则终止脚本继续执行

    ifelse        脚本中的判断,eg:ifelse(getprop("ro.flag") == "ok", ui_print("ok"), ui_print("no,ok"));

    run_program    执行第三方可执行文件,eg:run_program("test_binary");                    可执行文件必须是静态编译的

    脚本命令这里就不一一列举了,网上资料很多。

四、Recovery的适配及修改

    遇到一个新项目Recovery需要修改哪些呢?

    1、进入按键,在bootloader中需要检测哪些键被按下时进入Recovery

    2、分区信息,如果你是nandflash存储芯片可以查看cat /proc/mtd, 若是eMMC看recovery.fstab文件中的内容是否正确

    3、recovery中的按键,上下选择及执行按键是否映射正确,可以参看kernel中的头文件<linux/input.h>

    4、是否支持你的文件系统,笔者分析使用的代码只支持Yaffs2及ext3的

    5、签名验证,签名验证的密钥证书是否一致,可以写个小应用验证下

    6、显示,framebuffer设备节点是否一致读写是否正常,tty设备配置成图形类型是否正常,RGB是否适配

    7、misc分区,misc分区是recovery默认的传递参数的区域,可以换掉

 

安卓原生OTA和Recovery升级过程步骤

进入升级

上图中的上下两个部分,上面一部分是正常的启动模式,下面一部分为Recovery模式。正常的启动模式是从boot.img启动系统(Main System),而recovery模式则是从reovery.img启动系统;(reovery.img只包含内核、简单的文件管理系统和图形系统)

Boot分区包括linux内核和Ramdisk,Recovery分区也包括Linux内核和Ramdisk,一般来说内核是一样的,但Ramdisk区别则非常大,recovery中的ramdisk会有更多的recovery需要使用的程序和数据。

分区介绍

       这里说到的boot.img和recovery.img,其实就对应了一个Android设备中的分区,一般来说,android设备会包含以下几个分区

Boot:包含Linux内核和一个最小的root文件系统(装载到RAM disk中),用于挂载系统和其他的分区,并开始Runtime 
System:包括了系统应用和库文件(AOSP中可以获取到源代码),在运行的过程中,这个分区是read-only的,只有在OTA升级的时候才会发生变化 
Vendor:包括了系统应用和库文件(AOSP中不能获取到源代码),和System分区一样,只有在OTA升级的时候才会发生变化 
Userdata:用户安装的应用程序会把数据保存在这里,正常情况下OTA是不会清除这里的数据的,指定要删除出具的除外 
Cache:临时的保存应用数据(要把数据保存在这里,需要特地的app permission),OTA的升级包也可以保存在这里。OTA升级过程可能会清除这个分区的数据。 
Recovery:包括了一个完整Linux内核和一些特殊的recovery binary,可以读取升级文件用这些文件来更新其他的分区 
Misc:一个非常小的分区,recovery用这个分区来保存一些关于升级的信息,应对升级过程中的设备掉电重启的状况

这些分区是Google官方的标准,实际的情况可能不太一样,就Find 7而言,刷机包里面的分区只有以下几个 :芯片厂商和手机厂商会根据自己的需要加一些其他的分区,如下面的persist.img是高通的,reserve4是我们自己加的保留分区,MTK还有preloader、lk,高通的还有NON-HLOS.bin、sbl、emms_aboot等。

Bootloader

      - 2.1 什么是Bootloader? 
在嵌入式操作系统中,Bootloader在操作系统内核运行之前运行,可以初始化硬件设备、建立内存空间映射图,为调用操作系统内核准备好正确的环境。Bootloader和硬件是强相关的,且厂商一般都会对bootloader加锁,这样就不能随便刷机了。 
当然bootloader也是可以解锁的,这里不得不提一下root和bootloader解锁分别是怎么一回事:root是通过内核漏洞获取最高的权限,也就是所谓的超级用户(su,superuser),属于系统层面,root之后就可以修改system分区的数据;bootloader解锁则属于硬件层面的解锁boot和recovery分区,解锁bootloader不会root手机。

      - 2.2 Fastboot和recovery的区别? 
Bootloader过程中,先做一些初始化,然后根据组合键做不同的事情,这个过程内核没有加载,机器知识在按顺序执行指令。 
Fastboot:在这种模式下,可以修改手机的硬件,并且允许我们发送一些命令给Bootloader。如使用电脑刷机,则需要进入fastboot模式,通过电脑执行命令将系统镜像刷到通过USB刷到手机中。 
Recovery:Recovery是一个小型的操作系统,并且会加载部分文件系统,这样才能从sdcard中读取升级包。 
Bootloader.cpp 
Bootloader(bootable/recovery/bootloader.cpp)会读取位于MISC分区的启动控制信息块BCB(Bootloader Control Block)

如果command的值为“boot-recovery”时,就进入recovery模式。Recovery升级过程中掉电,下次按power键开机也会进入recovery模式就是因为misc分区依然存在recovery信息(掉电保护)。

Recovery模式

      - 3.1 Recovery、Bootloader以及Main System通信方法 
图1中给出模式比较简单,实际上三个实体的交互过程并不是图1那种单向的顺序流程,而是一个双向的流程,于是就涉及到个三个部分的通信方法的实现。 
      Recovery和Main System交互 
      这一部分源代码(bootable/recovery/recovery.cpp)注释中有详细的介绍,这里大致总结一下:这两个部分交互是通过/cache文件来实现的,涉及到的文件如下表。 

Recovery、Main System和Bootloader交互 
        这个交互过程是通过BCB来完成的,BCB其实就是一个struct(在misc分区),在(bootable/recovery/bootloader.h)中定义。

Command:重启进入recovery或者是更新radio或Bootloader硬件时,会更新这个域; 
     Status:在bootloader完成“update-radio”和”update-hboot”命令之后更新; 
     Recovery:用于system和recovery之间的通信; 
     Stage:需要重启多次的packages会写入这个值,表示所处的状态。 
     上面的过程可以总结为如下的示意图:

cache/recovery/command中command有哪些? 
     下表给出一些常用的命令和其含义。 

 - 3.3 Factory Reset—恢复出厂设置 
     恢复出厂设置也在recovery.cpp中完成,具体的流程如下: 
     1) 用户选择恢复出厂设置 
     2) Main System会在cache/recovery/command中写入—wipe-data的命令,然后重启进入recovery 
     3) 通过get_args在BCB中写入boot-recovery和wipe-data命令 
     4) 然后重启进行erase_volume 
     5) 擦除数据之后调用finish_recovery将BCB中的数据清除 
     6) main()函数调用reboot()进入Main System; 

OTA安装

  •      4.1 OTA简介

     OTA,即Over the air,它可以实现完整的版本升级,也可以是增量升级。用户可以选择在SD卡中作本地升级,也可以直接采用网络在线升级。不管是哪种方式,都有几个过程:生成升级包、获取升级包、执行升级包,生成升级包不做介绍,。 
实际上,所谓OTA的整个过程可以用如下示意图表示。 

首先,用户用手机的OTA检查更新(或者是自动更新),发送查询数据给服务器,然后服务器查询到相应的包,并返回下载地址给OTA.apk,然后OTA.apk进行下载,把下载的数据存储在手机的某个分区。完成之后用于选择是否升级,升级的时候OTA会发送命令给Main System,进入recovery,recovery根据利用下载下来的升级包完成升级过程。 
从图中,我们可以看出OTA.apk实际上只是完成了从服务下下载安装包,以及发送升级命令,功能似乎很容易描述,但是实际上要做的事情却是非常多的,特别是OTA 2.0将Applypatch功能移植到OTA.apk之后,复杂度进一步增加了。

  • 4.2 OTA安装流程

    下面对Recovery的升级步骤做一个梳理: 
    1) Main System下载OTA升级包,官方推荐下载到/cache分区下,但是实际上可以自己选择存储地方; 
    2) Main System在/cache/recovery/command中写入“—update_package=安装包路径” 
    3) 系统重启进入recovery,这一步是通过PowerManager的reboot(“recovery”)实现的 
    4) Get_args()在BCB中写入“boot-recovery”和”—update_package“,这样就保证了即便是设备出故障重启了,只有这两个命令还在,就会尝试重新安装OTA升级包 
    5) Install_package尝试开始安装OTA升级包 
    6) Finish_package擦除BCB 
    7) 如果升级失败 Prompt_and_wait显示错误,并等待用户响应 用户重启 
    8) 设备重启进入MainSystem 
    此外,还有个maybe_install_firmware_upadate,具体过程不做介绍了。

      下面是Recovery的代码的区别,实际上2.3/3.0/4.4和5.0的代码差别还是非常大的,原因是5.0的recovery用C++改写了一遍,官方文档两者改写的函数进行了对比 

4.3 Install_package过程

安装过程在/bootable/recovery/install.cpp中, 
install_package(const char* path, int* wipe_cache, const char* install_file, bool needs_mount) 
在recovery的main函数中传入的参数如下: 
install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true); 
参数列表: 
-Path:传入的安装包地址 
-wipe_cache:是否擦除cache分区 
-install_file:实际上是安装过程的临时安装文件的地址(/tmp/last_install),包括install_log 
-needs_mount:是否需要Mount,安装过程传入的是true 
在install_package函数中,其实真正调用的是really_install_package函数: 
result = really_install_package(path, wipe_cache, needs_mount); 
这里对几个重要的流程做一些介绍: 
-Set_install_mounts:检查安装包所在路径的分区是否挂载,否则返回安装失败 
-Set_backgroud:升级过程的UI界面显示 
-Load_keys,加载公钥,我们系统的签名文件放在/build/target/product/security/目录下。这样做的目的是为了防止用户使用不同的项目的升级包进行混刷,导致刷机变砖的情况; 
-verify_file():对升级包update.zip包进行签名验证; 
-mzOpenZipArchive():打开升级包,并将相关的信息拷贝到一个临时的ZipArchinve变量中。这一步并未对我们的update.zip包解压。 
-try_update_binary():在这个函数中才是对我们的update.zip升级的地方。这个函数一开始先根据我们上一步获得的zip包信息,以及升级包的绝对路径将update_binary文件拷贝到内存文件系统的/tmp/update_binary中 
-execv(binary,args)的作用就是去执行binary程序,这个程序的实质就是去解析update.zip包中的updater-script脚本中的命令并执行 

  • 4.4 Update-binary和Updater-script

     上面,我们讲到execv执行二进制文件和Updater-script脚本中的命令,这两个文件在下面的目录。Apply_patch过程其实是会预先在APK端提取出来这两个文件的。 
     try_update_binary运行时,会首先从安装包中读取出update-binary这个可执行文件。创建一个管道(作用稍后会讲到),并在一个新线程中运行这个update-binary(即updater,主函数见bootable/recovery/updater/updater.c),update-binary运行的时候,先会从升级包中读取update-script。 
     bootable/recovery/updater/updater.c中会注册函数,而这些被注册的函数在updater-script中有使用到。

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标识的。)

[code]// --> 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分区。(这种就所谓的恢复出厂设置)

1、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) 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

心得

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把图片换换,然后修改下位置和一些简单的细节就可以了。 

[code]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部分接口的说明,供参考

[code]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中。(我用的方案是这样,不清楚是不是安卓原本的)

安卓分区

除非你只是用你的手机打电话,发短信,和浏览基本的应用程序,你就应该知道,Android使用几个分区来组织管理设备上的文件和文件夹。每个分区都负责设备的特定功能,但不是很多Android用户知道每个分区及其内容的意义。在本指南中,我们将带您参观Android的分区,还有修改它们他们的内容后可能产生的后果。

让我们从Android手机和平板电脑的标准内存分区列表开始。分区有:
/boot
/system
/recovery
/data
/cache
/misc

此外,有SD卡的分区。
/sdcard
/sd-ext

请注意,只有 /sdcard 存在于所有Android设备,其余为目前仅在部分设备。让我们现在就看一看每个这些分区的目的和内容。

/boot

此分区可以保证手机正常启动,他包含了kernel(内核) and ramdisk(虚拟内存盘:通过软件将一部分内存(RAM)模拟为硬盘来使用的一种技术,可以极大的提高在其上进行的文件访问的速度),如果没有此分区,手机通常不能正常启动。只有必要的时候,才去通过Recovery软件擦除(format)这个分区,一旦擦除,设备只有再重新安装一个新的boot分区,可以通过安装一个包含boot分区的ROM来实现,否则无法启动安卓系统。

/system

此分区用来存放除kernel和ramdisk以外的系统相关配置,他包括了用户界面、手机预装的软件等。擦除这个分区将会删除整个系统,但不会导致不能启动。可以通过进入Recovery程序或者bootloader程序中,安装一个新ROM(安卓系统)。

/recovery

在正常分区或内核分区被破坏,不能正常启动时,可以进入此分区进行恢复,他相当与一个简易的OS或blos,可以认为是一个boot分区的替代品,通过他可以让我们在这一分区进行备份维护和恢复,我们通常说的刷机便指的是此分区。

进入此分区方法:
1、通过 adb reboot recovery
2、通过组合键,电源键+音量键


/data
也称为userdata,数据分区包含用户的数据,这是您的联系人,短信,设置和应用程序存放的地方,你必须安装。擦除这个分区,就相当于执行恢复出厂设置,恢复到你第一次启动它的状态,或者最后一次系统升级的状态。当你执行擦除数据/恢复工厂设置时,这个分区将被擦除。可以在Recovery模式中选择“data/factory reset ”擦除此分区。

[code]此分区下的一些常见目录:
/data/data/[packagename]/files 文件缓存目录,一般存小的文件缓存,如果是图片,不建议放这里,一般放到外置卡;
/data/data/[packagename]/cache目录,存放一些其他缓存;
/data/data/[packagename]/databases,存放数据库;
/data/data/[packagename]/lib,应用的so目录;
/data/data/[packagename]/shared_prefs 应用的SharedPreferences保存;


/cache

此分区是安卓系统缓存区,他保存系统最常访问的数据和应用程序。擦除这个分区,不会影响个人数据,只是删除了这个分区中已经保存的缓存内容,缓存内容会在后续手机使用过程中重新自动生成。


/misc

此分区包含了一些系统设置和系统功能启用禁用的相关设置。这些设置包括CID(运营商或区域识别码)、USB设置和一些硬件设置等等。这是一个很重要的分区,如果此分区损坏或者部分数据丢失,手机的一些特定功能可能不能正常工作。这个分区包含ON/OFF开关形式的系统设置。

/sdcard
这不是在设备内存上的分区,而是SD卡。在使用方面,这是你的存储空间,你可以随意存储您的媒体,文件,ROMs等。擦除这个分区是完全安全的,只要你备份所有你需要的数据到你的电脑。但是一些用户安装应用程序、保存数据和设置到SD卡,擦除这个分区会使你失去所有的数据。

在拥有内部和外部SD卡设备上,如三星Galaxy S 和 某些设备,/sdcard分区是指向内部SD卡。对于外部的SD卡,如果存在的话,不同的设备有不同的分区。例如三星Galaxy S系列设备,它是 /sdcard/sd ,而在许多其他设备,它是sdcard2 。不像/sdcard,没有任何系统或应用程序数据会自动存储在外部SD卡,它目前的一切都是由用户添加的。你可以在备份后安全地擦除任何数据。

/sd-ext

SD卡扩展分区,即我们通常所说的外部存储区。
这不是一个标准的分区,但已经在自定义ROM中流行。它基本上是与具有特殊功能称为APP2SD+或data2ext启用某些ROM中使用时充当/data分区上的SD卡额外的分区,使用它与支持此功能的自定义ROM,获得额外的存储空间用于安装他们的应用程序。擦除这个分区在本质上与擦/data分区是一样的,你失去了你的通讯录,短信,应用市场和设置。

安卓新增分区以及挂载方法

以高通平台代码为例来写的。以新增kaicom分区举例:
1.在partition.xml新增一个分区,分区名字为kaicom,分区block大小为262144,新生成GUID,权限为可读可写,使用kaicom.img类型来格式化,检查kaicom.img是否为稀疏类型
<partition label="kaicom" size_in_kb="262144" type="142EC413-03AB-4D39-B3DE-DA06F1AACF7C" bootable="false" readonly="false" filename="kaicom.img" sparse="true"/>

2.制作kaicom.img 类型要求为 ext4类型,格式为稀疏格式,打包内容为空。

打包方法:

 ①电脑连接设备,并获取root权限

 ②进入adb shell后,在data目录下创建一个空目录,如test目录

 ③执行命令 make_ext4fs -s -l 256M test.ext4 test 将test目录打包成 大小为256M的稀疏格式的ext4类型的文件,并命名test.ext4

 ④重命名test.ext4文件为kaicom.img待用

3.将修改后的partition.xml拷贝至高通代码的modem代码common/build目录下 生成对应分区文件以及rawprogram0.xml 待用

4.在安卓代码中修改init.rc脚本,创建挂载点 /kaicom 

5.在安卓代码中修改fstab表,挂载kaicom分区

6.将修改后的kaicom.img,安卓编译生成文件,modem编译生成的xml,全部替换到完整版本中,下载后设备第一次开机时即可创建并挂载/kaicom

== 注意 ==
kaicom.img文件打包空文件test,烧录kaicom.img到kaicom分区将擦除kaicom分区全部文件,谨慎使用。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: