Android Recovery OTA升級(二)—— Recovery原始碼解析
目錄
概述
之前部落格裡一篇文章講解了OTA包的生成原理,這篇文章主要是從Recovery原始碼的角度介紹一下Recovery是如何使用OTA包進行系統升級的。
為了防止洩密,本文原始碼都是基於Android4.4.2_r1版本進行分析。
Recovery原始碼解析
Recovery原始碼的入口位置為:bootable/recovery/recovery.cpp檔案。下面我就來分析一下Recovery的原始碼。
static const char *CACHE_LOG_DIR = "/cache/recovery";
static const char *COMMAND_FILE = "/cache/recovery/command" ;
static const char *INTENT_FILE = "/cache/recovery/intent";
static const char *LOG_FILE = "/cache/recovery/log";
註釋裡英文寫的很清楚:
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
同時,程式碼裡還有一段對Recovery識別命令的註釋描述:
The arguments which may be supplied in the command file:
1. –send_intent=anystring - write the text out to recovery.intent
2. –update_package=path - verify install an OTA package file
3. –wipe_data - erase user data (and cache), then reboot
4. –wipe_cache - wipe cache (but not user data), then reboot
main函式
接下面,我們分析一下recovery.c的入口main函式。
輸出重定向
static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log"
int main(int argc, char **argv)
{
time_t start = time(NULL);
freopen(TEMPORARY_LOG_FILE, "a", stdout);
setbuf(stdout, NULL);
freopen(TEMPORARY_LOG_FILE, "a", stderr);
setbuf(stderr, NULL);
// 列印啟動recovery的時間
printf("Starting recovery on %s", ctime(&start));
}
該部分的主要作用是:將標準輸出和錯誤輸出重定向到/tmp/recovery.log檔案中。
填充fstab結構體
struct fstab {
int num_entries;
struct fstab_rec *recs;
char *fstab_filename;
};
struct fstab_rec {
char *blk_device;
char *mount_point;
char *fs_type;
unsigned long flags;
char *fs_options;
int fs_mgr_flags;
char *key_loc;
char *verity_loc;
long long length;
char *label;
int partnum;
int swap_prio;
unsigned int zram_size;
};
typdef struct fstab_rec Volume;
void load_volume_table()
{
int i;
int ret;
// 解析/etc/recovery.fstab配置檔案,填充fstab結構體
fstab = fs_mgr_read_fstab("/etc/recovery.fstab");
if (!fstab) {
LOGE("failed to read /etc/recovery.fstab\n");
return;
}
// 在fstab結構體中增加/tmp分割槽
ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk", 0);
if (ret < 0) {
LOGE("failed to add /tmp entry to fstab\n");
fs_mgr_free_fstab(fstab);
fstab = NULL;
return;
}
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");
}
int main(int argc, char **argv)
{
load_volume_table();
}
該部分程式碼的主要作用是用recovery根目錄下的/etc/recovery.fstab中的分割槽內容和/tmp分割槽內容來填充了fstab結構體,並沒有真正的進行分割槽掛載。
掛載cache分割槽
原始碼分析如下:
typedef struct fstab_rec Volume;
#define LAST_LOG_FILE "/cache/recovery/last_log"
int ensure_path_mounted(const char* path)
{
// 這裡是在fstab結構體中找到掛載點為/cache的fstab_recs
Volume* v = volume_for_path(path);
if (v == NULL) {
LOGE("unknown volume for path [%s]\n", path);
return -1;
}
// ramdisk型別的分割槽是一直掛載的。
if (strcmp(v->fs_type, "ramdisk") == 0) {
return 0;
}
int result;
result = scan_mounted_volumes();
const MountedVolume* mv = find_mounted_volume_by_mount_point(v->mount_point);
if (mv) {
// 說明當前分割槽已經被掛載了
return 0;
}
// 下面是具體的掛載過程
mkdir(v->mount_point, 0755);
if (strcmp(v->fs_type, "yaffs2") == 0) {
// .......不用管這個yaffs2分割槽型別了,目前基本是ext4的。
} else if (strcmp(v->fs_type, "ext4") == 0 || strcmp(v->fs_type, "vfat") == 0) {
// ext4和vfat型別的分割槽呼叫mount函式進行掛載,掛載成功返回0,失敗返回-1
result = mount(v->blk_device, v->mount_point, v->fs_type, MS_NOATIME | MS_NODEV | MS_NODIRATIME, "");
if (result == 0) return 0;
LOGE("failed to mount %s (%s)\n", v->mount_point, strerror(errno));
return -1;
}
LOGE("unknown fs_type \"%s\" for %s\n", v->fs_type, v->mount_point);
return -1;
}
#define LAST_LOG_FILE "/cache/recovery/last_log"
int main(int argc, char **argv)
{
ensure_path_mounted(LAST_LOG_FILE);
// 這個不用care了,就是重新命名了log
rotate_last_logs(10);
}
從原始碼可以看出,ensure_path_mounted(LAST_LOG_FILE);程式碼的主要作用是保證/cache分割槽被掛載。
獲取Recovery命令引數
原始碼分析如下:
struct bootloader_message
{
char command[32];
char status[32];
char recovery[768];
char stage[32];
char reverse[224];
};
int get_bootloader_message(struct bootloader_message *out) {
Volume* v = volume_for_path("/misc");
if (v == NULL) {
return -1;
}
if (strcmp(v->fs_type, "mtd") == 0) {
return get_bootloader_message_mtd(out, v);
} else if (strcmp(v->fs_type, "emmc") == 0){
return get_bootloader_message_block(out, v);
}
return -1;
}
static const char *COMMAND_FILE = "/cache/recovery/command";
static const int MAX_ARGS = 100;
static void get_args(int *argc, char ***argv)
{
struct bootloader_message boot;
memset(&boot, 0, sizeof(boot));
// 首先從MISC分割槽中讀取BCB資料塊到boot變數中,可能存在為空的情況。
get_bootloader_message(&boot);
stage = strndup(boot.stage, sizeof(boot.stage));
// 從/cache/recovery/command獲取引數,一般常用的ota升級做法。
if (*argc < 1) {
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;
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);
} else {
--*argc;
}
}
check_and_fclose(fp, COMMAND_FILE);
}
}
// ....省略部分程式碼,程式碼的作用就是將從/cache/recovery/command獲取的引數寫入到misc分割槽
}
int main(int argc, char **argv)
{
get_args(&argc, &argv);
}
可以看到,對於ota升級來說,get_args函式的作用就是讀取/cache/recovery/command檔案,將引數存到argv二維陣列中。
分析recovery命令
原始碼如下:
int main(int argc, char **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;
case 't': show_text = 1; break;
case 'x': just_exit = true; break;
case 'l': locale = optarg; break;
case 'g':
if (stage == NULL || stage == '\0') {
char buffer[20] = "1/";
strncat(buffer, optarg, sizeof(buffer)-3);
stage = strdup(buffer);
}
case 'p': shutdown_after = true; break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
}
賦值結束後,就是UI和真正的OTA升級過程了。
install_package
原始碼如下:
// 解除安裝除了/tmp和/cache的其他分割槽
int setup_install_mounts() {
if (fstab == NULL) {
LOGE("can't set up install mounts: no fstab loaded\n");
return -1;
}
for (int i = 0; i < fstab.num_entries; i ++) {
Volume* v = fstab->recs + i;
if (strcmp(v->mount_point, "/tmp") == 0 ||
strcmp(v->mount_point, "/cache") == 0) {
if (ensure_path_mounted(v->mount_point) != 0) return -1;
} else {
if (ensure_path_unmounted(v->mount_point) != 0) return -1;
}
}
return 0;
}
// ota升級的真正實現,這裡去除掉跟UI顯示相關的邏輯
#define PUBLIC_KEYS_FILE "/res/keys"
static int really_install_package(const char *path, int* wipe_cache)
{
// 確保zip包所在的目錄是掛載的
if (ensure_path_mounted(path) != 0) {
LOGE("Can't mount %s\n", path);
return INSTALL_CORRUPT;
}
// 載入公鑰原始檔,根據公鑰對zip包進行校驗
int numKeys;
Certificate* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);
err = verify_file(path, loadedKeys, numKeys);
// 開啟升級包,並將相關的資訊拷貝到一個臨時的ZipArchinve變數中。這一步並未對我們的update.zip包解壓。
ZipArchive zip;
err = mzOpenZipArchive(path, &zip);
// 真正fota升級的過程
return try_update_binary(path, &zip, wipe_cache);
}
int install_package(const char* path, int* wipe_cache, const char* install_file)
{
FILE* install_log = fopen_path(install_file, "w");
if (install_log) {
fputs(path, install_log);
fputc('\n', install_log);
} else {
LOGE("failed to open last_install: %s\n", strerror(errno));
}
int result;
if (setup_install_mounts() != 0) {
result = INSTALL_ERROR;
} else {
result = really_install_package(path, wipe_cache);
}
if (install_log) {
fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log);
fputc('\n', install_log);
fclose(install_log);
}
return result;
}
static const char *TEMPORARY_INSTALL_FILE = "/tmp/last_install";
int main(int argc, char **argv)
{
if (update_package != NULL) {
status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE);
if (status == INSTALL_SUCCESS && wipe_cache) {
if (erase_volume("/cache")) {
LOGE("Cache wipe (requested by package) failed.");
}
}
if (status != INSTALL_SUCCESS) {
ui->Print("Installation aborted.\n");
}
}
}
接下來,分別講解一下really_install_package中具體函式實現。
load_keys——載入公鑰原檔案
load_keys原始碼實現如下:
#define RSANUMBYTES 256
#define RSANUMWORDS (RSANUMBYTES / sizeof(uint32_t))
typedef struct RSAPublicKey {
int len;
uint32_t n0inv;
uint32_t n[RSANUMWORDS];
uint32_t rr[RSANUMWORDS];
int exponent;
} RSAPublicKey;
typedef struct Certificate {
int hash_len;
RSAPublicKey* public_key;
} Certificate;
#define SHA_DIGEST_SIZE 20
#define SHA256_DIGEST_SIZE 32
Certificate* load_keys(const char *filename, int *numKeys)
{
Certificate *out = NULL;
*numKeys = 0;
// 開啟recovery根目錄下的/res/keys檔案
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
goto exit;
}
{
int i;
bool done = false;
while (! done) {
++ *numKeys;
out = (Certificate *)realloc(out, *numKeys * sizeof(Certificate));
Certificate *cert = out + (*numKeys - 1);
cert->public_key = (RSAPublicKey*)malloc(sizeof(RSAPublicKey));
// 分析第一個字元,獲取版本和長度
char start_char;
if (fscanf(f, "%c", &start_char) != 1) goto exit;
if (start_char == '{') {
cert->public_key->exponent = 3;
cert->hash_len = SHA_DIGEST_SIZE;
} else if (start_char == 'v') {
int version;
if (fscanf(f, "%d {", version) != 1) goto exit;
switch (version) {
case 2:
cert->public_key->exponent = 65537;
cert->hash_len = SHA_DIGEST_SIZE;
break;
case 3:
cert->public_key->exponent = 3;
cert->hash_len = SHA256_DIGEST_SIZE;
break;
case 4:
cert->public_key->exponent = 65537;
cert->hash_len = SHA256_DIGEST_SIZE;
break;
default:
goto exit;
}
}
RSAPublicKey *key = cert->public_key;
if (fscanf(f, " %i , 0x%x , { %u", &(key->len), &(key->n0inv), &(key->n[0])) != 3) goto exit;
// 依次讀取key->len個數字到key->n[i]中
for (i = 0; i < key->len; i ++) {
if (fscanf(f, " , %u", &(key->n[i])) != 1) goto exit;
}
// 再次讀取(key->len + 1)個數字到key->rr陣列中
if (fscanf(f, " } , { %u", &(key->rr[0])) != 1) goto exit;
for (i = 0; i < key->len; i ++) {
if (fscanf(f, " , %u", &(key->rr[i])) != 1) goto exit;
}
fscanf(f, " } } ");
switch (fgetc(f)) {
case ',':
// 還有需要load的key
break;
case EOF:
done = true;
break;
default:
goto exit;
}
}
}
fclose(f);
return out;
exit:
if (f) fclose(f);
free(out);
*numKeys = 0;
return NULL;
}
原始碼還是很簡單的,就是解析/res/keys檔案,將該檔案裡面的校驗key儲存到Certificate* loadedKeys中。
verify_file——對升級包進行簽名校驗
verify_file函式的原始碼如下:
#define VERIFY_SUCCESS 0
#define VERIFY_FAILURE 1
int verify_file(const char* path, const Certificate* pKeys, unsigned int numKeys) {
FILE *f = fopen(path, "rb");
if (f == NULL) {
return VERIFY_FAILURE;
}
// 從檔案最後6個字元開始校驗,並獲取comment_size和signature_start等資訊
#define FOOTER_SIZE 6
if (fseek(f, -FOOTER_SIZE, SEEK_END) != 0) {
fclose(f);
return VERIFY_FAILURE;
}
unsigned char footer[FOOTER_SIZE];
if (fread(footer, 1, FOOTER_SIZE, f) != FOOTER_SIZE) {
fclose(f);
return VERIFY_FAILURE;
}
if (footer[2] != 0xff || footer[3] != 0xff) {
fclose(f);
return VERIFY_FAILURE;
}
size_t comment_size = footer[4] + (footer[5] << 8);
size_t signature_start = footer[0] + (footer[1] << 8);
if (signature_start - FOOTER_SIZE < RSANUMBYTES) {
fclose(f);
return VERIFY_FAILURE;
}
// 校驗從檔案末尾開始的倒數eocd_size個字元
#define EOCD_HEADER_SIZE 22
size_t eocd_size = comment_size + EOCD_HEADER_SIZE;
if (fseek(f, -eocd_size, SEEK_END) != 0) {
fclose(f);
return VERIFY_FAILURE;
}
// ... 省略,感興趣的自己看原始碼
#define BUFFER_SIZE 4096
// ... rsa校驗,感興趣的自己看原始碼
}
verify_file函式校驗失敗的話,ota過程會直接返回校驗失敗的錯誤,不再進行ota升級流程。
mzOpenZipArchive-開啟升級包,獲取相關資訊
mzOpenZipArchive函式的作用是:開啟升級包,並將相關的資訊拷貝到一個臨時的ZipArchinve變數中。具體實現程式碼如下:
typedef struct MemMapping {
void *addr;
size_t length;
void* baseAddr;
size_t baseLength;
} MemMapping;
int mzOpenZipArchive(const char* fileName, ZipArchive* pArchive)
{
MemMapping map;
int err;
map.addr = NULL;
memset(pArchive, 0, sizeof(*pArchive));
pArchive->fd = open(fileName, O_RDONLY, 0);
if (pArchive->fd < 0) {
goto bail;
}
// 呼叫mmap將zip檔案對映到內容中,起始地址儲存到map->baseAddr和map->addr,對映長度儲存在map->baseLength和map->length中。
if (sysMapFileInShmem(pArchive->fd, &map) != 0) {
goto bail;
}
// 解析map中的內容到pArhchive中(其中,map是zip包檔案在記憶體中的對映)
if (!parseZipArchive(pArchive, &map)) {
goto bail;
}
sysCopyMap(&pArchive->map, &map);
map->addr = NULL;
bail:
// 資源回收
}
try_update_binary-ota真正執行的地方
try_update_binary才是recovery對update.zip真正開始進行升級的地方。原始碼如下:
static int try_update_binary(const char *path, ZipArchive *zip, int* wipe_cache)
{
// 將zip包META-INF/com/google/android/update-binary檔案內容儲存到binary_entry結構體中
const ZipEntry* binary_entry = mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);
// 這裡是將binary_entry結構體的內容儲存到/tmp/update_binary檔案中
// 其實說白了,就是將ota zip包中的META-INF/com/google/android/update-binary複製到recovery的/tmp/update_binary檔案中
const char* binary = "/tmp/update_binary";
unlink(binary);
int fd = creat(binary, 0755);
bool ok = mzExtractZipEntryToFile(META-INF/com/google/android/update-binaryzip, binary_entry, fd);
close(fd);
mzCloseZipArchive(zip);
// 建立管道,用於子程序和父程序的通訊
int pipefd[2];
pipe(pipefd);
//
const char** args = (const char**)malloc(sizeof(char*) * 5);
args[0] = binary;
args[1] = EXPAND(RECOVERY_API_VERSION);
char* temp = (char*)malloc(10);
sprintf(temp, "%s", pipefd[1]);
args[2] = temp;
args[3] = (char *)path;
args[4] = NULL;
pid_t pid = fork();
// 子程序執行update_binary,進行刷機操作
if (pid == 0) {
// 子程序傳送訊息,所以關閉receive fd.
close(pipefd[0]);
// 執行binary,即執行/tmp/update_binary,需要去研究update.c的原始碼
execv(binary, (char* const*)args);
}
// 父程序用來接收訊息,所以關閉send fd.
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) {
// 父程序顯示UI進度
} else if (strcmp(command, "set_progress") == 0) {
// 父程序設定UI進度
} else if (strcmp(command, "ui_print") == 0) {
// 父程序列印資訊
} else if (strcmp(command, "wipe_cache") == 0) {
// 清除cache分割槽
} else if (strcmp(command, "special_factory_reset") == 0) {
// 清除data和cache分割槽
} else if (strcmp(command, "clear_display") == 0) {
// 清除UI顯示
}
}
fclose(from_child);
int status;
waitpid(pid, &status, 0);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
return INSTALL_ERROR;
}
return INSTALL_SUCCESS;
}
需要注意的是:execv(binary,args)的作用就是去執行binary程式,這個程式的實質就是去解析update.zip包中的updater-script指令碼中的命令並執行。由此,Recovery服務就進入了實際安裝update.zip包的過程。繼續分析,則需要分析updater.c檔案的main函數了。