深入,并且广泛
-沉默犀牛
此篇博客原博客来自freebuf,原作者SetRet。原文链接:https://www.freebuf.com/news/135084.html
写这篇文章之前,我只好假定你所知道的跟我一样浅薄(针对本文这一方面),所以如果你看到一些实在是太小儿科的内容,请你多加担待,这确实就是我目前的水平,谢谢。
上一篇博客分析了aboot_init的一部分工作,总结一下:
显示获取分页大小,然后获取了device,初始化开始屏幕信息和全局的屏幕信息,获取序列号并保存在全局变量 sn_buf 中,然后检测启动方式(一般是检测按键组合),继而选择进入fastboot模式启动 还是非fastboot模式启动,进入fastboot模式启动的话,先把注册了fastboot指令,然后开启一个usb监听,对fastboot指令进行解析,这篇文章继续分析recovery(非fastboot)模式启动
(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
来进行:启动模式检测 ,读取 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
,如上述,这个函数加载和处理了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 命令就通过这个结构体向后传递
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 分区。
如上述,这个函数进行了:
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
检查是否进入 recoverycheck_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 中读取并加载到内存中。
这一部分的内容就是从 emmc 读取 boot_img_hdr
结构,这个结构是 image 头部结构,包含基础的加载信息
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 并验证的步骤
这一部分代码的作用就是将 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->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 进行对比,如果一致则签名验证通过
经过上面部分的加载和验证,需要 lk 启动的 boot/recovery 镜像已经加载到了内存的缓冲区中,但是现在还是完整的一个整体,并没有分开加载。下面的代码就是对每一个部分的代码和数据进行分开加载,然后才能进行系统启动的操作。
is_gzip_package
检查 kernel block 是否压缩out_addr
和out_avai_len
赋值decompress
函数解压 kernelkernel_start_addr
和 kernel_size
check_aboot_addr_range_overlap
检查是否越界boot_img_hdr
指定的加载地址中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
指定的内存地址。
如果没有专门的 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 的步骤就基本相同了。
到这一步 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代码的部分