该系列文章总纲链接:专题分纲目录 Android系统升级 Recovery模式
导图是不断迭代的,这里主要关注➕ recovery模式启动部分即可,主要从 更新包简介,mian函数解析开始解读,分析了main函数中关键的方法 获取参数getargs和执行菜单命令prompt_and_wait。
对于recovery模式,一般均采用第三方的方案,比如:在使用MTK平台时,我们这边一般会直接采用广升FOTA的服务商直接进行升级相关的工作。对于第三方的recovery代码,实际上也是参考android原生代码进行改动,核心原理不变,因此这里对recovery模式的代码研究还是采用 google 的原生recovery代码。
android 启动时会通过组合键 判定是否进入recovery模式,也可以通过android的RecoverySystem来进入。recovery模式下还是会启动bootloader、kernel,最后会通过bootargs来判定,如果进入recovery模式则加载recovery专属的rc文件,进而进入到recovery模式。
recovery模式主要是使用升级包进行升级,升级包解压后一般是这样
包含的文件简要说明:
最后META-INF的几个文件解读如下所示:
接下来我们从main函数启动开始分析,到如何传递参数,以及如何执行菜单命令 角度来逐步分析。
1 main函数启动分析
int main(int argc, char **argv) {
time_t start = time(NULL);
redirect_stdio(TEMPORARY_LOG_FILE);
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
adb_main();//如果参数中有adb,作为adbd的daemon启动
return 0;
}
//读取/etc/recovery.fstab文件,保存了recovery模式下分区情况(名称+参数)
load_volume_table();
ensure_path_mounted(LAST_LOG_FILE);
rotate_last_logs(KEEP_LOG_COUNT);
//获得启动参数(按照优先级,分别是recovery->misc->/cache/recovery/command中命令)
get_args(&argc, &argv);
const char *send_intent = NULL;
const char *update_package = NULL;
int wipe_data = 0, wipe_cache = 0, show_text = 0;
bool just_exit = false;
bool shutdown_after = false;
int arg;
//解析启动参数
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
switch (arg) {
case 's': send_intent = optarg; break;
case 'u': update_package = optarg; break; //升级系统
case 'w': wipe_data = wipe_cache = 1; break; //擦出数据
case 'c': wipe_cache = 1; break; //擦除cache
case 't': show_text = 1; break;//指示升级时是否显示UI
case 'x': just_exit = true; break; //退出
case 'l': locale = optarg; break; //指定locale
//...
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
//...
//Device很多函数是空函数,设计上留给厂商实现。
Device* device = make_device();
ui = device->GetUI();
gCurrentUI = ui;
ui->SetLocale(locale);
ui->Init();
int st_cur, st_max;
if (stage != NULL && sscanf(stage, "%d/%d", &st_cur, &st_max) == 2) {
ui->SetStage(st_cur, st_max);
}
ui->SetBackground(RecoveryUI::NONE);
if (show_text) ui->ShowText(true);
//SElinux相关
device->StartRecovery();//空函数
printf("Command:");
for (arg = 0; arg < argc; arg++) {
printf(" \"%s\"", argv[arg]);
}
printf("\n");
if (update_package) {//预处理更新命令
if (strncmp(update_package, "CACHE:", 6) == 0) {
int len = strlen(update_package) + 10;
char* modified_path = (char*)malloc(len);
strlcpy(modified_path, "/cache/", len);
strlcat(modified_path, update_package+6, len);
printf("(replacing path \"%s\" with \"%s\")\n",
update_package, modified_path);
update_package = modified_path;
}
}
printf("\n");
property_list(print_property, NULL);
property_get("ro.build.display.id", recovery_version, "");
printf("\n");
int status = INSTALL_SUCCESS;
if (update_package != NULL) {
//如果更新系统
status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);
if (status == INSTALL_SUCCESS && wipe_cache) {
//更新成功,启动并擦出cache
if (erase_volume("/cache")) {
LOGE("Cache wipe (requested by package) failed.");
}
}
if (status != INSTALL_SUCCESS) {
ui->Print("Installation aborted.\n");
char buffer[PROPERTY_VALUE_MAX+1];
property_get("ro.build.fingerprint", buffer, "");
if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) {
ui->ShowText(true);//屏幕上打印失败版本信息
}
}
} else if (wipe_data) {//擦除数据
if (device->WipeData()) status = INSTALL_ERROR;
if (erase_volume("/data")) status = INSTALL_ERROR;
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (erase_persistent_partition() == -1 ) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
} else if (wipe_cache) {//擦除cache
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n");
} else if (!just_exit) {//只是退出
status = INSTALL_NONE; // No command specified
ui->SetBackground(RecoveryUI::NO_COMMAND);
}
if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
copy_logs();
//执行命令错误,屏幕上显示标志
ui->SetBackground(RecoveryUI::ERROR);
}
Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
if (status != INSTALL_SUCCESS || 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(send_intent);
//根据after参数设置,关闭/重启系统
switch (after) {
case Device::SHUTDOWN:
ui->Print("Shutting down...\n");
property_set(ANDROID_RB_PROPERTY, "shutdown,");
break;
case Device::REBOOT_BOOTLOADER:
ui->Print("Rebooting to bootloader...\n");
property_set(ANDROID_RB_PROPERTY, "reboot,bootloader");
break;
default:
ui->Print("Rebooting...\n");
property_set(ANDROID_RB_PROPERTY, "reboot,");
break;
}
sleep(5); // should reboot before this finishes
return EXIT_SUCCESS;
}
main函数并不复杂,主要实现了几个关键功能:
2 如何传递参数
2.1 关键结构体bootloader_message
Bootloader和reovery模块以及主系统分区通信主要是通过misc来完成的,misc的数据结构是bootloader_message,定义如下:
struct bootloader_message {
char command[32];//bootloader 启动时读取改数据,决定是否进入recovery模式
char status[32]; //由recovery或者bootloader进行更新,标识升级的结果;
/* 由Android系统进行写入,recovery从中读取信息,以recovrery开头,
后面是recovery中执行的命令,命令以\n分割 */
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];
char reserved[224];
};
command命令详细说明:
recovery命令详细说明:存放的是recovery模块启动时的参数。
2.2 getargs获取参数(misc分区相关)
代码如下:
static void get_args(int *argc, char ***argv) {
struct bootloader_message boot;
memset(&boot, 0, sizeof(boot));
//读取misc分区命令到boot变量中
get_bootloader_message(&boot); // this may fail, leaving a zeroed structure
stage = strndup(boot.stage, sizeof(boot.stage));
//...
if (*argc <= 1) {//如果命令没有传递参数
boot.recovery[sizeof(boot.recovery) - 1] = '\0'; // Ensure termination
const char *arg = strtok(boot.recovery, "\n");
//从misc分区中读取命令建立启动参数,这样argc和argv就会有新的值。
if (arg != NULL && !strcmp(arg, "recovery")) {
*argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
(*argv)[0] = strdup(arg);
for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
if ((arg = strtok(NULL, "\n")) == NULL) break;
(*argv)[*argc] = strdup(arg);
}
} else if (boot.recovery[0] != 0 && boot.recovery[0] != 255) {
}
}
if (*argc <= 1) {//如果从misc分区中没有读到命令
//COMMAND_FILE 为/cache/recovery/command
FILE *fp = fopen_path(COMMAND_FILE, "r");
if (fp != NULL) {
char *token;
char *argv0 = (*argv)[0];
*argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
(*argv)[0] = argv0; // use the same program name
char buf[MAX_ARG_LENGTH];
//使用读取的文件内容建立启动参数
for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
if (!fgets(buf, sizeof(buf), fp)) break;
token = strtok(buf, "\r\n");
if (token != NULL) {
(*argv)[*argc] = strdup(token); // Strip newline.
} else {
--*argc;
}
}
check_and_fclose(fp, COMMAND_FILE);
}
}
//把启动参数放到boot对象中
strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
int i;
for (i = 1; i < *argc; ++i) {
strlcat(boot.recovery, (*argv)[i], sizeof(boot.recovery));
strlcat(boot.recovery, "\n", sizeof(boot.recovery));
}
set_bootloader_message(&boot);
}
这里继续分析上面提到的关键点
@1 get_bootloader_message的实现如下:
int get_bootloader_message(struct bootloader_message *out) {
Volume* v = volume_for_path("/misc");//打开misc分区
//...
if (strcmp(v->fs_type, "mtd") == 0) {//mtd格式
return get_bootloader_message_mtd(out, v);
} else if (strcmp(v->fs_type, "emmc") == 0) {//emmc格式
return get_bootloader_message_block(out, v);
}
LOGE("unknown misc partition fs_type \"%s\"\n", v->fs_type);
return -1;
}
该方法主要是从misc分区读取数据。
@2 set_bootloader_message的实现如下:
int set_bootloader_message(const struct bootloader_message *in) {
Volume* v = volume_for_path("/misc");
//...
if (strcmp(v->fs_type, "mtd") == 0) {
return set_bootloader_message_mtd(in, v);
} else if (strcmp(v->fs_type, "emmc") == 0) {
return set_bootloader_message_block(in, v);
}
LOGE("unknown misc partition fs_type \"%s\"\n", v->fs_type);
return -1;
}
这里把参数回写到misc分区,这样做是为了防止升级过程中发生崩溃,重启仍然可以从misc分区读取到更新的命令,继续进行更新操作,这也是为什么getargs要从几个地方读取启动参数的原因。
@3 finish_recovery函数
如果recovery模式下正常退出,则会清理掉misc分区中的内容,关键代码内容如下:
static void finish_recovery(const char *send_intent) {
//...
// Reset to normal system boot so recovery won't cycle indefinitely.
struct bootloader_message boot;
memset(&boot, 0, sizeof(boot));
set_bootloader_message(&boot);
//...
sync(); // For good measure.
}
这里就是向misc分区中写入0。
3 执行菜单命令
执行菜单关键的函数是prompt_and_wait,这里打印屏幕菜单并接收用户输入,函数代码如下:
static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
const char* const* headers = prepend_title(device->GetMenuHeaders());
for (;;) {
finish_recovery(NULL);
//根据命令执行,修改UI背景
switch (status) {
case INSTALL_SUCCESS:
case INSTALL_NONE:
ui->SetBackground(RecoveryUI::NO_COMMAND);
break;
case INSTALL_ERROR:
case INSTALL_CORRUPT:
ui->SetBackground(RecoveryUI::ERROR);
break;
}
ui->SetProgressType(RecoveryUI::EMPTY);
//等待用户输入
int chosen_item = get_menu_selection(headers, device->GetMenuItems(), 0, 0, device);
//用户选择权 交给device对象处理
Device::BuiltinAction chosen_action = device->InvokeMenuItem(chosen_item);
int wipe_cache = 0;
//处理菜单命令
switch (chosen_action) {
case Device::NO_ACTION:
break;
case Device::REBOOT:
case Device::SHUTDOWN:
case Device::REBOOT_BOOTLOADER:
return chosen_action;
case Device::WIPE_DATA:
wipe_data(ui->IsTextVisible(), device);
if (!ui->IsTextVisible()) return Device::NO_ACTION;
break;
case Device::WIPE_CACHE:
ui->Print("\n-- Wiping cache...\n");
erase_volume("/cache");
ui->Print("Cache wipe complete.\n");
if (!ui->IsTextVisible()) return Device::NO_ACTION;
break;
case Device::APPLY_EXT: {//sdcard卡上更新
ensure_path_mounted(SDCARD_ROOT);
char* path = browse_directory(SDCARD_ROOT, device);
if (path == NULL) {
ui->Print("\n-- No package file selected.\n", path);
break;
}
//...
break;
}
//...
case Device::APPLY_ADB_SIDELOAD://启动adbd,也是卡刷入口
status = apply_from_adb(ui, &wipe_cache, TEMPORARY_INSTALL_FILE);
//...
break;
}
}
}
整个函数的逻辑是:在屏幕上打印菜单,之后等待用户输入(用户只能通过音量+ 音量-来上下选择,power键确认),用户输入后,根据用户输入的命令,做不同的处理,命令与处理的对应关系如下:
命令 | 处理方式 |
REBOOT | 重启 |
WIPE_DATA | 擦除data分区,这就是恢复出厂设置所进行的操作,清除手机上所有的用户数据,包括cache分区 |
WIPE_CACHE | 仅擦出cache分区下的内容 |
APPLY_EXT | 通过UI在sdcard上选择一个文件进行更新操作 |
APPLY_CACHE | 同上,只是路径变成了cache |
APPLY_ADB_SIDELOAD | 启动adbd(注意:这里adbd只是一个mini版本),让用户通过adb连接来执行sideload命令上传,更新文件到/tmp/update.zip,然后再执行更新操作。 |