Recovery 模式最主要的兩個功能是恢複出廠設定和更新系統版本。本Recovery相關的分析内容主要為兩部分:FACTORY RESET+OTA INSTALL
Recovery模式的主界面
進入recovery的方式
我公司手機一般正确手動進入recovery模式的方式為:power+volume up+volume down
手機開機後,硬體系統上電,完成一系列的初始化工作:CPU、序列槽、終端、timer、DDR等硬體裝置,然後加載bootloader,為後面核心加載做準備工作。在系統啟動初始化完成後系統檢測進入哪一種工作模式,這一部分代碼的源檔案在\bootable\bootloader\lk\app\aboot\aboot.c檔案的aboot_init()函數中:
檢測使用者關機方式,如果是強制關機,則進入normal_boot模式
if (is_user_force_reset())
goto normal_boot;
檢測音量上下鍵的按鍵狀态,判斷進入何種模式:
if (keys_get_state(KEY_VOLUMEUP) && keys_get_state(KEY_VOLUMEDOWN))
{
dprintf(ALWAYS,"dload mode key sequence detected\n");
if (set_download_mode(EMERGENCY_DLOAD))
{
dprintf(CRITICAL,"dload mode not supported by target\n");
}
else
{
reboot_device(DLOAD);
dprintf(CRITICAL,"Failed to reboot into dload mode\n");
}
boot_into_fastboot = true;
}
if (!boot_into_fastboot)
{
if (keys_get_state(KEY_HOME) || keys_get_state(KEY_VOLUMEUP))
boot_into_recovery = 1;
if (!boot_into_recovery &&
(keys_get_state(KEY_BACK) || keys_get_state(KEY_VOLUMEDOWN)))
boot_into_fastboot = true;
}
根據以上代碼,開機過程中按home鍵或者音量上鍵會進入recovery模式,按back鍵或者音量下鍵會進入fastboot模式。
如果沒有組合鍵(代碼中稱為magic key)按下,則會檢測SMEM(在後頭會介紹SMEM的的來源) 中的reboot_mode 變量值。
reboot_mode = check_reboot_mode();
hard_reboot_mode = check_hard_reboot_mode();
if (reboot_mode == RECOVERY_MODE ||
hard_reboot_mode == RECOVERY_HARD_RESET_MODE) {
boot_into_recovery = 1;
} else if(reboot_mode == FASTBOOT_MODE ||
hard_reboot_mode == FASTBOOT_HARD_RESET_MODE) {
boot_into_fastboot = true;
} else if(reboot_mode == ALARM_BOOT ||
hard_reboot_mode == RTC_HARD_RESET_MODE) {
boot_reason_alarm = true;
}
reboot_mode 可取的值宏定義為:
#define FASTBOOT_MODE 0x77665500
#define RECOVERY_MODE 0x77665502
而check_reboot_mode 函數定義在\bootable\bootloader\lk\target\ msm8916\init.c 中,函數先讀取restart_reason_addr 處的數值,定義為0x2A05F65C,沒有采取宏定義,屬于不規範的表達。讀取完該值之後,在該位址寫入0x00,即擦除其内容,以防下次啟動又進入recovery 模式。
unsigned check_reboot_mode(void)
{
uint32_t restart_reason = 0;
restart_reason = readl(RESTART_REASON_ADDR);
writel(0x00, RESTART_REASON_ADDR);
return restart_reason;
}
如果reboot_mode 的值沒有定義,則讀取MISC 分區的BCB 進行判斷,調用函數為recovery_init(),其實作在\bootable\bootloader\lk\app\aboot\recovery.c 中,函數先通過調用get_recovery_message()把BCB 讀到recovery_message 結構體中,再讀取其command 字段。如果字段是“boot-recovery”,則進入recovery 模式;如果是“update-radio”,則進入固件更新流程。
系統啟動流程分析圖
如果以上條件皆不滿足,則進入正常啟動序列,系統會加載boot.img檔案,然後加載kernel,在核心加載完成之後,會根據核心的傳遞參數尋找android的第一個使用者程序,即init程序,該程序根據init.rc以及init.$(hardware).rc腳本檔案來啟動android的必要的服務,直到完成android系統的啟動。
當進入recovery模式時,系統加載的是recovery.img檔案,該檔案内容與boot.img類似,也包含了标準的核心和根檔案系統。但是recovery.img為了具有恢複系統的能力,比普通的boot.img目錄結構中:
1、多了/res/images目錄,在這個目錄下的圖檔是恢複時我們看到的背景畫面。
2、多了/sbin/recovery二進制程式,這個就是恢複用的程式。
3、/sbin/adbd不一樣,recovery模式下的adbd不支援shell。
4、初始化程式(init)和初始化配置檔案(init.rc)都不一樣。這就是系統沒有進入圖形界面而進入了類似文本界面,并可以通過簡單的組合鍵進行恢複的原因。
與正常啟動系統類似,也是啟動核心,然後啟動檔案系統。在進入檔案系統後會執行/init,init的配置檔案就是 /init.rc。這個配置檔案位于bootable/recovery/etc/init.rc。檢視這個檔案我們可以看到它做的事情很簡單:
1) 設定環境變量。
2) 建立etc連接配接。
3) 建立目錄,備用。
4) 挂載檔案系統。
5) 啟動recovery(/sbin/recovery)服務。
6) 啟動adbd服務(用于調試)。
上文所提到的fastboot模式,即指令或SD卡燒寫模式,不加載核心及檔案系統,此處可以進行工廠模式的燒寫。
綜上所述,有三種進入recovery模式的方法,分别是開機時按組合鍵,寫SMEM中的reboot_mode變量值,以及寫位于MISC分區的BCB中的command字段。
通過指令進入recovery模式:adb reboot recovery
Recovery主界面内容分析
OTA工作過程
更新所需要的update.zip包來源有兩種,一是OTA線上下載下傳(一般下載下傳到/CACHE分區),二是手動拷貝到SD卡中。不論是哪種方式獲得update.zip包,在進入Recovery模式前,都未對這個zip包做處理。隻是在重新開機之前将zip包的路徑告訴了Recovery服務。
當選擇更新後,調用RecoverySystem類的installPackage()方法。這個函數首先根據傳過來的封包件,擷取這個封包件的絕對路徑filename,然後将其拼成arg = “--update_package=” + filename,最終會被寫入到BCB中,這個就是重新開機進入Recovery模式後,Recovery服務要進行的操作。它被傳遞到函數bootCommand(context,arg),在這個函數中才是Main System在重新開機前真正做的準備。
Recovery模式主要的執行過程在bootable/recovery/recovery.cpp中,這裡從main()函數開始分析
Int main(int argc,char **argv){
…… ……
}
具體執行流程如下:
1、在函數的開始會對argc和argv進行檢驗:
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
adb_main();//進入adb_main()過程
return 0;
}
2、 Load and parse volume data from /etc/recovery.fstab.
void load_volume_table();該函數在bootable/recovery/roots.cpp中,主要功能根據“etc/recovery.fstab”加載和解析分區;
3、確定參數傳入的分區是被成功mounted,成功時傳回0
ensure_path_mounted(LAST_LOG_FILE);// LAST_LOG_FILE =”/cache/recovery/last_log”存放的是最近一次的recovery過程的日志檔案;
4、重命名日志, Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max
rotate_last_logs(KEEP_LOG_COUNT);// KEEP_LOG_COUNT不超過10個
5、擷取指令參數,get_args(&argc, &argv);
如果參數未提供,則從BCB(Bootloader Control Block)查找,如果BCB中也沒有則嘗試在指令檔案中查找相應的指令,将最終獲得的參數寫進BCB中,直到finish_recovery被調用完,再從BCB中清除。
6、根據指令參數給控制變量指派,具體過程見如下的循環:
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);
}
break;
}
case 'p': shutdown_after = true; break;
case 'r': reason = optarg; break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
其中的OPTIONS定義如下:
static const struct option OPTIONS[] = {
{ "send_intent", required_argument, NULL, 's' },
{ "update_package", required_argument, NULL, 'u' },
{ "wipe_data", no_argument, NULL, 'w' },
{ "wipe_cache", no_argument, NULL, 'c' },
{ "show_text", no_argument, NULL, 't' },
{ "just_exit", no_argument, NULL, 'x' },
{ "locale", required_argument, NULL, 'l' },
{ "stages", required_argument, NULL, 'g' },
{ "shutdown_after", no_argument, NULL, 'p' },
{ "reason", required_argument, NULL, 'r' },
{ NULL, 0, NULL, 0 },
};
此處這樣做的原因我認為是Java中swich case中智能使用單個字元,例如’c’,’x’等,通過轉換獲得其參數指令後給對應的控制變量指派。
從以上的參數可以判斷locale值,從cache中加載現場
if (locale == NULL) {
load_locale_from_cache();
//從cache/recovery/last_locale檔案中獲得
}
7、初始化UI界面:Recovery 服務使用了一個基于framebuffer 的miniui 系統,ui_init 函數對其進行了簡單的初始化。在Recovery 服務的過程中主要用于顯示一個背景圖檔(正在安裝或安裝失敗)和一個進度條(用于顯示進度)。另外還啟動了兩個線程,一個用于處理進度條的顯示(progress_thread),另一個用于響應使用者的按鍵(input_thread)。
獲得recovery過程使用的device :Device* device = make_device();
進一步獲得Recovery_Ui對象:ui=device->GetUI();并對ui進行初始化和現場設定。
在Init()之後調用SetStage(),顯示一個狀态訓示
設定背景SetBackground(RecoveryUI::NONE);
調用顯示内容,ShowText(true),内部調用update_screen_locked(),用來重新繪制更新界面。
到這為止做的準備工作基本完成:參數已經被解析、初始化完成、UI被捕獲以及繪制,接着進行的recovery核心部分,調用StartRecovery()。
8、執行recovery操作
if (update_package != NULL) {
status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);
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");
// 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.
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 (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
} else if (wipe_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);
}
判斷update_package是否存在,若存在則調用install_package()進行更新包更新,在此過程如果傳回INSTALL_SUCCESS且wipe_cache is true,則執行wipe cache partition;
install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true)
如果update_package為NULL,wipe_data為true則進行device->WipeData()、erase_volume(“/data”)、erase_volume(“/cache”)操作;
如果wipe_cache為true,則執行erase_volume(“/cache”);
9、根據6過程中中shutdown_after的指派以及8過程中傳回的更新結果傳回的狀态status更新後續操作:
Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
ui->ShowText(true);
Device::BuiltinAction temp = prompt_and_wait(device, status);
if (temp != Device::NO_ACTION) after = temp;
}
10、執行recovery後續工作:清除recovery相關的指令,準備啟動系統,并copy日志檔案到cache、記錄intent跟main system溝通、删除指令檔案、解除安裝cache分區。
finish_recovery(send_intent);
(1) 将intent(字元串)的内容作為參數傳進finish_recovery 中。如果有intent 需要告知Main System,則将其寫入/cache/recovery/intent 中。
(2) 将記憶體檔案系統中的Recovery 服務的日志(/tmp/recovery.log)拷貝到cache 分區中的 /cache/recovery/log 檔案,以便告知重新開機後的Main System 發生過什麼。
(3) 擦除MISC 分區中的BCB 資料塊的内容,以便系統重新開機後不再進入Recovery 模式,而是進入更新後的主系統。
(4) 删除/cache/recovery/command 檔案。這一步也是很重要的,因為重新開機後bootloader 會自動檢索這個檔案,如果未删除的話又會進入Recovery 模式。
11、根據在9步設定的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;
}
最後手機進行關機或者重新開機或者reboot bootloader模式
從上層進入recovery服務
在setting-->備份與重置--->恢複出廠設定--->重置手機--->手機關機--->開機--->進行恢複出廠的操作--->開機流程。
恢複出廠設定其實是擦除使用者資料分區,同時删除緩存區。在系統設定中選擇“恢複出廠設定”選項後, APK 會發出一個廣播:“android.intent.action.MASTER_CLEAR”,接收者是MasterClearReceiver 類,在收到廣播之後會開啟一個新線程:
public void onReceive(final Context context, final Intent intent) {
……
// The reboot call is blocking, so we need to do it on another thread.
Thread thr = new Thread("Reboot") {
@Override
public void run() {
try {
RecoverySystem.rebootWipeUserData(context, shutdown, reason);
Log.wtf(TAG, "Still running after master clear?!");
} catch (IOException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
} catch (SecurityException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
}
}
};
thr.start();
}
線程中調用了\frameworks\base\core\java\android\os\RecoverySystem.java類的rebootWipeUserData方法:
public static void rebootWipeUserData(Context context, boolean shutdown, String reason) throws IOException {
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
throw new SecurityException("Wiping data is not allowed for this user.");
}
final ConditionVariable condition = new ConditionVariable();
Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
android.Manifest.permission.MASTER_CLEAR,
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
condition.open();
}
}, null, 0, null, null);
// Block until the ordered broadcast has completed.
condition.block();
String shutdownArg = null;
if (shutdown) {
shutdownArg = "--shutdown_after";
}
String reasonArg = null;
if (!TextUtils.isEmpty(reason)) {
reasonArg = "--reason=" + sanitizeArg(reason);
}
final String localeArg = "--locale=" + Locale.getDefault().toString();
bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
}
最終調用bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);完成擦除資料的操作
bootCommand()函數分析:
private static void bootCommand(Context context, String... args) throws IOException {
RECOVERY_DIR.mkdirs(); // In case we need it
COMMAND_FILE.delete(); // In case it's not writable
LOG_FILE.delete();
FileWriter command = new FileWriter(COMMAND_FILE);
//将參數逐行寫入command檔案中
try {
for (String arg : args) {
if (!TextUtils.isEmpty(arg)) {
command.write(arg);
command.write("\n");
}
}
} finally {
command.close();
}
// 寫入cache/recovery/command檔案中後,繼續執行reboot的後續操作
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
//這裡執行reboot操作進入recovery模式
pm.reboot(PowerManager.REBOOT_RECOVERY);
throw new IOException("Reboot failed (no permissions?)");
}
這裡進入recovery模式的入口在于: frameworks\base\core\java\android\os\PowerManager.java
public void reboot(String reason) {
try {
mService.reboot(false, reason, true);
} catch (RemoteException e) {
}
}
IPowerManager.aidl檔案中的方法如下:
void reboot(boolean confirm, String reason, boolean wait);
其中對應的實作位于PowerManagerService.java中的内部類BinderService中:
private final class BinderService extends IPowerManager.Stub {
……
@Override // Binder call
public void reboot(boolean confirm, String reason, boolean wait) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null);
if (PowerManager.REBOOT_RECOVERY.equals(reason)) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
}
final long ident = Binder.clearCallingIdentity();
try {
shutdownOrRebootInternal(false, confirm, reason, wait);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
……
}
bootCommand()經過如下調用後最終到調用到本地JNI 接口rebootNative(),就是\frameworks\base\core\jni\android_os_Power.cpp 中的android_os_Power_reboot 函數。
具體的調用流程如下:
該函數繼續調用\system\core\libcutils\android_reboot.c 中的int android_reboot(int cmd, int flags UNUSED, char *arg),該函數根據第一個參數的不同走向不同的分支,
switch (cmd) {
case ANDROID_RB_RESTART:
ret = reboot(RB_AUTOBOOT);
break;
case ANDROID_RB_POWEROFF:
ret = reboot(RB_POWER_OFF);
break;
case ANDROID_RB_RESTART2:
ret = syscall(__NR_reboot,LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, LINUX_REBOOT_CMD_RESTART2, arg);
break;
default:
ret = -1;
}
相關宏定義見\kernel\include\linux\reboot.h。在reboot.h中定義的函數實作在kernel/kernel/sys.c中的函數 SYSCALL_DEFINE4():為了避免誤操作而進入 recovery模式,該函數首先檢測所謂的魔法參數,之後判斷啟動指令,如果是 LINUX_REBOOT_CMD_RESTART2,表示用所給的指令字元串重新開機系統。調用關系如下:
可以看出,雖然經過了層層調用,但始終有一個内容為“recovery”的字元串參數沒有被丢棄過,最後傳給了arch_reset(mode, cmd),最終在這裡使用:
可以看到,當參數為“recovery”時,會給restart_reason 位址處寫入0x77665502。
在代碼裡有:
45: #define RESTART_REASON_ADDR 0x65C
247: restart_reason = MSM_IMEM_BASE + RESTART_REASON_ADDR;
在\kernel\arch\arm\mach-msm\include\mach\msm_iomap-8x60.h 中有
是以restart_reason 的實際實體位址是0x2A05F000+0x65C =0x2A05F65C,在之前講到手機啟動時, check_reboot_mode 函數會檢測SMEM 中的reboot_mode 變量值以判斷進入何種工作方式,該函數中讀取記憶體位址0x2A05F65C,這正是arch_reset 函數寫入restart_reason 的位址。這樣調用pm.reboot(“recovery”) 函數後,最終實作了重新開機後進入recovery 模式。