摘要
Recovery模式指的是一種可以對安卓機內部的資料或系統進行修改的模式(類似于windows PE或DOS),也可以稱之為安卓的恢復模式,在這個所謂的恢復模式下,我們可以刷入新的安卓系統,或者對已有的系統進行備份或升級,也可以在此恢復出廠設定(格式化資料和快取),
1. Recovery相關概念
- Recovery: Recovery模式指的是一種可以對安卓機內部的資料或系統進行修改的模式,也指Android的Recovery磁區
- OTA: Over-the-Air Technology,即空中下載技術,是 Android 系統提供的標準軟體升級方式, 它功能強大,提供了完全升級、增量升級模式,可以通過 SD 卡升級,也可以通過網路升級,不管是哪種方式,都有幾個程序:生成升級包、下載升級包、安裝升級包,
- RecoverySystem:Android系統內部實作的一個工具類,Android應用層操作Recovery模式的一個重要途徑,它提供了幾個重要的API,用于實作OTA包校驗、升級以及恢復出廠設定(格式化資料和快取),
- Main System:主系統模式,即Android正常開機所進入的Android系統
- Bootloader:Bootloader是嵌入式系統在加電后執行的第一段代碼,在它完成CPU和相關硬體的初始化之后,再將作業系統映像或固化的嵌入式應用程式裝在到記憶體中然后跳轉到作業系統所在的空間,啟動作業系統運行,
- BCB:Bootloader Control Block,啟動控制資訊塊,位于misc磁區,從代碼上看,就是一個結構體,
2. Android系統的啟動模式
2.1 Android 各個磁區介紹
一般來說,安卓手機和平板一般包括以下標準內部磁區:
Boot:包含Linux內核和一個最小的root檔案系統(裝載到ramdisk中),用于掛載系統和其他的磁區,并開始Runtime,正如名字所代表的意思(注:boot的意思是啟動),這個磁區使Android設備可以啟動,如果沒有這個磁區,Android設備通常無法啟動到Android系統,
System:這個磁區幾乎包含了除kerner和ramdisk之外的整個android作業系統,包括了用戶界面、和所有預裝的系統應用程式和庫檔案(AOSP中可以獲取到源代碼),在運行的程序中,這個磁區是read-only的,當然,一些Android設備,也允許在remount的情況下,對system磁區進行讀寫, 擦除這個磁區,相當于洗掉整個安卓系統,會導致不能進入Main System, 但不會影響到Recovery,因此,可以通過進入Recovery程式或者bootloader程式中,升級安裝一個新ROM,
Userdata:用戶資料區,用戶安裝的應用程式會把資料保存在這里,包含了用戶的資料:聯系人、短信、設定、用戶安裝的程式,擦除這個磁區,本質上等同于手機恢復出廠設定,也就是手機系統第一次啟動時的狀態,或者是最后一次安裝官方或第三方ROM后的狀態,在Recovery程式中進行的“data/factory reset ”操作就是在擦除這個磁區,正常情況下OTA是不會清除這里的資料的,指定要洗掉資料的除外,
Cache:系統快取區,臨時的保存應用資料(要把資料保存在這里,需要特地的app permission), OTA的升級包也可以保存在這里,OTA升級程序可能會清楚這個磁區的資料,一般來講,Android差分包升級也需要依賴此磁區存放一些中間檔案,
Recovery:包括了一個完整Linux內核和一些特殊的recovery binary,可以讀取升級檔案用這些檔案來更新其他的磁區,
Misc:一個非常小的磁區,4 MB左右,recovery用這個磁區來保存一些關于升級的資訊,應對升級程序中的設備掉電重啟的狀況,Bootloader啟動的時候,會讀取這個磁區里面的資訊,以決定系統是否進Recovery System 或 Main System,
以上幾個磁區是Google官方的標準,對于第三方Android設備廠商來講,磁區的情況可能稍微不一樣,比如Rockchip平臺,還增加了user磁區、kernel磁區和backup磁區,其中:
kernel:顧名思義,是存放kernel.img鏡像的,在boot磁區里面的kernel內核鏡像損壞的情況下(比如flash損壞),bootloader會嘗試加載kerner磁區里面的內核鏡像,
backup:存放整個系統鏡像(update.img), 可用于恢復設備到出廠ROM,
user: 用戶磁區,也就是平時我們所說的內置sdcard,另外還有外置的sdcard磁區,用于存放用戶相片、視頻、檔案、ROM安裝包等,
2.2 Android的啟動模式
一般來講,Android有三種啟動模式:Fastboot模式,Recovery System 以及Main System,
- Fastboot:在這種模式下,可以修改手機的硬體,并且允許我們發送一些命令給Bootloader,如使用電腦刷機,則需要進入fastboot模式,通過電腦執行命令將系統鏡像刷到通過USB刷到Android設備中中,
- Recovery:Recovery是一個小型的作業系統,并且會加載部分檔案系統,這樣才能從sdcard中讀取升級包,
- Main System: 即我們平時正常開機后所使用的手機作業系統模式
首先說一下,正常啟動和進入Recovery的區別,一圖以概之:
2.3 如何進入Recovery模式
一般來講,進入recovery有兩種方式,一種是通過組合鍵進入recovery,按鍵指引的方式,各個Android平臺都不一樣,比如三星的手機是在關機狀態下同時按住【音量上】、【HOME鍵】、【電源鍵】,等待螢屏亮起后即可放開,進入Recovery模式,而Rockchip的機頂盒,則是使用按【Reset鍵】加【電源鍵】開機的方式,形式不一,
另一種,則是使用系統命令啟動到Recovery模式的,這對絕大部分Android設備是通用的:
reboot recovery
3. Recovery升級原理
3.1 應用層升級流程
在Android應用層部分,OTA系統升級流程,大概的流程圖如下所示:
以上部分,只介紹了應用層層面的 ota升級包的下載、校驗以及最后的發起安裝程序,在這里,重要講解進入Recovery模式后,OTA包的升級程序,
首先,在應用層下載升級包后,會呼叫RecoverySystem.installPackage(Context context, File packageFile)函式來發起安裝程序,這個程序主要的原理,實際上只是往 /cache/recovery/command 寫入ota升級包存放路徑,然后重啟到recovery模式,僅此而已,
public static void installPackage(Context context, File packageFile)
throws IOException {
String filename = packageFile.getCanonicalPath();
Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
final String filenameArg = "--update_package=" + filename;
final String localeArg = "--locale=" + Locale.getDefault().toString();
bootCommand(context, filenameArg, localeArg);
}
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);
try {
for (String arg : args) {
if (!TextUtils.isEmpty(arg)) {
command.write(arg);
command.write("\n");
}
}
} finally {
command.close();
}
// Having written the command file, go ahead and reboot
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
pm.reboot(PowerManager.REBOOT_RECOVERY);
throw new IOException("Reboot failed (no permissions?)");
}
因此,實質上等同于以下命令:
echo -e "--update_package=/mnt/sdcard/ota/update.zip" > /cache/recovery/command
reboot recovery
3.2 OTA升級包的目錄結構
OTA升級包的目錄結構大致如下所示:
|----boot.img
|----system/
|----recovery/
|----recovery-from-boot.p
|----etc/
`|----install-recovery.sh
|---META-INF/
|CERT.RSA
|CERT.SF
|MANIFEST.MF
|----com/
|----google/
|----android/
|----update-binary
|----updater-script
|----android/
|----metadata
其中:
- boot.img 是更新boot磁區所需要的鏡像檔案,這個boot.img主要包括kernel、ramdisk,
- system/目錄的內容在升級后會放在系統的system磁區,主要是系統app,library和binary二進制檔案
- update-binary是一個二進制檔案,相當于一個腳本解釋器,能夠識別updater-script中描述的操作,
- updater-script:此檔案是一個腳本檔案,具體描述了更新程序,
- metadata檔案是描述設備資訊及環境變數的元資料,主要包括一些編譯選項,簽名公鑰,時間戳以及設備型號等,
- 我們還可以在包中添加userdata目錄,來更新系統中的用戶資料部分,這部分內容在更新后會存放在系統的/data目錄下,
- update.zip包的簽名:update.zip更新包在制作完成后需要對其簽名,否則在升級時會出現認證失敗的錯誤提示,而且簽名要使用和目標板一致的加密公鑰,默認的加密公鑰及加密需要的三個檔案在Android原始碼編譯后生成的具體路徑為:
out/host/linux-x86/framework/signapk.jar
build/target/product/security/testkey.x509.pem
build/target/product/security/testkey.pk8
-
MANIFEST.MF:這個manifest檔案定義了與包的組成結構相關的資料,類似Android應用的mainfest.xml檔案,
-
CERT.RSA:與簽名檔案相關聯的簽名程式塊檔案,它存盤了用于簽名JAR檔案的公共簽名,
-
CERT.SF:這是JAR檔案的簽名檔案,其中前綴CERT代表簽名者,
3.3 Recovery模式下的OTA升級流程
進入Recovery模式之后,便開始對下載的升級包進行升級,整體的流程圖如下所示:
BCB(Bootloader與Recovery通過BCB(Bootloader Control Block)通信)
這里,詳解介紹一下升級流程中的各個模塊,
1. get_args(&argc, &argv)
get_args的原理流程圖如下所示:
get_args()函式的主要作用是獲取系統的啟動引數,并回寫到bootloader control block(BCB)塊中,如果系統在啟動recovery時已經傳遞了啟動引數,那么這個函式只是把啟動引數的內容復制到函式的引數boot物件中,否則函式會首先通過get_bootloader_message()函式從/misc磁區的BCB中獲取命令字串來構建啟動引數,如果/misc磁區下沒有內容,則會嘗試決議/cache/recovery/command檔案并讀取檔案的內容來建立啟動引數,
接著,會把啟動引數的資訊通過set_bootloader_message()函式又保存到了BCB塊中,這樣做的目的是防止一旦升級或擦除資料的程序中發生崩潰或不正常斷電,下次重啟,Bootloader會依據BCB的指示,引導進入Recovery模式,從/misc磁區中讀取更新的命令,繼續進行更新操作,因此,可以說是一種掉電保護機制,
get_args具體的流程如下圖所示:
get_args函式核心代碼如下:
static void get_args(int *argc, char ***argv) {
struct bootloader_message boot;
memset(&boot, 0, sizeof(boot));
//決議BCB模塊
get_bootloader_message(&boot); // this may fail, leaving a zeroed structure
......
// --- if that doesn't work, try the command file
if (*argc <= 1) {
FILE *fp = fopen_path(COMMAND_FILE, "r");//COMMAND_FILE指/cache/recovery/command
if (fp != NULL) {
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;
(*argv)[*argc] = strdup(strtok(buf, "\r\n")); // Strip newline.
}
check_and_fclose(fp, COMMAND_FILE);
LOGI("Got arguments from %s\n", COMMAND_FILE);
}
}
......
set_bootloader_message(&boot); //回寫BCB
這里需要說一下“BCB”,即bootloader control block, 中文可以呼之為“啟動控制模資訊塊”**,位于/misc磁區,從代碼上看,就是一個struct 結構體 :
struct bootloader_message {
char command[32];
char status[32];
char recovery[1024];
};
bootloader_message 結構體包含三個欄位,具體含義如下:
command 欄位中存盤的是命令,它有以下幾個可能值:
- boot-recovery:系統將啟動進入Recovery模式
- update-radia 或者 update-hboot:系統將啟動進入更新firmware的模式,這個更新程序由bootloader完成
- NULL:空值,系統將啟動進入Main System主系統,正常啟動,
status 欄位存盤的是更新的結果,更新結束后,由Recovery或者Bootloader將更新結果寫入到這個欄位中,
recovery 欄位存放的是recovry模塊的啟動引數,一般包括升級包路徑,其存盤結構如下:第一行存放字串“recovery”,第二行存放路徑資訊“–update_package=/mnt/sdcard/update.zip”等, 因此,引數之間是以“\n”分割的,
2. update_package
ota升級包的存放路徑,從BCB或者/cache/recovery/command里面決議得到的,升級包一般下載后存放在cache或sdcard磁區,當然,也有一些是存放到U盤之類的外接存盤設備中的,一般賦值格式如下:
--update_package=/mnt/sdcard/update.zip 或 --update_package=CACHE:update.zip
3. int install_package (const char path, int wipe_cache, const char* install_file)
int install_package(const char* path, int* wipe_cache, const char* install_file)
{
//install_file 為 /cache/recovery/last_install
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 = 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;
}
4. static int really_install_package(const char path, int wipe_cache)
really_install_package函式在install_package函式中被呼叫,函式的主要作用是呼叫ensure_path_mounted確保升級包所在的磁區已經掛載,另外,還會對升級包進行一系列的校驗,在具體升級時,對update.zip包檢查時大致會分三步:
-
檢驗SF檔案與RSA檔案是否匹配;
-
檢驗MANIFEST.MF與簽名檔案中的digest是否一致;
-
檢驗包中的檔案與MANIFEST中所描述的是否一致
通過校驗后,呼叫try_update_binary函式去實作真正的升級,
5. static int try_update_binary(const char path, ZipArchive zip, int wipe_cache)
try_update_binary是真正實作對升級包進行升級的函式:
static int try_update_binary(const char *path, ZipArchive *zip, int* wipe_cache) {
const ZipEntry* binary_entry = mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);
......
const char* binary = "/tmp/update_binary";
unlink(binary);
int fd = creat(binary, 0755);
.....
//將升級包里面的update_binary解壓到/tmp/update_binary
bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd);
close(fd);
mzCloseZipArchive(zip);
......
int pipefd[2];
pipe(pipefd);
const char** args = (const char**)malloc(sizeof(char*) * 5);
args[0] = binary; //update_binary存放路徑
args[1] = EXPAND(RECOVERY_API_VERSION); // Recovery版本號
char* temp = (char*)malloc(10);
sprintf(temp, "%d", pipefd[1]);
args[2] = temp;
args[3] = (char*)path; //升級包存放路徑
args[4] = NULL;
pid_t pid = fork();//fork一個子行程
if (pid == 0) {
close(pipefd[0]);
//子行程呼叫update-binary執行升級操作
execv(binary, (char* const*)args);
fprintf(stdout, "E:Can't run %s (%s)\n", binary, strerror(errno));
_exit(-1);
}
......
int status;
waitpid(pid, &status, 0);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
//安裝失敗,回傳INSTALL_ERROR
return INSTALL_ERROR;
}
//安裝成功,回傳INSTALL_SUCCESS
return INSTALL_SUCCESS;
}
總的來說,try_update_binary主要做了以下幾個操作:
(1)mzOpenZipArchive():打開升級包,并將相關的資訊拷貝到一個臨時的ZipArchinve變數中,注意這一步并未對我們的update.zip包解壓,
(2)mzExtractZipEntryToFile(): 解壓升級包特定檔案,將升級包里面的META-INF/com/google/android/update-binary 解壓到記憶體檔案系統的/tmp/update_binary中,
(3)fork創建一個子行程 , 使用系統呼叫函式execv( ) 去執行/tmp/update-binary程式,
(4)update-binary: 這個是Recovery OTA升級的核心程式,是一個二進制檔案,實作代碼位于系統原始碼bootable/recovery/updater,其實質是相當于一個腳本解釋器,能夠識別updater-script中描述的操作并執行,
(5)updater-script:updater-script是我們升級時所具體使用到的腳本檔案,具體描述了更新程序,它主要用以控制升級流程的主要邏輯,具體位置位于升級包中/META-INF/com/google/android/update-script,在我們制作升級包的時候產生,在升級的時候,由update_binary程式從升級包里面解壓到記憶體檔案系統的/tmp/update_script中,并按照update_script里面的命令,對系統進行升級,比如,一個完整包升級的update_script的內容大致如下所示:
assert(getprop("ro.product.device") == "rk31sdk" ||
getprop("ro.build.product") == "rk301dk");
show_progress(0.500000, 0);
format("ext4", "EMMC", "/dev/block/mtd/by-name/system", "0", "/system");
mount("ext4", "EMMC", "/dev/block/mtd/by-name/system", "/system");
package_extract_dir("recovery", "/system");
package_extract_dir("system", "/system");
symlink("Roboto-Bold.ttf", "/system/fonts/DroidSans-Bold.ttf");
symlink("mksh", "/system/bin/sh");
......
set_perm_recursive(0, 0, 0755, 0644, "/system");
set_perm_recursive(0, 2000, 0755, 0755, "/system/bin");
......
set_perm(0, 0, 06755, "/system/xbin/su");
set_perm(0, 0, 06755, "/system/xbin/tcpdump");
show_progress(0.200000, 0);
show_progress(0.200000, 10);
write_raw_image(package_extract_file("boot.img"), "boot");
show_progress(0.100000, 0);
clear_misc_command();
unmount("/system");
update_script常用的命令如下:
因此,根據上面的升級腳本,可以知道,升級包的大致升級流程如下:
- 判斷是不是升級包是否適用于該設備,如果不適用,則停止升級,否則繼續,
- 顯示進度條
- 格式化system磁區
- 掛載system磁區
- 將ota升級包里面的system、recovery目錄解壓到system分區
- 建立一些軟鏈接,升級程序需要用到
- 設定部分檔案權限
- 將升級包里面的boot.img寫入到/boot磁區
- 清空misc磁區,即BCB塊置為NULL
- 卸載system磁區
6. wipe data/cache
main函式,在執行完install_package后,會根據傳入的wipe_data/wipe_cache,決定是否執行/data和/cache磁區的清空操作,
7. prompt_and_wait
這個函式的作用就是一直在等待用戶輸入,是一個不斷的回圈,可以選擇Recovery模式下的一些選項進行操作,包括恢復出廠設定和重啟等,如果升級失敗, prompt_and_wait會顯示錯誤,并等待用戶回應,
8. finish_recovery
OTA升級成功,清空misc磁區(BCB置零),并將保存到記憶體系統的升級日志/tmp/recovery.log保存到/cache/recovery/last_log,重啟設備進入Main System,升級完成,
9. install-recovery.sh
從上面的流程中,可以知道,Recovery模式下的OTA升級成功,只是更新了/system和/boot兩個最核心的磁區,而本身用來升級的Recovery自身并沒有在那個時候得到更新,Recovery磁區的更新,是在重啟進入主系統的時候,由install-recovery.sh來更新的,這樣可以保證,即使升級失敗,Recovery模式也不會受到影響,仍然可以手動進入Recovery模式執行升級或擦除資料操作,
在Recovery升級的時候,有一句:
package_extract_dir("recovery", "/system");
這條命令就是將升級包里面的recovery目錄的內容,解壓到/system磁區
recovery目錄下的檔案,主要有install-recovery.sh和 recovery-from-boot.p,目錄結構如下所示:
├── bin
│ └── install-recovery.sh
└── recovery-from-boot.p
其中:
- recovery-from-boot.p 是boot.img和recovery.img的補丁(patch)
- install-recovery.sh 則是來用安裝recovery-from-boot.p的升級腳本, 主要是利用android系統的 applypatch 工具來打補丁,
至此,一個完整的OTA包升級就正式完成了!
4. Bootloader、BCB、Recovery與Main System之間的互動
首先,通過前面的介紹,可以知道, Recovery System與Main System的互動,主要是通過/cache磁區下的檔案進行資訊互動的,具體如下:
其中,command的值一般有以下一個或多個:
其次,Bootloader與Recovery和Main System之間也是存在互動的: Bootloader會通過決議BCB模塊,決定啟動系統到Recovery或Main System,而Recovery或Main System也能夠操作BCB,進而影響到Bootloader的行為,
當Main System系統關鍵行程崩潰太多次的時候,系統還會自發啟動進入到Recovery模式,
另外,部分平臺的Android設備,在Recovery模式下,也能夠對Bootloader進行升級,
Bootloader、BCB、Recovery與Main System四者相互影響,又獨立作業,它們之間斬不斷理還亂的關系,可以以下圖概括之:
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/40767.html
標籤:嵌入式
