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代码的部分

你可能感兴趣的:(Linux,bootloader)