1. 程式人生 > >Linux啟動流程_LK流程_recovery/normal_boot(2.2)

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:這個流程中的程式碼太多了,不全部貼出來,只貼出部分重要程式碼,請配合原始碼享用本文)

  1. 程式碼中首先會判斷boot_into_fastboot變數,如果為0,則進入recovery boot的程式碼部分
  2. 呼叫target_is_emmc_boot來判斷是否是從emmc boot (大多為是,本文只分析是的情況)
  3. 呼叫emmc_recovery_init 來載入和處理recovery預指令
  4. 呼叫boot_linux_from_mmc
    來進行:啟動模式檢測 ,讀取 boot_img_hdr ,快取並驗證映象 ,解壓釋放 kernel 和 ramdisk ,解壓釋放 device tree ,呼叫 boot_linux 啟動系統

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;
}
  1. boot-recovery這條命令處理邏輯是最簡單的,只是將全域性變數 boot_into_recovery 設定為 1, 這個變數在後面的載入部分會用到。
  2. update-readio這條命令是檢查基帶升級是否成功,根據狀態設定recovery_message.status, 然後設定 boot_into_recovery 為 1。
  3. reset-device-info根據 aboot_initinit部分的分析,我們知道 device_info 的資料結構,這裡是重設 device_info.is_tampered 為 0, 並寫入 emmc 中。
  4. root-detect這條命令正好和reset-device-info相反,這裡會設定device_info.is_tampered為 1, 也就是說 device_info.is_tampered是手機是否 root 的標誌位。

當以上 4 條命令任意一條執行後,就會清理掉 recovery_message 並重新協會 misc 分割槽。


boot_linux_from_mmc

如上述,這個函式進行了:

  1. 啟動模式檢測
  2. 讀取 boot_img_hdr
  3. 快取並驗證映象
  4. 解壓釋放 kernel 和 ramdisk
  5. 解壓釋放 device tree
  6. 呼叫 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;
}
  1. 通過 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
};
  1. 通過 get_ffbm 檢查是否進入 ffbm1 模式, get_ffbm 所完成的任務很簡單,只是讀取 misc 分割槽,並判斷內容是否為 ffbm- 開頭,如果是就將讀取到的資訊儲存到全域性變數 ffbm_mode_string 中,並且設定全域性變數 boot_into_ffbm 為 true。
  2. 現在會檢查記憶體固定位置 EMMC_BOOT_IMG_HEADER_ADDR 是否和 boot.img 的 BOOT_MAGIC 值 “ANDROID!” 相同,如果相同,則直接按照這個記憶體地址來啟動系統,不再從 emmc 中讀取。

啟動模式 檢測完成後,除了直接從記憶體啟動的方式以外,其他方式都需要將需要啟動的 image 從 emmc 中讀取並載入到記憶體中。


2.讀取 boot_img_hdr

這一部分的內容就是從 emmc 讀取 boot_img_hdr 結構,這個結構是 image 頭部結構,包含基礎的載入資訊

  1. 根據啟動模式獲得需要讀取的分割槽偏移(其中 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;
}
  1. 進行基礎的 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;
	}
  1. 根據 boot_img_hdr 初始化兩個重要的變數(image_addrimagesize_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 是否合法

  1. 先呼叫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 keyuser key:
oem key會編譯到 lk 程式碼中,其位置在 platform/msm_shared/include/oem_keystore.h 檔案中,作用是為了驗證user key
user key儲存在 keystore 分割槽中,作用驗證 boot.img。

  1. 呼叫check_aboot_addr_range_overlap檢查“即將載入到記憶體的” boot/recovery 是否會覆蓋到 aboot 的地址。

  2. 讀取位於 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->length

    a.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 映象已經載入到了記憶體的緩衝區中,但是現在還是完整的一個整體,並沒有分開載入。下面的程式碼就是對每一個部分的程式碼和資料進行分開載入,然後才能進行系統啟動的操作。

  1. 呼叫 is_gzip_package 檢查 kernel block 是否壓縮
  2. out_addrout_avai_len賦值
  3. 呼叫 decompress 函式解壓 kernel
  4. 儲存解壓後的 kernel 頭地址和大小 kernel_start_addrkernel_size
  5. 呼叫check_aboot_addr_range_overlap 檢查是否越界
  6. 將 kernel 和 ramdisk 拷貝到boot_img_hdr 指定的載入地址中

5.解壓釋放 device tree

1.在 boot_img_hdr 中指定了 dtsize
  1. 首先需要明確 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;
  1. 驗證 device tree block 的資料是否合法
    2.1 第一點需要驗證的就是 MAGIC 是否為正確
    2.2 第二步是檢查 device tree 格式的版本是否支援
    2.3 計算並驗證所需要記憶體大小是否正確

  2. 這裡呼叫 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

  3. (如果獲取到了最佳匹配的 dt_entry)按照載入 kernel 的步驟來載入到記憶體地址
    4.1 即按照如下步驟:根據標誌位來解壓資料。 拷貝到 boot_img_hdr指定的記憶體地址。

2.在 boot_img_hdr 中沒有指定了 dtsize

如果沒有專門的 device tree block, 則需要判斷 kernel block 是否附加了 device tree 資訊。整個過程都是通過呼叫函式 dev_tree_appended 函式實現。

  1. 首先需要獲取 device tree table 的偏移
    1.1 偏移有以下兩種情況:
    kernel 是經過解壓的,則指定的位置在解壓 kernel 時確定。
    kernel 沒有經過壓縮,則偏移在 kernel + 0x2C 的位置上獲取。

  2. 從 device tree 偏移位置開始到 kernel 尾部的範圍內遍歷 device tree 資料

  3. 檢查遍歷到的 device treefdt_header是否通過fdt_check_header

  4. 通過檢查的 device tree呼叫 dev_tree_compatible函式檢查相容性,符合條件的新增到連結串列中

  5. 甚於的步驟和指定了 dtsize 的步驟就基本相同了。


6.呼叫 boot_linux 啟動系統

到這一步 boot/recovery 基本的初始化工作,載入工作就基本完成了,下一步就可以通過 boot_linux 函式來進行啟動了,啟動完成後就會將控制權移交給 linux kernel,android 系統就開始正式運行了。

  1. 首先進行地址轉換,不過在目前的實現中 PA 巨集是直接返回傳入的地址,所以地址不會被轉換,相當於一個預留的擴充套件介面。

  2. 呼叫 update_cmdline更新boot_img_hdr.cmdline 欄位中啟動命令。
    更新 cmdline 可以分為以下幾個步驟:
    2.1 通過已有的命令和需要新增的命令計算出final_cmdline 需要的長度
    2.2 申請儲存 final_cmdline 的 buffer, 並且清零
    2.3 拷貝需要的 cmd 命令到 final_cmdline 中

  3. 呼叫 update_device_tree更新 device tree 資訊

  4. 在未解鎖的情況下對 devinfo 分割槽進行防寫(這個分割槽儲存的是 bootloader 的解鎖資訊和驗證資訊,進行防寫避免誤操作)

  5. 呼叫 kernel 入口點,進入 kernel 的程式碼區域(32 位是直接 call kernel 的入口點,將入口點作為函式呼叫。而 64 位 kernel 則是通過 scm_elexec_call來進入核心空間。)


到此bootloader的部分就全部結束了,真正進入了kernel程式碼的部分