一、Recovery系统简介
Recovery模式指的是一种可以对安卓机内部的数据或系统进行修改的模式(类似于windows PE或DOS)。在这个模式下我们可以刷入新的Android系统,或者对已有的系统进行备份或升级,也可以在此模式下恢复出厂设置。系统进入recovery模式后会装载recovery分区,该分区包含recovery.img(与boot.img类似,也包含了标准的内核和根文件系统).进入该模式后主要就是运行了recovery服务(/sbin/recovery)。
二、Recovery系统的启动流程
2.1 进入Recovery系统的三种常见方法:
- 在关机情况下,同时按住电源(Power)+ 音量加(Vol +)键,直到出现Recovery界面为止。注:有的系统按键方式可能不同。
- 使用安卓辅助工具,如:刷机精灵、360手机助手等等。
- 使用adb命令reboot recovery启动。
2.2 从reboot启动到Recovery服务的流程
在Bootloader开始如果没有组合键按下,就从MISC分区读取BCB块的command字段(在主系统时已经将“boot-recovery”写入)。然后就以Recovery模式开始启动。与正常启动不同的是Recovery模式下加载的镜像是recovery.img。这个镜像同boot.img类似,也包含了标准的内核和根文件系统。其后就与正常的系统启动类似,也是启动内核,然后启动文件系统。在进入文件系统后会执行/init,init的配置文件在bootable/recovery/etc/init.rc。这个文件的主要作用就是:
- 设置环境变量。
- 建立etc连接。
- 新建目录,备用。
- 挂载/tmp为内存文件系统tmpfs
- 启动recovery(/sbin/recovery)服务。
- 启动adbd服务(用于调试)。
这里最重要的当然就是启动recovery服务了。
重启到recovery模式的流程图如下:
三、Recovery系统的框架结构
3.1 源码路径和主要原文件
在Android源码环境中,recovery的源码主要在bootable/recovery文件夹下,另外在device目录下,会根据各个设备定制自己的接口以及UI界面。
在bootable/recovery目录下,主要的源文件有:
LOCAL_SRC_FILES := \
adb_install.cpp \ //设置usb驱动,升级系统
asn1_decoder.cpp \ //解码asn1格式
device.cpp \ //recovery的头部显示和列表项,和通过make_device方法实现一个device设备
fuse_sdcard_provider.cpp \ //加载升级文件升级
recovery.cpp \ //会最先执行recovery.cpp中的main方法,及清除data等方法
roots.cpp \ //进行进行分区挂载操作
rotate_logs.cpp \ //mstar添加的文件
screen_ui.cpp \ //界面的绘制文件,初始化UI等
ui.cpp \ //初始化输入设备,如初始化按键,背光等
verifier.cpp \ //签名验证的功能实现方法
wear_ui.cpp \ // 继承于ScreenRecoveryUI的UI
wear_touch.cpp \ //界面的触摸事件响应
该部分代码在编译后,会统一输出到 out/recovery/root/目录;
3.2 Recovery模式的三个部分
Recovery的工作需要整个软件平台的配合,从通信架构上来看,主要有三个部分。
- MainSystem:即上面提到的正常启动模式(BCB中无命令情况),是引导boot.img启动的系统,Android的正常工作模式。更新时,在这种模式下的操作就是往 /cache/recovery/command 文件中写入ota升级命令及包存放路径。在重启进入Recovery模式之前,会向BCB中写入命令,以便在重启后告诉bootloader进入Recovery模式。
- Recovery:系统进入Recovery模式后会装载Recovery分区,该分区包含recovery.img。进入该模式后主要是运行Recovery服务(/sbin/recovery)来做相应的操作(重启、升级update.zip、擦除cache分区等)。
- Bootloader:除了正常的加载启动系统之外,还会通过读取MISC分区(BCB)获得来至Main system和Recovery的消息。
3.3 Recovery模式的两个通信接口
在Recovery服务中上述的三个实体之间的通信是必不可少的,他们相互之间又有以下两个通信接口。
3.3.1 通过CACHE分区中的三个文件通信
Recovery通过/cache/recovery/目录下的三个文件与mian system通信。具体如下
-
/cache/recovery/command:这个文件保存着Main system传给Recovery的命令行,每一行就是一条命令,下表给出一些常用的命令及其含义:
命令 取值 含义 send_intent 字符串 Recovery结束后将字符串写到这里,
然后写入/cache/recovery/intent,比如升级结果update_package 路径 安装OTA升级包的路径 wipe_data 无 擦除userdata以及cache,然后重启 wipe_cache 无 擦除cache,然后重启 set_encrypted_filesystem on off just_exit 无 退出和重启 -
/cache/recovery/last_log:Recovery模式在工作中的log打印。在recovery服务运行过程中,stdout以及stderr会重定位到/tmp/recovery.log在recovery退出之前会将其转存到/cache/recovery/last_log中,供查看。
-
/cache/recovery/intent:Recovery传递给Main system的信息。列如反馈升级是否成功。
3.3.2 通过BCB(Bootloader Control Block)通信
BCB是bootloader与Recovery的通信接口,也是Bootloader与Main system之间的通信接口。存储在flash中的MISC分区,占用三个page,其本身就是一个结构体,具体成员以及各成员含义如下:
struct bootloader_message{
char command[32];
char status[32];
char recovery[1024];
};
- command成员:其可能的取值我们在上文已经分析过了,即当我们想要在重启进入Recovery模式时,Main System会将boot-recovery命令写入。另外在退出Recovery时,会清除这个成员的值,防止重启时再次进入Recovery模式。
- status:在完成相应的更新后,Bootloader会将执行结果写入到这个字段。
- recovery:可被Main System写入,也可被Recovery服务程序写入。该文件的内容格式为:
“recovery\n
<recovery command>\n
<recovery command>”
该文件存储的就是一个字符串,必须以recovery\n开头,否则这个字段的所有内容域会被忽略。“recovery\n”
之后的部分,是/cache/recovery/command
支持的命令。可以将其理解为Recovery操作过程中对命令操作的备份。Recovery对其操作的过程为:先读取BCB的recovery字段然后读取/cache/recovery /command
,然后将二者重新写回BCB的recovery字段,这样在进入Main system之前,确保操作被执行。在操作之后进入Main system之前,Recovery又会清空BCB的command域和recovery域,这样确保重启后不再进入Recovery模式。
注意!这里比较容易弄混淆的点:
BCB中的command和/cache/recovery/command的内容不等价,且不同类型。
- BCB中的command:决定了bootloader该去引导启动android系统还是recovery系统
- BCB中的recovery:这个字段就是操作命令的备份,此处内容和/cache/recovery/command内容是等价的。
Recovery系统的三个部分和两个通信接口的示意图如下:
四、Recovery的主要源码分析
在进入文件系统后会执行bootable/recovery/etc/init.rc,在init.rc中下面代码可知,进入recovery模式后会执行sbin/recovery,此文件是bootable/recovery.cpp生成的(查看Android.mk可知),所以recovery.cpp是recovery模式的入口。
service recovery /sbin/recovery
seclabel u:r:recovery:s0
因为recovery.cpp的main函数太长了,这里分块分析recovery的主要源码,其实在main函数中主要做了下面几件事情:
- 设置adb进程。
- log重定向到recovery.log。
- 装载分区表,填充fstab结构体。
- 读取控制参数。
- 加载语言显示。
- 加载UI模型。
- 死循环prompt_and_wait,等待操作;
- 退出recovery模式
recovery.cpp的main方法执行的流程图大概如下:
4.1 设置adb进程
在recovery的main方法中首先判断命令行参数是否为–adbd,如果有则执行minadbd_main函数,这样是为了方便使用adb sideload命令,如果参数为-adbd的话,那么它会变成精简版adbd,只支持sideload命令。
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
minadbd_main();
return 0;
}
4.2 输出log重定向到recovery.log
重定向标准输出和标准出错log到/tmp/recovery.log这个文件里,这个文件是临时log文件,在recovery模式finish的时候会将这个文件里面的log保存到/cache/recovery/last_log中。为了方便调试,可以将临时log重定位到控制台输出,修改参数:static const char ``*TEMPORARY_LOG_FILE = ``"/dev/console"``;
redirect_stdio(TEMPORARY_LOG_FILE);
4.3 装载分区表
之后会调用roots.cpp文件中的load_volume_table()
方法来初始化并装载recovery的分区表到fstab结构体中,load_volume_table()
方法如下:
roots.cpp
void load_volume_table()
{
int i;
int ret;
//加载分区表到fstab,具体就是去加载/etc/recovery.fstab这个文件,是Vold进程中的函数
fstab = fs_mgr_read_fstab_default();
if (!fstab) {
LOG(ERROR) << "failed to read default fstab";
return;
}
//将对应的信息加入到一条链表中
ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk");
//如果load到的分区表为空,则做释放操作
if (ret < 0 ) {
LOG(ERROR) << "failed to add /tmp entry to fstab";
fs_mgr_free_fstab(fstab);
fstab = NULL;
return;
}
//打印分区表信息,这类信息在recovery启动的时候在log中可以看到,具体形式如下:
//编号| 挂载节点| 文件系统类型| 块设备| 长度
printf("recovery filesystem table\n");
printf("=========================\n");
for (i = 0; i < fstab->num_entries; ++i) {
Volume* v = &fstab->recs[i];
printf(" %d %s %s %s %lld\n", i, v->mount_point, v->fs_type,
v->blk_device, v->length);
}
printf("\n");
}
上面提到的Vold进程是在kernel初始化的时候启动的,所有的热插拔设备都是通过Vold 进程挂载的,Vold的入口是/system/vold/main.cpp文件的main函数,fs_mgr_read_fstab_default()
方法就是去解析/etc/recovery.fstab这个文件,上面具体log如下:
0 /vendor ext4 /dev/block/platform/mstar_mci.0/by-name/vendor 0
[ 12.959471] 1 /system ext4 /dev/block/platform/mstar_mci.0/by-name/system 0
[ 12.959476] 2 /system ext4 /dev/block/platform/mstar_mci.0/by-name/system 0
[ 12.959491] 3 /data ext4 /dev/block/platform/mstar_mci.0/by-name/userdata 0
[ 12.959496] 4 /cache ext4 /dev/block/platform/mstar_mci.0/by-name/cache 0
[ 12.959501] 5 /vendor ext4 /dev/block/platform/mstar_mci.0/by-name/vendor 0
[ 12.959506] 6 /tvservice ext4 /dev/block/platform/mstar_mci.0/by-name/tvservice 0
[ 12.959511] 7 /tvconfig ext4 /dev/block/platform/mstar_mci.0/by-name/tvconfig 0
[ 12.959517] 8 /tvdatabase ext4 /dev/block/platform/mstar_mci.0/by-name/tvdatabase 0
[ 12.959522] 9 /tvcustomer ext4 /dev/block/platform/mstar_mci.0/by-name/tvcustomer 0
[ 12.959527] 10 /tvcertificate ext4 /dev/block/platform/mstar_mci.0/by-name/tvcertificate 0
[ 12.959532] 11 /boot1 emmc /dev/block/mmcblk0boot0 0
[ 12.959537] 12 /boot2 emmc /dev/block/mmcblk0boot1 0
[ 12.959542] 13 /MBOOT emmc /dev/block/platform/mstar_mci.0/by-name/MBOOT 0
[ 12.959547] 14 /MPOOL emmc /dev/block/platform/mstar_mci.0/by-name/MPOOL 0
[ 12.959552] 15 /misc emmc /dev/block/platform/mstar_mci.0/by-name/misc 0
[ 12.959556] 16 /recovery emmc /dev/block/platform/mstar_mci.0/by-name/recovery 0
[ 12.959561] 17 /boot emmc /dev/block/platform/mstar_mci.0/by-name/boot 0
[ 12.959566] 18 /tee emmc /dev/block/platform/mstar_mci.0/by-name/tee 0
[ 12.959571] 19 /RTPM emmc /dev/block/platform/mstar_mci.0/by-name/RTPM 0
[ 12.959576] 20 /dtb emmc /dev/block/platform/mstar_mci.0/by-name/dtb 0
[ 12.959581] 21 /optee emmc /dev/block/platform/mstar_mci.0/by-name/optee 0
[ 12.959586] 22 /armfw emmc /dev/block/platform/mstar_mci.0/by-name/armfw 0
[ 12.959591] 23 auto auto /devices/platform/mstar_fcie* 0
[ 12.959595] 24 auto auto /devices/platform/mstar_sdio* 0
[ 12.959600] 25 auto auto /devices/Mstar-ehci* 0
[ 12.959605] 26 auto auto /devices/Mstar-xhci* 0
[ 12.959610] 27 /tmp ramdisk ramdisk 0
挂载完相应的分区以后,就需要获取命令参数,因为只有挂载了对应的分区,才能访问到记录操作命令的/cache/recovery/command这个文件及BCB块,如果分区都没找到,那么当然就找不到分区上的文件,挂载分区这个步骤是至关重要的。
//从上面建立的分区表信息中读取是否有cache分区,因为log等重要信息都存在cache分区里
has_cache = volume_for_path(CACHE_ROOT) != nullptr;
// MStar Android Patch Begin
if(has_cache){
//mstar添加的确定是否有cache分区的方法
ensure_path_mounted(CACHE_ROOT);
}
// MStar Android Patch End
4.4 读取控制参数
在main方法中通过get_args方法获取启动参数。
//从传入的参数或/cache/recovery/command文件中得到相应的命令
std::vector<std::string> args = get_args(argc, argv);
recovery和bootloader要通过/misc才能相互通信,对应的信息数据结构体为bootloader_message;get_args(argc,argv)方法如下:
struct bootloader_message{
char command[32];//bootloader 启动时读取改数据,决定是否进入recovery模式
char status[32];//由bootloader进行更新,标识升级的结果;
char recovery[768];//recovery要执行的命令,recovery从中读取信息;
char stage[32]; // 恢复字段,它仅用于存储恢复命令行
char reserved[1148]; // 保留字段
};
static std::vector<std::string> get_args(const int argc, char** const argv) {
CHECK_GT(argc, 0);
bootloader_message boot = {
};//参数结构体
std::string err;
if (!read_bootloader_message(&boot, &err)) {
// 从BCB中获取参数,这里有可能是为空的情况。
LOG(ERROR) << err;
// If fails, leave a zeroed bootloader_message.
boot = {
};
}
...
// 将启动recovery时的参数放入args,这里至少有一个/sbin/recovery元素
std::vector<std::string> args(argv, argv + argc);
// 去解析recovery字段的值,然后写入到args中
if (args.size() == 1) {
boot.recovery[sizeof(boot.recovery) - 1] = '\0'; // Ensure termination
std::string boot_recovery(boot.recovery);
std::vector<std::string> tokens = android::base::Split(boot_recovery, "\n");
if (!tokens.empty() && tokens[0] == "recovery") {
for (auto it = tokens.begin() + 1; it != tokens.end(); it++) {
// Skip empty and '\0'-filled tokens.
if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
}
LOG(INFO) << "Got " << args.size() << " arguments from boot message";
} else if (boot.recovery[0] != 0) {
LOG(ERROR) << "Bad boot message: \"" << boot_recovery << "\"";
}
}
// 如果上述情况为空,则从/cache/recovery/command获取参数,其中COMMAND_FILE=/cache/recovery/command
if (args.size() == 1 && has_cache) {
std::string content;
if (ensure_path_mounted(COMMAND_FILE) == 0 &&
android::base::ReadFileToString(COMMAND_FILE, &content)) {
std::vector<std::string> tokens = android::base::Split(content, "\n");
// All the arguments in COMMAND_FILE are needed (unlike the BCB message,
// COMMAND_FILE doesn't use filename as the first argument).
for (auto it = tokens.begin(); it != tokens.end(); it++) {
// Skip empty and '\0'-filled tokens.
if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
}
LOG(INFO) << "Got " << args.size() << " arguments from " << COMMAND_FILE;
}
}
//将启动参数写入到BCB块的recovery字段中
std::vector<std::string> options(args.cbegin() + 1, args.cend());
if (!update_bootloader_message(options, &err)) {
LOG(ERROR) << "Failed to set BCB message: " << err;
}
return args;
}
get_args()函数的主要作用是建立recovery的启动参数,如果系统启动recovery时已经传递了启动参数,那么这个函数只是把启动参数的内容复制到函数的参数boot对象中,否则函数会首先从/misc分区中获取命令字符串来构建启动参数。如果/misc分区下没有内容,则尝试打开/cache/recovery/command文件并读取文件的内容来建立启动参数。从这个函数我们可以看到,更新系统最简单的方式是把更新命令写到/cache/recovery/command文件中。
get_args()函数的结尾调用了update_bootloader_message()函数,函数的作用是把启动参数的信息又保存到了/misc分区的BCB的recovery字段,以及给command字段添加boot-recovery命令。这样做的目的是防止升级过程中发生崩溃,这样重启后仍然可以从/misc分区中读取更新的命令,继续进行更新操作。这也是为什么get_args()函数要从几个地方读取启动参数的原因。
之后通过while循环解析获取到的参数,并把对应的功能设置为true或者给相应的变量赋值获取到对应的命令,后面会根据变量来执行对应的操作。
while ((arg = getopt_long(args_to_parse.size(), args_to_parse.data(), "", OPTIONS,
&option_index)) != -1) {
switch (arg) {
case 'n': android::base::ParseInt(optarg, &retry_count, 0); break;
case 'u': update_package = optarg; break;
case 'w': should_wipe_data = true; break;
case 'c': should_wipe_cache = true; break;
case 't': show_text = true; break;
case 's': sideload = true; break;
case 'a': sideload = true; sideload_auto_reboot = true; break;
case 'x': just_exit = true; break;
case 'l': locale = optarg; break;
case 'p': shutdown_after = true; break;
case 'r': reason = optarg; break;
case 'e': security_update = true; break;
case 0: {
std::string option = OPTIONS[option_index].name;
if (option == "wipe_ab") {
should_wipe_ab = true;
} else if (option == "wipe_package_size") {
android::base::ParseUint(optarg, &wipe_package_size);
} else if (option == "prompt_and_wipe_data") {
should_prompt_and_wipe_data = true;
}
break;
}
// MStar Android Patch Begin
case 'd': dev_uuid = optarg; break;
case 'b': dev_label= optarg; break;
// MStar Android Patch Begin
case '?':
LOG(ERROR) << "Invalid command argument";
continue;
}
}
4.5 加载显示语言
这个方法就是去判断/cache/recovery/last_locale文件是否存在,如果存在就读取里面的值,获取到的内容关系到显示那个国家的语言,如果没有获取到locale就使用默认的语言,848中的默认语言是英语。
if (locale.empty()) {
if (has_cache) {
locale = load_locale_from_cache();
}
if (locale.empty()) {
locale = DEFAULT_LOCALE;
}
}
4.6 加载UI模式
加载UI界面的流程大概有下面几步:
- 新建一个Device类的对象;
- 调用Device类的GetUI()返回一个RecoveryUI对象,这里应该返回的是ScreenRecoveryUI,ScreenRecoveryUI继承于RecoveryUI;
- 调用Init()初始化UI;
- 调用RecoveryUi的init方法去设置国家语言,然后初始化输入设备,并创建一个线程用于监听输入事件;
- 调用minui库的gr_init方法初始化图形显示,主要是打开设备、分配内存、初始化一些参数;
- 通过LoadBitmap()加载png图片生成surface对象
- 创建一个子线程更新progress进度条;
- 调用SetBackground方法设置背景图片;
Recovery中显示UI界面的framebuffer使用的是minui库,该库在网上也能查到相应的方法说明,下面会有详细介绍。
4.6.1 在main方法中
这里主要做了这几件事情:
- 新建一个device设备;
- 获取到UI;
- 调用UI的Init方法进行初始化;
- 设置背景;
Device* device = make_device();//新建一个Device设备
if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
//如果是静态UI模式则进入这里
printf("Quiescent recovery mode.\n");
ui = new StubRecoveryUI();
} else {
ui = device