原创文章,欢迎转载,转载请注明出处http://www.cnblogs.com/becklc/archive/2012/09/24/2676600.html
本文依据android2.3源码只分析Recovery相关原理,不针对代码走读,现在Android版本已经4.x.x但是recovery的基本原理不变。
一、Recovery是如何构成的
说recovery的构成并不贴切,应该说recovery.img的构成,它是由boot_img_hdr + zImage + recovery-ramdisk构成。boot_img_hd是个结构体它描述了很多重要的信息。
1 struct boot_img_hdr 2 { 3 unsigned char magic[BOOT_MAGIC_SIZE]; 4 unsigned kernel_size; /* size in bytes */ 5 unsigned kernel_addr; /* physical load addr */ 6 unsigned ramdisk_size; /* size in bytes */ 7 unsigned ramdisk_addr; /* physical load addr */ 8 unsigned second_size; /* size in bytes */ 9 unsigned second_addr; /* physical load addr */ 10 unsigned tags_addr; /* physical addr for kernel tags */ 11 unsigned page_size; /* flash page size we assume */ 12 unsigned unused[2]; /* future expansion: should be 0 */ 13 unsigned char name[BOOT_NAME_SIZE]; /* asciiz product name */ 14 unsigned char cmdline[BOOT_ARGS_SIZE]; 15 unsigned id[8]; /* timestamp / checksum / sha1 / etc */ 16 };
其中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打包生成的命令如下:
mkbootfs ramdiskdir | gzip > recovery-ramdisk.gz
我们可以通过解压recovery.img来获取真正的recovery可执行文件,操作如下:
1 ./split_bootimg.pl recovery.img 2 mkdir out 3 cd out 4 gunzip -c ../recovery.img-ramdisk.gz | cpio -i
/sbin目录下的recovery就是可执行文件了,还原可参照如下操作:
1 find . | cpio -o -H newc | gzip > ../recovery.img-ramdisk.gz 2 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结构信息。
1 struct bootloader_message { 2 char command[32]; 3 char status[32]; 4 char recovery[1024]; 5 };
(这里顺便提一下:如果你的存储芯片是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中有如下一条:
service recovery /sbin/recovery
执行到这条时我们的recovery就算启动了。
题外话:我们可以发现recovery可执行文件是静态编译的,之所以这样是因为recovery模式中没有共享库还有缺动态链接库加载器(/system/bin/linker)。
三、Recovery是如何工作的
上图主要描述recovery执行的主要功能的主体框架
1、UpdateXXX.zip
我们知道recovery升级时需要一个特殊的UpdateXXX.zip包,解压出来可以发现META-INF是一定会有的,在看其里面的内容:
|-- CERT.RSA |-- CERT.SF |-- com | `-- google | `-- android | |-- update-binary | |-- updater-script | `-- update-script `-- MANIFEST.MF
其中 CERT.RSA、CERT.SF、MANIFEST.MF是做包时产生的签名文件如下脚本可以生成:
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为例)
1 format("MTD", "system"); 2 mount("MTD", "system", "/system"); 3 package_extract_dir("system", "/system"); 4 package_extract_file("boot.img", "/tmp/boot.img"), 5 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默认的传递参数的区域,可以换掉
上述只例举笔者在工作中遇到的一些情况,不同的平台、环境、需求时需注意的地方可能不尽相同
本文主要分析了Recovery涉及到的相关原理,从这么一个小小的系统中可以发现它涵盖了大量的知识点,此篇只揭开了它一片小小的面纱。