前言
什么是Android的Recovery?
Recovery是Android手机备份功能,指的是一种可以对安卓手机内部的数据或系统进行修改的模式(类似于windows PE或DOS)。在这个模式下可以,对已有的系统进行备份或升级,也可以在此恢复出厂设置。
Android 系统的启动模式
1、一般启动模式(normal mode)
进入方法:按power启动手机 描述:常规启动手机的方法
2、安全模式(safe mode)
进入方法:长按menu+power键 描述:与Windows安全模式类似,android的安全模式是系统屏蔽掉了所有非系统自带程序,仅启动原系统自带程序
3、引导模式(BootLoader mode)
进入方法:长按camera键+power键 描述:配合fastboot工具对手机进行rom升级
4、恢复模式(recovery mode)
进入方法:长按HOME键+power键 描述:能够打开命令解释程序(shell)、刷新映像文件(flash image)、擦除data分区、擦除cache分区、恢复出厂设置、OTA升级等
android系统不同的启动模式的进入是在不同的情形下触发,除了组合按键能进入之外还有其他方式,比如系统崩溃(persist属性软件多次异常),或者命令行输入指令都能进入。
一、启动流程
下图是系统进入recovery的流程图:
如上图,所描绘的分别是三条进入recovery模式的路径:
读取寄存器中的recovery标志 组合按键 对misc分区和cache分区写入指令
1、读取寄存器中的recovery标志
此情形有两种方式触发:adb reboot recovery 和Powermanager.reboot(…,“recovery”,…)
先看通过Powermanager方式进入recovery,powermanager 调用reboot函数,最终往寄存器中写入了一个recovery标志,在机器重启时在BootLoader中读取该标志,然后进入到recovery模式,与adb reboot recovery 方式类似,两者都是通过修改sys.powerctl的值来达到目的
2、组合按键
组合按键的方式比较简单,在机器重启进入BootLoader时,会先检查寄存器中的recovery标志,如果没有标志则会检测组合按键是否按下,如果按下则选择进入recovery模式。
3、MISC和CACHE分区指令进入
此方法一般root模式可手动修改CACHE分区,还有手机设置中的恢复出厂设置是通过这种方式进入,后面详细说明
二、Recovery模式介绍
若启动过程中用户没有按下不论什么组合键。bootloader会读取位于MISC分区的启动控制信息块BCB(Bootloader Control Block)。它是一个结构体。存放着启动命令command。依据不同的命令。系统又能够进入三种不同的启动模式:MainSystem、Recovery、Bootloader。
Recovery的工作须要整个软件平台的配合,从通信架构上来看。主要有三个部分。
MainSystem
:即上面提到的正常启动模式(BCB中无命令),是用boot.img启动的系统,Android的正常工作模式。更新时。在这样的模式中我们的上层操作就是使用OTA或则从SD卡中升级update.zip包。在重新启动进入Recovery模式之前,会向BCB中写入命令,以便在重新启动后告诉bootloader进入Recovery模式。
Recovery
:系统进入Recovery模式后会装载Recovery分区。该分区包括recovery.img(同boot.img同样。包括了标准的内核和根文件系统)。进入该模式后主要是执行Recovery服务(/sbin/recovery)来做对应的操作(重新启动、升级update.zip、擦除cache分区等)。
Bootloader
:除了正常的载入启动系统之外。还会通过读取MISC分区(BCB)获得来至Main system和Recovery的消息。
三个部分相互之间如何通信?
1、主系统和Recovery之间的通过/cache/recovery通信
Recovery通过/cache/recovery/文件夹下的三个文件与main system通信。
/cache/recovery文件夹下有两个功能文件:
/cache/recovery/command 用于传递主系统的指令 /cache/recovery/log 保存recovery模式运行时产生的log
/cache/recovery/command支持的指令有(一行一个指令’\n’结尾):
--update_package=path 验证安装OTA包,OTA升级使用 --wipe_data 清楚用户数据(cache分区),然后重启 --prompt_and_wipe_data 提示用户数据已损坏,征得用户同意后擦除用户数据(和缓存),然后重新启动 --wipe_cache 清楚cache分区(但是不包含用户数据),然后重启 --set_encrypted_filesystem=on|off 启用/禁用加密的文件系统 --just_exit 不做操作,直接退出,重启
2、BootLoader与recovery通过BCB(BootLoader Control Block)通信
BCB不仅是BootLoader与recovery之间的通信桥梁,更是BootLoader与main system之间的通信桥梁,从而变成了recovery与main system之间的通信方式,目前最新android系统中对于/cache/recovery已经用的很少,主要是使用BCB实现recovery与main system之间的信息交换。存储在flash中的MISC分区。其本身就是一个结构体struct bootloader_message,MISC分区使用空间分配如下:
0k - 2k 用于bootloader_message 2k - 16k 用于BootLoader(2K-4K范围可以选择性地用作bootloader_message_ab结构体) 16K - 64K 由uncrypt和recovery用于存储A/B设备的wipe_package
bootloader_message结构定义如下:
struct bootloader_message {
char command[32];
char status[32];
char recovery[768];
// The 'recovery' field used to be 1024 bytes. It has only ever
// been used to store the recovery command line, so 768 bytes
// should be plenty. We carve off the last 256 bytes to store the
// stage string (for multistage packages) and possible future
// expansion.
char stage[32];
// The 'reserved' field used to be 224 bytes when it was initially
// carved off from the 1024-byte recovery field. Bump it up to
// 1184-byte so that the entire bootloader_message struct rounds up
// to 2048-byte.
char reserved[1184];
};
command
:当要重新启动进入Recovery模式或更新radio、bootloader固件时。linux会更新这个值。当固件更新完毕后Bootloader也会更新这个值。另外在成功更新后结束Recovery时。会清除这个成员的值,防止重新启动时再次进入Recovery模式。
status
:在完毕对应的更新后。Bootloader会将运行结果写入到这个字段。
recovery
:可被Main System写入,也可被Recovery服务程序写入。该文件的内容格式为:
“recovery\n
<recovery command>\n
<recovery command>”
该文件存储的就是一个字符串,必须以recovery\n开头,否则这个字段的全部内容域会被忽略。“recovery\n”之后的部分,是/cache/recovery/command支持的命令。
三、Recovery 流程
recovery模式相对于main system来说其实就是一个微型系统,没有主系统那么复杂,只负责简单的功能和绘制简单的界面,并且代码逻辑也相对简单,容易理解。主要逻辑放在recovery.cpp中实现,我们来看下它主要做了什么:
int main(int argc, char **argv) {
// We don't have logcat yet under recovery; so we'll print error on screen and
// log to stdout (which is redirected to recovery.log) as we used to do.
android::base::InitLogging(argv, &UiLogger); // 初始化log
// Take last pmsg contents and rewrite it to the current pmsg session.
static const char filter[] = "recovery/";
// Do we need to rotate?
bool doRotate = false;
__android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logbasename, &doRotate);
// Take action to refresh pmsg contents
__android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logrotate, &doRotate);
// If this binary is started with the single argument "--adbd",
// instead of being the normal recovery binary, it turns into kind
// of a stripped-down version of adbd that only supports the
// 'sideload' command. Note this must be a real argument, not
// anything in the command file or bootloader control block; the
// only way recovery should be run with this argument is when it
// starts a copy of itself from the apply_from_adb() function.
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
minadbd_main();
return 0;
}
time_t start = time(nullptr);
// redirect_stdio should be called only in non-sideload mode. Otherwise
// we may have two logger instances with different timestamps.
redirect_stdio(TEMPORARY_LOG_FILE); //重定向log到/tmp中
printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));
load_volume_table();
has_cache = volume_for_mount_point(CACHE_ROOT) != nullptr; //检查cache是否挂载
has_nvdata = volume_for_mount_point(NVDATA_ROOT) != nullptr; // 检查NVDATA是否挂载
mt_init_partition_type();
std::vector<std::string> args = get_args(argc, argv); // 获取参数:首先从BCB读取指令,如果BCB没有,则从/cache/recovery/command读取,然后将指令更新到BCB中
std::vector<char*> args_to_parse(args.size());
std::transform(args.cbegin(), args.cend(), args_to_parse.begin(),
[](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
const char* update_package = nullptr;
bool should_wipe_data = false;
bool should_prompt_and_wipe_data = false;
bool should_wipe_cache = false;
bool should_wipe_ab = false;
size_t wipe_package_size = 0;
bool show_text = false;
bool sideload = false;
bool sideload_auto_reboot = false;
bool just_exit = false;
bool shutdown_after = false;
int retry_count = 0;
bool security_update = false;
int arg;
int option_index;
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;
}
case '?':
LOG(ERROR) << "Invalid command argument";
continue;
}
}
if (locale.empty()) {
if (has_cache) {
locale = load_locale_from_cache();
}
if (locale.empty()) {
locale = DEFAULT_LOCALE;
}
}
printf("locale is [%s]\n", locale.c_str());
printf("stage is [%s]\n", stage.c_str());
printf("reason is [%s]\n", reason);
Device* device = make_device();
if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
printf("Quiescent recovery mode.\n");
ui = new StubRecoveryUI();
} else {
ui = device->GetUI(); //初始化UI界面
if (!ui->Init(locale)) {
printf("Failed to initialize UI, use stub UI instead.\n");
ui = new StubRecoveryUI();
}
}
// Set background string to "installing security update" for security update,
// otherwise set it to "installing system update".
ui->SetSystemUpdateText(security_update);
int st_cur, st_max;
if (!stage.empty() && sscanf(stage.c_str(), "%d/%d", &st_cur, &st_max) == 2) {
ui->SetStage(st_cur, st_max);
}
ui->SetBackground(RecoveryUI::NONE);
if (show_text) ui->ShowText(true);
sehandle = selinux_android_file_context_handle();
selinux_android_set_sehandle(sehandle);
if (!sehandle) {
ui->Print("Warning: No file_contexts\n");
}
device->StartRecovery();
printf("Command:");
for (const auto& arg : args) {
printf(" \"%s\"", arg.c_str());
}
printf("\n\n");
property_list(print_property, nullptr);
printf("\n");
ui->Print("Supported API: %d\n", kRecoveryApiVersion);
int status = INSTALL_SUCCESS;
if (update_package != nullptr) { //如果有更新OTA则开始更新OTA
// It's not entirely true that we will modify the flash. But we want
// to log the update attempt since update_package is non-NULL.
modified_flash = true;
if (!is_battery_ok()) {
ui->Print("battery capacity is not enough for installing package, needed is %d%%\n",
BATTERY_OK_PERCENTAGE);
// Log the error code to last_install when installation skips due to
// low battery.
log_failure_code(kLowBattery, update_package);
status = INSTALL_SKIPPED;
} else if (bootreason_in_blacklist()) {
// Skip update-on-reboot when bootreason is kernel_panic or similar
ui->Print("bootreason is in the blacklist; skip OTA installation\n");
log_failure_code(kBootreasonInBlacklist, update_package);
status = INSTALL_SKIPPED;
} else {
// It's a fresh update. Initialize the retry_count in the BCB to 1; therefore we can later
// identify the interrupted update due to unexpected reboots.
if (retry_count == 0) {
set_retry_bootloader_message(retry_count + 1, args); //设置重试次数,一般用于防止升级途中突然断电,重启后继续升级
}
status = install_package(update_package, &should_wipe_cache, TEMPORARY_INSTALL_FILE, true, // 安装升级包,核心函数,升级成功会把结果写入BCB通知main system
retry_count);
if (status == INSTALL_SUCCESS && should_wipe_cache) {
wipe_cache(false, device);
}
if (status != INSTALL_SUCCESS) {
ui->Print("Installation aborted.\n");
// When I/O error happens, reboot and retry installation RETRY_LIMIT
// times before we abandon this OTA update.
if (status == INSTALL_RETRY && retry_count < RETRY_LIMIT) {
copy_logs();
retry_count += 1;
set_retry_bootloader_message(retry_count, args); // 升级失败增加重试次数
// Print retry count on screen.
ui->Print("Retry attempt %d\n", retry_count);
// Reboot and retry the update
if (!reboot("reboot,recovery")) { // 如果升级失败会重启系统重新进入recovery
ui->Print("Reboot failed\n");
} else {
while (true) {
pause();
}
}
}
// If this is an eng or userdebug build, then automatically
// turn the text display on if the script fails so the error
// message is visible.
if (is_ro_debuggable()) {
ui->ShowText(true);
}
}
}
} else if (should_wipe_data) {
if (!wipe_data(device)) {
status = INSTALL_ERROR;
}
} else if (should_prompt_and_wipe_data) {
ui->ShowText(true);
ui->SetBackground(RecoveryUI::ERROR);
if (!prompt_and_wipe_data(device)) {
status = INSTALL_ERROR;
}
ui->ShowText(false);
} else if (should_wipe_cache) {
if (!wipe_cache(false, device)) {
status = INSTALL_ERROR;
}
} else if (should_wipe_ab) {
if (!wipe_ab_device(wipe_package_size)) {
status = INSTALL_ERROR;
}
} else if (sideload) {
// 'adb reboot sideload' acts the same as user presses key combinations
// to enter the sideload mode. When 'sideload-auto-reboot' is used, text
// display will NOT be turned on by default. And it will reboot after
// sideload finishes even if there are errors. Unless one turns on the
// text display during the installation. This is to enable automated
// testing.
if (!sideload_auto_reboot) {
ui->ShowText(true);
}
status = apply_from_adb(&should_wipe_cache, TEMPORARY_INSTALL_FILE);
if (status == INSTALL_SUCCESS && should_wipe_cache) {
if (!wipe_cache(false, device)) {
status = INSTALL_ERROR;
}
}
ui->Print("\nInstall from ADB complete (status: %d).\n", status);
if (sideload_auto_reboot) {
ui->Print("Rebooting automatically.\n");
}
} else if (!just_exit) {
// If this is an eng or userdebug build, automatically turn on the text display if no command
// is specified. Note that this should be called before setting the background to avoid
// flickering the background image.
if (is_ro_debuggable()) {
ui->ShowText(true);
}
status = INSTALL_NONE; // No command specified
ui->SetBackground(RecoveryUI::NO_COMMAND);
}
mt_main_write_result(status, update_package); // 写入升级结果
if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
ui->SetBackground(RecoveryUI::ERROR);
if (!ui->IsTextVisible()) {
sleep(5);
}
}
Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
// 1. If the recovery menu is visible, prompt and wait for commands.
// 2. If the state is INSTALL_NONE, wait for commands. (i.e. In user build, manually reboot into
// recovery to sideload a package.)
// 3. sideload_auto_reboot is an option only available in user-debug build, reboot the device
// without waiting.
// 4. In all other cases, reboot the device. Therefore, normal users will observe the device
// reboot after it shows the "error" screen for 5s.
if ((status == INSTALL_NONE && !sideload_auto_reboot) || ui->IsTextVisible()) {
Device::BuiltinAction temp = prompt_and_wait(device, status); // 等待用户选择界面上的某些选项
if (temp != Device::NO_ACTION) {
after = temp;
}
}
// Save logs and clean up before rebooting or shutting down.
finish_recovery(); // 结束recovery模式,并且清楚recovery标志,包括/cache分区及BCB
switch (after) {
case Device::SHUTDOWN:
ui->Print("Shutting down...\n");
android::base::SetProperty(ANDROID_RB_PROPERTY, "shutdown,");
break;
case Device::REBOOT_BOOTLOADER:
ui->Print("Rebooting to bootloader...\n");
android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,bootloader");
break;
default:
ui->Print("Rebooting...\n");
reboot("reboot,");
break;
}
while (true) {
pause();
}
// Should be unreachable.
return EXIT_SUCCESS;
}
大体代码流程图如下:
恢复出厂设置流程
:
-
选择“设置”->“系统”->”系统还原”。
-
Main system向BCB写入”–wipe_data”;
-
Main system 重新启动reboot(‘recovery’),进入Recovery模式;
-
get_args() 函数向 BCB写入”boot-recovery”和”–wipe_data”,然后开始擦除。
– after this, rebooting will restart the erase –
-
erase_volume() 又一次格式化/data分区
-
erase_volume() 又一次格式化/cache分区
-
finish_recovery() 清除BCB。然后call reboot()进入Main system。
OTA升级流程
: -
主系统下载更新包到 /cache/some-filename.zip
-
主系统向BCB写入 “–update_package=/cache/some-filename.zip”
-
主系统重启进入recovery
-
get_args() 向BCB写入 “boot-recovery” and “–update_package=…”
– 如此再下次异常重启后会重新进入recovery安装更新 – -
install_package() 安装更新
-
finish_recovery() 完成安装,完成recovery,擦除BCB
– 如此之后系统重启将进入主系统 – -
如果安装失败
7a. prompt_and_wait() 显示错误,并等待用户操作
7b. 用户重启进入主系统
异常流程
:
1、应用发生异常,触发android自救机制 (请参考android的自救–RescueParty),RescueParty调用RecoverySystem重启
2、RecoverySystem向BCB写入“–prompt_and_wipe_data”,
3、主系统重启进入recovery
4、get_args() 在BCB中读到“–prompt_and_wipe_data”,并向BCB写入 “boot-recovery”, “–prompt_and_wipe_data”
5、显示recovery异常页面(用户数据已损坏),等待用户重启或者恢复出厂设置
6、finish_recovery() 清楚recovery标志
7、重启进入主系统
8、用户选择恢复出厂设置
8a、擦除data分区
8b、重启进入主系统