Linux啟動流程_LK流程_recovery/normal_boot(2.2)
深入,並且廣泛
-沉默犀牛
此篇部落格原部落格來自freebuf,原作者SetRet。原文連結:https://www.freebuf.com/news/135084.html
寫在前面的話
寫這篇文章之前,我只好假定你所知道的跟我一樣淺薄(針對本文這一方面),所以如果你看到一些實在是太小兒科的內容,請你多加擔待,這確實就是我目前的水平,謝謝。
這裡就開始啦!
上一篇部落格分析了aboot_init的一部分工作,總結一下:
顯示獲取分頁大小,然後獲取了device,初始化開始螢幕資訊和全域性的螢幕資訊,獲取序列號並儲存在全域性變數 sn_buf 中,然後檢測啟動方式(一般是檢測按鍵組合),繼而選擇進入fastboot模式啟動 還是非fastboot模式啟動,進入fastboot模式啟動的話,先把註冊了fastboot指令,然後開啟一個usb監聽,對fastboot指令進行解析,這篇文章繼續分析recovery(非fastboot)模式啟動
大致描述recovery/normal boot
(recovery 和 normal 使用的是同一套載入流程,所以放在一起分析,下只稱為recovery boot
ps:這個流程中的程式碼太多了,不全部貼出來,只貼出部分重要程式碼,請配合原始碼享用本文)
- 程式碼中首先會判斷
boot_into_fastboot
變數,如果為0,則進入recovery boot的程式碼部分 - 呼叫
target_is_emmc_boot
來判斷是否是從emmc boot (大多為是,本文只分析是的情況) - 呼叫
emmc_recovery_init
來載入和處理recovery預指令 - 呼叫
boot_linux_from_mmc
1,2兩步中的函式呼叫僅僅根據巨集定義返回數值,無需分析,著重分析emmc_recovery_init
& boot_linux_from_mmc
emmc_recovery_init
做的事情: 載入recovery指令,處理recovery指令(通過usb監聽)
boot_linux_from_mmc
做的事情:啟動模式檢測,讀取 boot_img_hdr, 快取並驗證映象 ,解壓釋放 kernel 和 ramdisk ,解壓釋放 device tree ,呼叫 boot_linux 啟動系統
emmc_recovery_init
直接呼叫_emmc_recovery_init
,如上述,這個函式載入和處理了recovery預指令,下面分為程式碼塊來介紹:
載入recovery指令
int _emmc_recovery_init(void)
{
int update_status = 0;
struct recovery_message *msg;
uint32_t block_size = 0;
block_size = mmc_get_device_blocksize();
msg = (struct recovery_message *)memalign(CACHE_LINE, block_size);
ASSERT(msg);
if(emmc_get_recovery_msg(msg)) //從 misc 分割槽中讀取 recovery 指令
{
if(msg)
free(msg);
return -1;
}
msg->command[sizeof(msg->command)-1] = '\0'; //Ensure termination
if (msg->command[0] != 0 && msg->command[0] != 255) {
dprintf(INFO,"Recovery command: %d %s\n",
sizeof(msg->command), msg->command);
}
...
return 0;
}
整個載入 recovery 命令的過程比較重要的結構是 recovery_message
, 用作儲存讀取到的 recovery 命令,它的結構如下:
/* Recovery Message */
struct recovery_message {
char command[32];
char status[32];
char recovery[1024];
};
通過 emmc_get_recovery_msg
從 misc 分割槽讀取到 recovery 命令就通過這個結構體向後傳遞
處理 recovery 指令
int _emmc_recovery_init(void)
{
...
if (!strcmp(msg->command, "boot-recovery")) {
boot_into_recovery = 1;
}
if (!strcmp("update-radio",msg->command))
{
/* We're now here due to radio update, so check for update status */
int ret = get_boot_info_apps(UPDATE_STATUS, (unsigned int *) &update_status);
if(!ret && (update_status & 0x01))
{
dprintf(INFO,"radio update success\n");
strlcpy(msg->status, "OKAY", sizeof(msg->status));
}
else
{
dprintf(INFO,"radio update failed\n");
strlcpy(msg->status, "failed-update", sizeof(msg->status));
}
boot_into_recovery = 1; // Boot in recovery mode
}
if (!strcmp("reset-device-info",msg->command))
{
reset_device_info();
}
if (!strcmp("root-detect",msg->command))
{
set_device_root();
}
else
goto out;// do nothing
strlcpy(msg->command, "", sizeof(msg->command)); // clearing recovery command
emmc_set_recovery_msg(msg); // send recovery message
out:
if(msg)
free(msg);
return 0;
}
boot-recovery
這條命令處理邏輯是最簡單的,只是將全域性變數boot_into_recovery
設定為 1, 這個變數在後面的載入部分會用到。update-readio
這條命令是檢查基帶升級是否成功,根據狀態設定recovery_message.status,
然後設定boot_into_recovery
為 1。reset-device-info
根據aboot_init
中init部分
的分析,我們知道device_info
的資料結構,這裡是重設device_info.is_tampered
為 0, 並寫入 emmc 中。root-detect
這條命令正好和reset-device-info
相反,這裡會設定device_info.is_tampered
為 1, 也就是說device_info.is_tampered
是手機是否 root 的標誌位。
當以上 4 條命令任意一條執行後,就會清理掉 recovery_message 並重新協會 misc 分割槽。
boot_linux_from_mmc
如上述,這個函式進行了:
- 啟動模式檢測
- 讀取 boot_img_hdr
- 快取並驗證映象
- 解壓釋放 kernel 和 ramdisk
- 解壓釋放 device tree
- 呼叫 boot_linux 啟動系統
1.啟動模式檢測
int boot_linux_from_mmc(void)
{
...
if (check_format_bit())
boot_into_recovery = 1;
if (!boot_into_recovery) {
memset(ffbm_mode_string, '\0', sizeof(ffbm_mode_string));
rcode = get_ffbm(ffbm_mode_string, sizeof(ffbm_mode_string));
if (rcode <= 0) {
boot_into_ffbm = false;
if (rcode < 0)
dprintf(CRITICAL,"failed to get ffbm cookie");
} else
/* TS add for VIGO-390 by haolingjie at 2018/11/16 start */
boot_into_ffbm = false;
/* TS add for VIGO-390 by haolingjie at 2018/11/16 end */
} else
boot_into_ffbm = false;
uhdr = (struct boot_img_hdr *)EMMC_BOOT_IMG_HEADER_ADDR;
if (!memcmp(uhdr->magic, BOOT_MAGIC, BOOT_MAGIC_SIZE)) {
dprintf(INFO, "Unified boot method!\n");
hdr = uhdr;
goto unified_boot;
}
...
return 0;
}
- 通過
check_format_bit
檢查是否進入 recovery
check_format_bit
唯一的作用就是讀取bootselect
分割槽的資訊,然後存放到boot_selection_info
結構體(結構體如下),然後判斷該結構體是否符合條件(各個平臺不同,msm8953上是判斷signature 和 version),若符合則設定全域性標誌位 boot_into_recovery 為 true。
/* bootselect partition format structure */
struct boot_selection_info {
uint32_t signature; // Contains value BOOTSELECT_SIGNATURE defined above
uint32_t version;
uint32_t boot_partition_selection; // Decodes which partitions to boot: 0-Windows,1-Android
uint32_t state_info; // Contains factory and format bit as definded above
};
- 通過
get_ffbm
檢查是否進入 ffbm1 模式, get_ffbm 所完成的任務很簡單,只是讀取 misc 分割槽,並判斷內容是否為 ffbm- 開頭,如果是就將讀取到的資訊儲存到全域性變數 ffbm_mode_string 中,並且設定全域性變數 boot_into_ffbm 為 true。 - 現在會檢查記憶體固定位置
EMMC_BOOT_IMG_HEADER_ADDR
是否和 boot.img 的BOOT_MAGIC
值 “ANDROID!” 相同,如果相同,則直接按照這個記憶體地址來啟動系統,不再從 emmc 中讀取。
啟動模式 檢測完成後,除了直接從記憶體啟動的方式以外,其他方式都需要將需要啟動的 image 從 emmc 中讀取並載入到記憶體中。
2.讀取 boot_img_hdr
這一部分的內容就是從 emmc 讀取 boot_img_hdr
結構,這個結構是 image 頭部結構,包含基礎的載入資訊
- 根據啟動模式獲得需要讀取的分割槽偏移(其中 normal 儲存在 boot 分割槽, recovery 儲存在 recovery 分割槽)
int boot_linux_from_mmc(void)
{
...
if (boot_into_recovery &&
(!partition_multislot_is_supported()))
ptn_name = "recovery"; //啟動模式
else
ptn_name = "boot"; //啟動模式
index = partition_get_index(ptn_name);
ptn = partition_get_offset(index); //分割槽偏移
if(ptn == 0) {
dprintf(CRITICAL, "ERROR: No %s partition found\n", ptn_name);
return -1;
}
return 0;
}
- 進行基礎的
boot_img_hdr
合法性檢查
if (memcmp(hdr->magic, BOOT_MAGIC, BOOT_MAGIC_SIZE)) {
dprintf(CRITICAL, "ERROR: Invalid boot image header\n");
return ERR_INVALID_BOOT_MAGIC;
}
if (hdr->page_size && (hdr->page_size != page_size)) {
if (hdr->page_size > BOOT_IMG_MAX_PAGE_SIZE) {
dprintf(CRITICAL, "ERROR: Invalid page size\n");
return -1;
}
page_size = hdr->page_size;
page_mask = page_size - 1;
}
- 根據
boot_img_hdr
初始化兩個重要的變數(image_addr
和imagesize_actual
)
kernel_actual = ROUND_TO_PAGE(hdr->kernel_size, page_mask);
ramdisk_actual = ROUND_TO_PAGE(hdr->ramdisk_size, page_mask);
second_actual = ROUND_TO_PAGE(hdr->second_size, page_mask);
image_addr = (unsigned char *)target_get_scratch_address();
memcpy(image_addr, (void *)buf, page_size);
...
#if DEVICE_TREE
#ifndef OSVERSION_IN_BOOTIMAGE
dt_size = hdr->dt_size;
#endif
dt_actual = ROUND_TO_PAGE(dt_size, page_mask);
if (UINT_MAX < ((uint64_t)kernel_actual + (uint64_t)ramdisk_actual+ (uint64_t)second_actual + (uint64_t)dt_actual + page_size)) {
dprintf(CRITICAL, "Integer overflow detected in bootimage header fields at %u in %s\n",__LINE__,__FILE__);
return -1;
}
imagesize_actual = (page_size + kernel_actual + ramdisk_actual + second_actual + dt_actual);
#else
if (UINT_MAX < ((uint64_t)kernel_actual + (uint64_t)ramdisk_actual + (uint64_t)second_actual + page_size)) {
dprintf(CRITICAL, "Integer overflow detected in bootimage header fields at %u in %s\n",__LINE__,__FILE__);
return -1;
}
imagesize_actual = (page_size + kernel_actual + ramdisk_actual + second_actual);
#endif
當 image_addr 和 imagesize_actual 確定後就可以進行快取 image 並驗證的步驟
3.快取並驗證映象
這一部分程式碼的作用就是將 image 從 emmc 載入到記憶體中的 image_addr 位置,並且驗證 image 是否合法
- 先呼叫
boot_verifier_init
,初始化對boot/recovery的驗證
void boot_verifier_init()
{
uint32_t boot_state;
/* Check if device unlock */
if(device.is_unlocked) //判斷解鎖bootloader標誌位
{
boot_verify_send_event(DEV_UNLOCK);
boot_verify_print_state();
dprintf(CRITICAL, "Device is unlocked! Skipping verification...\n");
return;
}
else
{
boot_verify_send_event(BOOT_INIT);
}
/* Initialize keystore */
boot_state = boot_verify_keystore_init();
if(boot_state == YELLOW)
{
boot_verify_print_state();
dprintf(CRITICAL, "Keystore verification failed! Continuing anyways...\n");
}
}
a) 可以看到如果手機已經解鎖 bootloader
則不會進行驗證,而是將 boot_state 設定為 ORANGE
狀態
在 android 中存在以下幾種啟動狀態2:
green yellow orange red
b) 然後會在 boot_verify_keystore_init
函式中讀取兩個 key, oem key
和 user key
:
oem key
會編譯到 lk 程式碼中,其位置在 platform/msm_shared/include/oem_keystore.h 檔案中,作用是為了驗證user key
。
user key
儲存在 keystore
分割槽中,作用驗證 boot.img。
-
呼叫
check_aboot_addr_range_overlap
檢查“即將載入到記憶體的” boot/recovery 是否會覆蓋到 aboot 的地址。 -
讀取位於 boot/recovery 尾部的簽名,並通過
verify_signed_bootimg
來驗證簽名是否能夠匹配,通過這裡可以檢查出 boot/recovery 是否被修改。
a)verify_signed_bootimg
會呼叫boot_verify_image
來進行簽名驗證,這個函式的主要作用是是將 boot/recovery 的簽名資料轉化為VERIFIED_BOOT_SIG *
的結構,簽名轉換完成後就通過verify_image_with_sig
來驗證簽名。其中比較重要的引數如下:char* pname
, 即將要驗證的分割槽名稱
VERIFIEDBOOTSIG *sig
, 分割槽所帶的簽名
KEYSTORE *ks
, 驗證所使用的金鑰
整個驗證過程分為以下幾個部分:a.1) 簽名對應的分割槽是否正確,簽名中攜帶的分割槽資訊為以下兩個成員:
分割槽名稱:sig->authattr->target->data
名稱長度:sig->authattr->target->lengtha.2) 檢查 boot/recovery 的大小是否和簽名中儲存的大小資訊相等,大小資訊儲存在以下兩個成員中:
大小資訊:sig->authattr->len->data
資料長度:sig->authattr->len->length這裡需要注意的是 data 是按網路位元組的順序儲存,例如 len 原值為 0xAABBCC 則 data 中實際儲存的值為 0x00CCBBAA。而 len->length 的作用就是指明這個 data 佔了多少個位元組,所以需要轉換為 unsigned int
a.3) 最後一步就是比對 SHA256 的值是否正確:
從keystore
中獲取 rsa 公鑰,ks->mykeybag->mykey->keymaterial。
使用 rsa 解密簽名中攜帶的 SHA256 值,sig->sig->data。
計算傳入的 boot/recovery 的 SHA256 hash 值。
將解密後的 hash 和解密前的 hash 進行對比,如果一致則簽名驗證通過
4.解壓釋放 kernel 和 ramdisk
經過上面部分的載入和驗證,需要 lk 啟動的 boot/recovery 映象已經載入到了記憶體的緩衝區中,但是現在還是完整的一個整體,並沒有分開載入。下面的程式碼就是對每一個部分的程式碼和資料進行分開載入,然後才能進行系統啟動的操作。
- 呼叫
is_gzip_package
檢查 kernel block 是否壓縮 - 為
out_addr
和out_avai_len
賦值 - 呼叫
decompress
函式解壓 kernel - 儲存解壓後的 kernel 頭地址和大小
kernel_start_addr
和kernel_size
- 呼叫
check_aboot_addr_range_overlap
檢查是否越界 - 將 kernel 和 ramdisk 拷貝到
boot_img_hdr
指定的載入地址中
5.解壓釋放 device tree
1.在 boot_img_hdr 中指定了 dtsize
- 首先需要明確 device tree 在 image 中的位置,其位置計算如下
dt_table_offset = ((uint32_t)image_addr + page_size + kernel_actual + ramdisk_actual + second_actual);
table = (struct dt_table*) dt_table_offset;
-
驗證 device tree block 的資料是否合法
2.1 第一點需要驗證的就是 MAGIC 是否為正確
2.2 第二步是檢查 device tree 格式的版本是否支援
2.3 計算並驗證所需要記憶體大小是否正確 -
這裡呼叫
dev_tree_get_entry_info
來實現( device tree 在儲存中實際上是陣列結構,這裡就是遍臨構造出這個 device tree 陣列)
3.1 首先開始遍歷整個陣列,每個陣列成員是一個dt_entry
結構體,獲取到一個dt_entry
都儲存到cur_dt_entry
變數
3.2 然後呼叫platform_dt_absolute_match
儲存到dt_entry_queue
中
(msmid匹配,platformhwid匹配, platformsubtype 匹配 ,ddr size 匹配 ,soc 版本匹配 ,board 版本匹配 ,pmic 版本匹配 )
3.3 通過platform_dt_match_best
來獲取最佳匹配,並且賦值給輸出引數dt_entry_info
中
(比對與硬體的匹配度,找到最佳匹配的dt_entry
然後返回)
3.4 -
(如果獲取到了最佳匹配的 dt_entry)按照載入 kernel 的步驟來載入到記憶體地址
4.1 即按照如下步驟:根據標誌位來解壓資料。 拷貝到boot_img_hdr
指定的記憶體地址。
2.在 boot_img_hdr 中沒有指定了 dtsize
如果沒有專門的 device tree block, 則需要判斷 kernel block 是否附加了 device tree 資訊。整個過程都是通過呼叫函式 dev_tree_appended
函式實現。
-
首先需要獲取 device tree table 的偏移
1.1 偏移有以下兩種情況:
kernel 是經過解壓的,則指定的位置在解壓 kernel 時確定。
kernel 沒有經過壓縮,則偏移在 kernel + 0x2C 的位置上獲取。 -
從 device tree 偏移位置開始到 kernel 尾部的範圍內遍歷 device tree 資料
-
檢查遍歷到的
device tree
的fdt_header
是否通過fdt_check_header
-
通過檢查的
device tree
呼叫dev_tree_compatible
函式檢查相容性,符合條件的新增到連結串列中 -
甚於的步驟和指定了 dtsize 的步驟就基本相同了。
6.呼叫 boot_linux 啟動系統
到這一步 boot/recovery 基本的初始化工作,載入工作就基本完成了,下一步就可以通過 boot_linux
函式來進行啟動了,啟動完成後就會將控制權移交給 linux kernel,android 系統就開始正式運行了。
-
首先進行地址轉換,不過在目前的實現中 PA 巨集是直接返回傳入的地址,所以地址不會被轉換,相當於一個預留的擴充套件介面。
-
呼叫
update_cmdline
更新boot_img_hdr.cmdline
欄位中啟動命令。
更新 cmdline 可以分為以下幾個步驟:
2.1 通過已有的命令和需要新增的命令計算出final_cmdline
需要的長度
2.2 申請儲存 final_cmdline 的 buffer, 並且清零
2.3 拷貝需要的 cmd 命令到 final_cmdline 中 -
呼叫
update_device_tree
更新 device tree 資訊 -
在未解鎖的情況下對 devinfo 分割槽進行防寫(這個分割槽儲存的是 bootloader 的解鎖資訊和驗證資訊,進行防寫避免誤操作)
-
呼叫 kernel 入口點,進入 kernel 的程式碼區域(32 位是直接 call kernel 的入口點,將入口點作為函式呼叫。而 64 位 kernel 則是通過
scm_elexec_call
來進入核心空間。)
到此bootloader的部分就全部結束了,真正進入了kernel程式碼的部分