⑧ 使用 fs_config(build/tools/fs_config)配置上面的 zip 包内所有系统文件(system/ 下各目录、文件)的权限、属主等信息。fs_config 包含了一个头文件 #include "private/android_filesystem_config.h",在这个头文件中以硬编码的方式设定了 system 目录下各文件的权限、属主。执行完配置后会将配置后的信息以文本方式输出到 META/filesystem_config.txt 中,并再一次 zip 压缩成我们最终需要的原始包。
第二步:上面的 zip 包只是一个编译过程中生成的原始包。
这个原始 zip 包在实际的编译过程中有两个作用:一是用来生成 OTA update 升级包,二是用来生成系统镜像。在编译过程中若生成 OTA update 升级包,会调用(具体位置在 Makefile 的 1037 行到 1058 行)一个名为 ota_from_target_files 的 Python 脚本,位置在 build/tools/releasetools/ota_from_target_files。这个脚本的作用是以第一步生成的 zip 原始包作为输入,最终生成可用的 OTA 升级 zip 包。
我们的实际情况是,在用命令 make otapackage 时生成的包中是没有 RADIO 目录下的 bootloader.img 镜像文件的(因为这部分更新已被屏蔽掉了)。但是这个函数中对于从包中未读取到 bootloader.img 文件的情况是有错误处理的,即返回。所以我们要从出现的实际错误中寻找问题的原由。
③ /cache/recovery/intent:Recovery 传递给 Main System 的信息。作用不详。
(二) 通过 BCB(Bootloader Control Block):
BCB 是 bootloader 与 Recovery 的通信接口,也是 Bootloader 与 Main System 之间的通信接口。存储在 Flash 中的 MISC 分区,占用三个 page,其本身就是一个结构体,具体成员及各成员含义如下:
struct bootloader_message {
char command32;
char status32;
char recovery1024;
};
① command 成员:其可能的取值我们在上文已经分析过了,即当我们想要在重启后进入 Recovery 模式时,会更新这个成员的值。另外,在成功更新后结束 Recovery 时,会清除这个成员的值,防止重启时再次进入 Recovery 模式。
② status:在完成相应的更新后,Bootloader 会将执行结果写入到这个字段。
③ recovery:可被 Main System 写入,也可被 Recovery 服务程序写入。该文件的内容格式为:
"recovery\n
\n
"
该文件存储的就是一个字符串,必须以 recovery\n 开头,否则这个字段的所有内容域会被忽略。recovery\n 之后的部分,是 /cache/recovery/command 支持的命令。可以将其理解为 Recovery 操作过程中对命令操作的备份。Recovery 对其操作的过程为:先读取 BCB,然后读取 /cache/recovery/command,然后将二者重新写回 BCB,这样在进入 Main System 之前,确保操作被执行。在操作之后、进入 Main System 之前,Recovery 又会清空 BCB 的 command 域和 recovery 域,这样确保重启后不再进入 Recovery 模式。
三、如何从 Main System 重启并进入 Recovery 模式
我们先看一下以上三个部分是怎样进行通信的,先看下图:
我们只看从 Main System 如何进入 Recovery 模式,其他的通信在后文中详述。
先从 Main System 开始看。当我们在 Main System 使用 update.zip 包进行升级时,系统会重启并进入 Recovery 模式。在系统重启之前,我们可以看到,Main System 必定会向 BCB 中的 command 域写入 boot-recovery(粉红色线),用来告知 Bootloader 重启后进入 recovery 模式。这一步是必须的。
至于 Main System 是否向 recovery 域写入值,我们在源码中不能肯定这一点。即便如此,重启进入 Recovery 模式后,Bootloader 会从 /cache/recovery/command 中读取值并放入到 BCB 的 recovery 域。而 Main System 在重启之前,肯定会向 /cache/recovery/command 中写入 Recovery 将要进行的操作命令。
至此,我们就大概知道了,在上层使用 update.zip 升级时,主系统是怎样告知重启后的系统进入 Recovery 模式的,以及在 Recovery 模式中要完成什么样的操作。
下一篇开始分析第一个阶段,即我们在上层使用 update.zip 包升级时,Main System 怎样重启并进入 Recovery 服务的细节流程。
文章开头我们就提到 update.zip 包来源有两种:
一个是 OTA 在线下载(一般下载到 /CACHE 分区);
一个是手动拷贝到 SD 卡中。不论是哪种方式获得 update.zip 包,
在进入 Recovery 模式前,都未对这个 zip 包做处理,只是在重启之前将 zip 包的路径告诉了 Recovery 服务(通过将 --update_package=CACHE:some_filename.zip 或 --update_package=SDCARD:update.zip 命令写入到 /cache/recovery/command 中)。在这里我们假设 update.zip 包已经制作好并拷贝到了 SD 卡中,并以 Settings --> About Phone --> System Update --> Installed From SDCARD 方式升级。
我们的测试开发板是 TCC8800,使用的 Android 源码是 gingerbread0919,在这种方式下升级的源码位于 gingerbread/device/telechips/common/apps/TelechipsSystemUpdater/src/com/telechips/android/systemupdater/ 下。下面我们具体分析这种升级方式下,我们的 update.zip 是怎样从上层一步步进入到 Recovery 模式的。
一、从 System Update 到 Reboot
当我们依次选择 Settings --> About Phone --> System Update --> Installed From SDCARD 后,会弹出一个对话框,提示已有 update.zip 包是否现在更新,我们从这个地方跟踪。这个对话框的源码是 SystemUpdateInstallDialog.java。
① 在 mNowButton 按钮的监听事件里,会调用 mService.rebootAndUpdate(new File(mFile))。
/*
* The recovery tool communicates with the main system through /cache files.
* /cache/recovery/command - INPUT - command line for tool, one arg per line
* /cache/recovery/log - OUTPUT - combined log file from recovery run(s)
* /cache/recovery/intent - OUTPUT - intent that was passed in
*
* The arguments which may be supplied in the recovery.command file:
* --send_intent=anystring - write the text out to recovery.intent
* --update_package=path - verify install an OTA package file
* --wipe_data - erase user data (and cache), then reboot
* --wipe_cache - wipe cache (but not user data), then reboot
* --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
*
* After completing, we remove /cache/recovery/command and reboot.
* Arguments may also be supplied in the bootloader control block (BCB).
* These important scenarios must be safely restartable at any point:
*
* FACTORY RESET
* 1. user selects "factory reset"
* 2. main system writes "--wipe_data" to /cache/recovery/command
* 3. main system reboots into recovery
* 4. get_args() writes BCB with "boot-recovery" and "--wipe_data"
* -- after this, rebooting will restart the erase --
* 5. erase_volume() reformats /data
* 6. erase_volume() reformats /cache
* 7. finish_recovery() erases BCB
* -- after this, rebooting will restart the main system --
* 8. main() calls reboot() to boot main system
*
* OTA INSTALL
* 1. main system downloads OTA package to /cache/some-filename.zip
* 2. main system writes "--update_package=/cache/some-filename.zip"
* 3. main system reboots into recovery
* 4. get_args() writes BCB with "boot-recovery" and "--update_package=..."
* -- after this, rebooting will attempt to reinstall the update --
* 5. install_package() attempts to install the update
* NOTE: the package install must itself be restartable from any point
* 6. finish_recovery() erases BCB
* -- after this, rebooting will (try to) restart the main system --
* 7. ** if install failed **
* 7a. prompt_and_wait() shows an error icon and waits for the user
* 7b; the user reboots (pulling the battery, etc) into the main system
* 8. main() calls maybe_install_firmware_update()
* ** if the update contained radio/hboot firmware **:
* 8a. m_i_f_u() writes BCB with "boot-recovery" and "--wipe_cache"
* -- after this, rebooting will reformat cache & restart main system --
* 8b. m_i_f_u() writes firmware image into raw cache partition
* 8c. m_i_f_u() writes BCB with "update-radio/hboot" and "--wipe_cache"
* -- after this, rebooting will attempt to reinstall firmware --
* 8d. bootloader tries to flash firmware
* 8e. bootloader writes BCB with "boot-recovery" (keeping "--wipe_cache")
* -- after this, rebooting will reformat cache & restart main system --
* 8f. erase_volume() reformats /cache
* 8g. finish_recovery() erases BCB
* -- after this, rebooting will (try to) restart the main system --
* 9. main() calls reboot() to boot main system
*
* SECURE FILE SYSTEMS ENABLE/DISABLE
* 1. user selects "enable encrypted file systems"
* 2. main system writes "--set_encrypted_filesystems=on|off" to
* /cache/recovery/command
* 3. main system reboots into recovery
* 4. get_args() writes BCB with "boot-recovery" and
* "--set_encrypted_filesystems=on|off"
* -- after this, rebooting will restart the transition --
* 5. read_encrypted_fs_info() retrieves encrypted file systems settings from /data
* Settings include: property to specify the Encrypted FS istatus and
* FS encryption key if enabled (not yet implemented)
* 6. erase_volume() reformats /data
* 7. erase_volume() reformats /cache
* 8. restore_encrypted_fs_info() writes required encrypted file systems settings to /data
* Settings include: property to specify the Encrypted FS status and
* FS encryption key if enabled (not yet implemented)
* 9. finish_recovery() erases BCB
* -- after this, rebooting will restart the main system --
* 10. main() calls reboot() to boot main system
*/
从注释中我们可以看到,Recovery 的服务内容主要有三类:
① FACTORY RESET,恢复出厂设置。
② OTA INSTALL,即我们的 update.zip 包升级。
③ ENCRYPTED FILE SYSTEM ENABLE/DISABLE,使能/关闭加密文件系统。具体的每一类服务的大概工作流程,注释中都有,我们在下文中会详细讲解 OTA INSTALL 的工作流程。这三类服务的大概流程都是通用的,只是不同操作体现为不同的操作细节。下面我们看 Recovery 服务的通用流程。
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include "common.h"
#include "install.h"
#include "mincrypt/rsa.h"
#include "minui/minui.h"
#include "minzip/SysUtil.h"
#include "minzip/Zip.h"
#include "mtdutils/mounts.h"
#include "mtdutils/mtdutils.h"
#include "roots.h"
#include "verifier.h"
#define ASSUMED_UPDATE_BINARY_NAME "META-INF/com/google/android/update-binary"
#define PUBLIC_KEYS_FILE "/res/keys"
// If the package contains an update binary, extract it and run it.
static int
try_update_binary(const char *path, ZipArchive *zip) {
const ZipEntry* binary_entry =
mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);
if (binary_entry == NULL) {
mzCloseZipArchive(zip);
return INSTALL_CORRUPT;
}
char* binary = "/tmp/update_binary";
unlink(binary);
int fd = creat(binary, 0755);
if (fd < 0) {
mzCloseZipArchive(zip);
LOGE("Can't make %s\n", binary);
return 1;
}
bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd);
close(fd);
mzCloseZipArchive(zip);
if (!ok) {
LOGE("Can't copy %s\n", ASSUMED_UPDATE_BINARY_NAME);
return 1;
}
int pipefd[2];
pipe(pipefd);
// When executing the update binary contained in the package, the
// arguments passed are:
//
// - the version number for this interface
//
// - an fd to which the program can write in order to update the
// progress bar. The program can write single-line commands:
//
// progress <frac> <secs>
// fill up the next <frac> part of of the progress bar
// over <secs> seconds. If <secs> is zero, use
// set_progress commands to manually control the
// progress of this segment of the bar
//
// set_progress <frac>
// <frac> should be between 0.0 and 1.0; sets the
// progress bar within the segment defined by the most
// recent progress command.
//
// firmware <"hboot"|"radio"> <filename>
// arrange to install the contents of <filename> in the
// given partition on reboot.
//
// (API v2: <filename> may start with "PACKAGE:" to
// indicate taking a file from the OTA package.)
//
// (API v3: this command no longer exists.)
//
// ui_print <string>
// display <string> on the screen.
//
// - the name of the package zip file.
//
char** args = malloc(sizeof(char*) * 5);
args[0] = binary;
args[1] = EXPAND(RECOVERY_API_VERSION); // defined in Android.mk
args[2] = malloc(10);
sprintf(args[2], "%d", pipefd[1]);
args[3] = (char*)path;
args[4] = NULL;
pid_t pid = fork();
if (pid == 0) {
close(pipefd[0]);
execv(binary, args);
fprintf(stdout, "E:Can't run %s (%s)\n", binary, strerror(errno));
_exit(-1);
}
close(pipefd[1]);
char buffer[1024];
FILE* from_child = fdopen(pipefd[0], "r");
while (fgets(buffer, sizeof(buffer), from_child) != NULL) {
char* command = strtok(buffer, " \n");
if (command == NULL) {
continue;
} else if (strcmp(command, "progress") == 0) {
char* fraction_s = strtok(NULL, " \n");
char* seconds_s = strtok(NULL, " \n");
float fraction = strtof(fraction_s, NULL);
int seconds = strtol(seconds_s, NULL, 10);
ui_show_progress(fraction * (1-VERIFICATION_PROGRESS_FRACTION),
seconds);
} else if (strcmp(command, "set_progress") == 0) {
char* fraction_s = strtok(NULL, " \n");
float fraction = strtof(fraction_s, NULL);
ui_set_progress(fraction);
} else if (strcmp(command, "ui_print") == 0) {
char* str = strtok(NULL, "\n");
if (str) {
ui_print("%s", str);
} else {
ui_print("\n");
}
} else {
LOGE("unknown command [%s]\n", command);
}
}
fclose(from_child);
int status;
waitpid(pid, &status, 0);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
LOGE("Error in %s\n(Status %d)\n", path, WEXITSTATUS(status));
return INSTALL_ERROR;
}
return INSTALL_SUCCESS;
}
// Reads a file containing one or more public keys as produced by
// DumpPublicKey: this is an RSAPublicKey struct as it would appear
// as a C source literal, eg:
//
// "{64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}"
//
// (Note that the braces and commas in this example are actual
// characters the parser expects to find in the file; the ellipses
// indicate more numbers omitted from this example.)
//
// The file may contain multiple keys in this format, separated by
// commas. The last key must not be followed by a comma.
//
// Returns NULL if the file failed to parse, or if it contain zero keys.
static RSAPublicKey*
load_keys(const char* filename, int* numKeys) {
RSAPublicKey* out = NULL;
*numKeys = 0;
FILE* f = fopen(filename, "r");
if (f == NULL) {
LOGE("opening %s: %s\n", filename, strerror(errno));
goto exit;
}
int i;
bool done = false;
while (!done) {
++*numKeys;
out = realloc(out, *numKeys * sizeof(RSAPublicKey));
RSAPublicKey* key = out + (*numKeys - 1);
if (fscanf(f, " { %i , 0x%x , { %u",
&(key->len), &(key->n0inv), &(key->n[0])) != 3) {
goto exit;
}
if (key->len != RSANUMWORDS) {
LOGE("key length (%d) does not match expected size\n", key->len);
goto exit;
}
for (i = 1; i < key->len; ++i) {
if (fscanf(f, " , %u", &(key->n[i])) != 1) goto exit;
}
if (fscanf(f, " } , { %u", &(key->rr[0])) != 1) goto exit;
for (i = 1; i < key->len; ++i) {
if (fscanf(f, " , %u", &(key->rr[i])) != 1) goto exit;
}
fscanf(f, " } } ");
// if the line ends in a comma, this file has more keys.
switch (fgetc(f)) {
case ',':
// more keys to come.
break;
case EOF:
done = true;
break;
default:
LOGE("unexpected character between keys\n");
goto exit;
}
}
fclose(f);
return out;
exit:
if (f) fclose(f);
free(out);
*numKeys = 0;
return NULL;
}
int
install_package(const char *path)
{
ui_set_background(BACKGROUND_ICON_INSTALLING);
ui_print("Finding update package...\n");
ui_show_indeterminate_progress();
LOGI("Update location: %s\n", path);
if (ensure_path_mounted(path) != 0) {
LOGE("Can't mount %s\n", path);
return INSTALL_CORRUPT;
}
ui_print("Opening update package...\n");
int numKeys;
RSAPublicKey* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);
if (loadedKeys == NULL) {
LOGE("Failed to load keys\n");
return INSTALL_CORRUPT;
}
LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE);
// Give verification half the progress bar...
ui_print("Verifying update package...\n");
ui_show_progress(
VERIFICATION_PROGRESS_FRACTION,
VERIFICATION_PROGRESS_TIME);
int err;
err = verify_file(path, loadedKeys, numKeys);
free(loadedKeys);
LOGI("verify_file returned %d\n", err);
if (err != VERIFY_SUCCESS) {
LOGE("signature verification failed\n");
return INSTALL_CORRUPT;
}
/* Try to open the package.
*/
ZipArchive zip;
err = mzOpenZipArchive(path, &zip);
if (err != 0) {
LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : "bad");
return INSTALL_CORRUPT;
}
/* Verify and install the contents of the package.
*/
ui_print("Installing update...\n");
return try_update_binary(path, &zip);
}
下面顺着上面的流程图和源码来分析这一流程:
① ensure_path_mount():先判断所传的 update.zip 包路径所在的分区是否已经挂载。如果没有则先挂载。
② load_keys():加载公钥源文件,路径位于 /res/keys。这个文件在 Recovery 镜像的根文件系统中。
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "edify/expr.h"
#include "updater.h"
#include "install.h"
#include "minzip/Zip.h"
// Generated by the makefile, this function defines the
// RegisterDeviceExtensions() function, which calls all the
// registration functions for device-specific extensions.
#include "register.inc"
// Where in the package we expect to find the edify script to execute.
// (Note it's "updateR-script", not the older "update-script".)
#define SCRIPT_NAME "META-INF/com/google/android/updater-script"
int main(int argc, char** argv) {
// Various things log information to stdout or stderr more or less
// at random. The log file makes more sense if buffering is
// turned off so things appear in the right order.
setbuf(stdout, NULL);
setbuf(stderr, NULL);
if (argc != 4) {
fprintf(stderr, "unexpected number of arguments (%d)\n", argc);
return 1;
}
char* version = argv[1];
if ((version[0] != '1' && version[0] != '2' && version[0] != '3') ||
version[1] != '\0') {
// We support version 1, 2, or 3.
fprintf(stderr, "wrong updater binary API; expected 1, 2, or 3; "
"got %s\n",
argv[1]);
return 2;
}
// Set up the pipe for sending commands back to the parent process.
int fd = atoi(argv[2]);
FILE* cmd_pipe = fdopen(fd, "wb");
setlinebuf(cmd_pipe);
// Extract the script from the package.
char* package_data = argv[3];
ZipArchive za;
int err;
err = mzOpenZipArchive(package_data, &za);
if (err != 0) {
fprintf(stderr, "failed to open package %s: %s\n",
package_data, strerror(err));
return 3;
}
const ZipEntry* script_entry = mzFindZipEntry(&za, SCRIPT_NAME);
if (script_entry == NULL) {
fprintf(stderr, "failed to find %s in %s\n", SCRIPT_NAME, package_data);
return 4;
}
char* script = malloc(script_entry->uncompLen+1);
if (!mzReadZipEntry(&za, script_entry, script, script_entry->uncompLen)) {
fprintf(stderr, "failed to read script from package\n");
return 5;
}
script[script_entry->uncompLen] = '\0';
// Configure edify's functions.
RegisterBuiltins();
RegisterInstallFunctions();
RegisterDeviceExtensions();
FinishRegistration();
// Parse the script.
Expr* root;
int error_count = 0;
yy_scan_string(script);
int error = yyparse(&root, &error_count);
if (error != 0 || error_count > 0) {
fprintf(stderr, "%d parse errors\n", error_count);
return 6;
}
// Evaluate the parsed script.
UpdaterInfo updater_info;
updater_info.cmd_pipe = cmd_pipe;
updater_info.package_zip = &za;
updater_info.version = atoi(version);
State state;
state.cookie = &updater_info;
state.script = script;
state.errmsg = NULL;
char* result = Evaluate(&state, root);
if (result == NULL) {
if (state.errmsg == NULL) {
fprintf(stderr, "script aborted (no error message)\n");
fprintf(cmd_pipe, "ui_print script aborted (no error message)\n");
} else {
fprintf(stderr, "script aborted: %s\n", state.errmsg);
char* line = strtok(state.errmsg, "\n");
while (line) {
fprintf(cmd_pipe, "ui_print %s\n", line);
line = strtok(NULL, "\n");
}
fprintf(cmd_pipe, "ui_print\n");
}
free(state.errmsg);
return 7;
} else {
fprintf(stderr, "script result was [%s]\n", result);
free(result);
}
if (updater_info.package_zip) {
mzCloseZipArchive(updater_info.package_zip);
}
free(script);
return 0;
}
通过上面的源码来分析这个程序的执行过程:
① 函数参数以及版本的检查:当前 updater binary API 所支持的版本号有 1、2、3 这三个。
② 获取管道并打开:在执行此程序的过程中向该管道写入命令,用于通知其父进程根据命令去更新 UI 显示。
③ 读取 updater-script 脚本:从 update.zip 包中将 updater-script 脚本读到一块动态内存中,供后面执行。