Recovery 模式

  • Post author:
  • Post category:其他




前言


什么是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;
}

大体代码流程图如下:

在这里插入图片描述


恢复出厂设置流程

  1. 选择“设置”->“系统”->”系统还原”。

  2. Main system向BCB写入”–wipe_data”;

  3. Main system 重新启动reboot(‘recovery’),进入Recovery模式;

  4. get_args() 函数向 BCB写入”boot-recovery”和”–wipe_data”,然后开始擦除。

    – after this, rebooting will restart the erase –

  5. erase_volume() 又一次格式化/data分区

  6. erase_volume() 又一次格式化/cache分区

  7. finish_recovery() 清除BCB。然后call reboot()进入Main system。


    OTA升级流程

  8. 主系统下载更新包到 /cache/some-filename.zip

  9. 主系统向BCB写入 “–update_package=/cache/some-filename.zip”

  10. 主系统重启进入recovery

  11. get_args() 向BCB写入 “boot-recovery” and “–update_package=…”

    – 如此再下次异常重启后会重新进入recovery安装更新 –

  12. install_package() 安装更新

  13. finish_recovery() 完成安装,完成recovery,擦除BCB

    – 如此之后系统重启将进入主系统 –

  14. 如果安装失败

    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、重启进入主系统



版权声明:本文为wh2526422原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。