基于Android13的系统启动流程分析(三)之FirstStageMain阶段

Android13系统启动阶段大致分为FirstStageMain阶段和SecondStageMain,此章主要讲FirstStageMain阶段
(若分析有误敬请指教)

本章讲解的方向和你将收获的知识:

  1. 用户空间进程的调用流程
  2. 当进程挂掉后该如何处理
  3. 何时挂载上的基本文件系统和文件系统小知识
  4. FirstStageMain阶段会挂载上什么分区,会创建哪些设备节点

一. Android系统启动基本介绍

init进程是Android系统中用户空间的第一个进程,作为第一个进程,它被赋予了很多极其重要的工作职责,比如创建zygote(孵化器)和属性服务等。init进程是由多个源文件共同组成的,这些文件位于源码目录system/core/init

1. Bootloader 引导

当按下设备电源键时,最先运行的就是 bootloader(固化在ROM的程序),bootloader 的主要作用就是硬件设备(如 CPU、flash、内存)的初始化并加载到RAM,通过建立内存空间映射,为装载 Linux 内核做好准备,。如果 bootloader 在运行期间,按下预定义的组合按键,可以进入系统fastboot模式 或者 Receiver 模式。

2. 装载和启动 Linux 内核

在编译完AOSP时会生成boot.img或者boot_debug.img,该镜像就是 Linux 内核和根文件系统,bootloader 会把该镜像装载到内存中,然后 linux 内核会执行整个系统的初始化,完成后装载根文件系统,最后启动 init 进程。

3. 启动 Init 进程

Linux 内核加载完毕后,会从kernel内核层调用到用户空间层,则会首先启动 init 进程,init 进程是系统的第一个进程,在 init 进程的启动过程中会解析最主要的 init.rc 脚本,根据 init.rc 文件的描述,系统会创建文件及目录以及权限的赋予,初始化属性和启动 Android 系统重要的守护进程

4. 启动 ServiceManager

ServiceManager 由 init 进程启动。它的主要作用是管理 Binder 服务,service 服务的注册和查找,如 AMS、PMS, 都是通过 ServiceManger 来管理。

5. 启动 MediaServer

MediaServer 是由 init 进程启动,它包含了一些多媒体 binder 服务,包括 CameraService、MediaPlayerService、AudioPolicyService 等等

onrestart restart audioserver
onrestart restart cameraserver
onrestart restart media
onrestart restart media.tuner
onrestart restart netd
onrestart restart wificond

6. 启动 Zygote 进程

init 进程初始化结束后,会启动 Zygote 进程。在 Android 系统中所有的应用程序进程和系统服务进程都是通过Zygote 进程 fork 出来的。预装载系统的资源文件,所有从 Zygote 进程 fork 出的子进程都会共享这些资源,节省了资源加载的时间,提高的应用的启动速度。Zygote 启动结束后也会变为守护进程,负责响应启动 APK 的请求。

7. 启动 SystemServer

SystemServer 是跟随Zygote创建出来的第一个子进程,同时也是整个 Android 系统的核心。在系统中运行的大部分系统服务都是有 SystemServer 创建,接着会启动 AMS、WMS、PMS 等。阅读过源码可以发现大部分服务会继承自systemServer

PD2183:/ $ ps --pid 1                                                                                                                                                             
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME                       
root             1     0 13133644  8264 0                   0 S init

init进程由init_task进程fork而来,在kernel初始化完成后init_task便化身为idle进程,可以看到init的进程pid为:1
而init_task进程是Linux中第一个进程,也即0号进程(PID为0),这里不进一步分析linux内核层

二. FirstStageMain阶段分析

镜像 内容
ramdisk.img 系统root根目录,挂载点 /
boot.img 包含了kernel + ramdisk.img
super.img 在Android R版本开始引入的动态分区结构,目的是为了解决system和vender等分区size不能动态调整的问题
super.img 包含了system_a(b).img,vendor_a(b).img,product_a(b).img,拿到super.img可以进行解包拿到子镜像
userdata.img 挂载点/data,包含了用户的所有数据,例如应用数据等

针对super分区,在开机init的first stage第一阶段运行期间,会解析并验证metadata元数据并创建虚拟block设备来表示每个子分区,super分区头部信息就是metadata元数据,用于映射(mapping)出虚拟block设备,这里盗用一张大佬的图来看一下super结构

super镜像super分区除了包含system\product\vendor,在头部信息包含了描述分区布局的metadata,系统加载动态分区时读取metadata,对其进行解析。

Metadata中的信息会被转换成device mapper中的映射表Mapping Table,基于这个映射表,super分区对应设备/dev/block/by-name/super的不同部分被映射成多个虚拟设备,如/dev/block/mapper/system_a/dev/block/mapper/vendor_a

1. 用户空间层main.cpp

using namespace android::init;

int main(int argc/*1*/, char** argv/*init*/) {
    
     // 内核层传过来的,argc:1,argv:init
#if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
#elif __has_feature(hwaddress_sanitizer)
    __hwasan_set_error_report_callback(AsanReportCallback);
#endif
    // Boost prio which will be restored later
    setpriority(PRIO_PROCESS, 0, -20); // 设置进程最高优先级 -20最高,20最低
    if (!strcmp(basename(argv[0]), "ueventd")) {
    
    
        return ueventd_main(argc, argv);
    }

    if (argc > 1) {
    
    
        if (!strcmp(argv[1], "subcontext")) {
    
    
            android::base::InitLogging(argv, &android::base::KernelLogger);
            const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();

            return SubcontextMain(argc, argv, &function_map);
        }

        if (!strcmp(argv[1], "selinux_setup")) {
    
    
            return SetupSelinux(argv);
        }

        if (!strcmp(argv[1], "second_stage")) {
    
    
            return SecondStageMain(argc, argv);
        }
    }

    return FirstStageMain(argc, argv); //第一阶段执行
}

内核层传过来的参数:argc:1,argv:init,所以第一阶段仍然是调用return FirstStageMain(argc, argv); 这里不详细介绍subcontext,ueventd,只关注FirstStageMain阶段即可,调用顺序如下

2. FirstStageMain(int argc, char** argv)解析

该阶段所挂载的文件系统都属于ramdisk,运行在虚拟内存上

int FirstStageMain(int argc, char** argv) {
    
    
    // init阶段的启动引导加载程序(bootLoader),若发生异常重启也会再次执行,主要处理init || fork的子进程进程异常行为
    // init信号处理器,调试版本当init crash,默认重启到 bootLoader
    if (REBOOT_BOOTLOADER_ON_PANIC) {
    
    
        InstallRebootSignalHandlers();// setup1
    }
    // 用来设置创建目录或文件时所应该赋予权限的掩码
    // Linux中,文件默认最大权限是666,目录最大权限是777,当创建目录时,假设掩码为022,那赋予它的权限为(777 & ~022)= 755
    // 在执行init第一阶段时,先执行umask(0),使创建的目录或文件的默认权限为最高
    umask(0);

    // 第一次执行时清除环境变量,reset path
    CHECKCALL(clearenv());
    CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
    // setup 2
    // 设置linux最基本的文件系统并且挂载到 / 目录(init ram disk)上,
    // 并给0755权限(即用户具有读/写/执行权限,组用户和其它用户具有读写权限),后续会通过rc文件处理一些分区权限和进程
    CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755")); //将/dev设置为tmpfs并挂载,设置0755权限,tmpfs是在内存上建立的文件系统(Filesystem)
    CHECKCALL(mkdir("/dev/pts", 0755));//tmpfs文件系统类型
    CHECKCALL(mkdir("/dev/socket", 0755));
    CHECKCALL(mkdir("/dev/dm-user", 0755));//tmpfs文件系统类型
    CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
    // setup 3
 	CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR
    // 修改 「保存操作系统的启动参数」 的权限:0440,
    // 修改权限的目的是为了 不要将原始bootConfig暴露给非特权进程,部分文件系统只能是0440权限,如果修改权限则无法读取和操作
    // /proc/cmdline中保存bootloader 启动linux kernel 时 的参数
    CHECKCALL(chmod("/proc/cmdline", 0440));
    std::string cmdline;
    // 读取操作系统的启动参数
    android::base::ReadFileToString("/proc/cmdline", &cmdline);
    // 修改权限的目的是为了 不要将原始bootConfig暴露给非特权进程
    // 部分文件系统只能是0440权限,如果修改权限则无法读取和操作
    chmod("/proc/bootconfig", 0440);
    std::string bootconfig;
    // 读取系统启动参数配置
    android::base::ReadFileToString("/proc/bootconfig", &bootconfig);
    gid_t groups[] = {
    
    AID_READPROC};
    CHECKCALL(setgroups(arraysize(groups), groups));
    // setup 4
    // 挂载/sys内核,并设置为sysfs文件系统类型,sysfs是一个伪文件系统。
    // 不代表真实的物理设备,在linux内核中,sysfs文件系统将长期存在于RAM中
    // sysfs文件系统将每个设备抽象成文件,挂载sysfs文件系统在sys目录,用来访问内核信息
    CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
    CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));
    CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));
    if constexpr (WORLD_WRITABLE_KMSG) {
    
    
        CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
    }
    // 文件系统:/dev/random和 /dev/urandom是 Linux 上的字符设备文件,它们是随机数生成器,为系统提供随机数
    CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
    CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));
    
    // 创建日志系统的串口log(伪终端),这是日志包装器所需要的,它在ueventd运行之前被调用。
    CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
    CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));
    
    // setup 5
    // 重要文件系统及分区在第一阶段挂载,其他可以在rc执行流程中挂载
    // 挂载/mnt/{vendor,product},这些相对比较重要,所以只挂载重要的,其余的动作会在第二阶段的解析rc文件中处理
    // tmpfs之前说过了,是运作在RAM的文件系统
    CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=1000"));
    CHECKCALL(mkdir("/mnt/vendor", 0755));
    CHECKCALL(mkdir("/mnt/product", 0755));
    CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));
    // 创建selinux驱动节点,类型属于:tmpfs
    CHECKCALL(mkdir("/dev/selinux", 0744));
    // 初始化kernel的日志,之前已经创建过了/dev/kmsg系统,用于处理日志的
    SetStdioToDevNull(argv);
    InitKernelLogging(argv);
    // log可以使用了,第一阶段正式开始 创建设备
    LOG(INFO) << "init first stage started!";
    // setup 6
    // 打开根目录 / ,隶属ramdisk,就是上面挂载的基本文件系统
    auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{
    
    opendir("/"), closedir};
    // 用stat函数获取根目录的文件信息给(old_root_info),例如访问的时间,修改的时间,目录下的文件数量
    // 若!=0则是获取失败,提示未释放ramdisk,估计是基本文件系统还未处理完成
    if (stat("/", &old_root_info) != 0) {
    
    
        PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
        old_root_dir.reset();
    }
    // 加载kernel模块且是非正常的启动模式
    // 根据ALLOW_FIRST_STAGE_CONSOLE(want_console)决定是否打开串口log(控制台log),并加载kernel模块且处于非正常的启动模式
    // 再根据want_console来决定是否打开串口日志
    if (!LoadKernelModules(IsRecoveryMode() && !ForceNormalBoot(cmdline, bootconfig), want_console,
                           want_parallel, module_count)) {
    
    
        if (want_console != FirstStageConsoleParam::DISABLED) {
    
    
            LOG(ERROR) << "Failed to load kernel modules, starting console";
        } else {
    
    
            LOG(FATAL) << "Failed to load kernel modules";
        }
    }
    // 继续根据是否打开串口日志来创建devices
    // want_console == 1
    if (want_console == FirstStageConsoleParam::CONSOLE_ON_FAILURE) {
    
    
    	 // 非恢复模式下进行,在 recovery 模式下不允许创建设备
        if (!IsRecoveryMode()) {
    
    
            // 在创建逻辑分区之前挂载 /metadata
            // metadata.img和userdata.img密切相连,metadata存放了一把Primary key,如果没有这个key,userdata数据不能访问
            // 在工作时可能需要单独刷分区来验证问题,所以有时候不能单独刷metadata分区
            created_devices = DoCreateDevices();
            if (!created_devices) {
    
    
                LOG(ERROR) << "Failed to create device nodes early";
            }
        }
        StartConsole(cmdline);
    }
    // setup 7
    // 当build.prop具有访问权限时,主要就是生成/second_stage_resources/system/etc/ramdisk/build.prop
    // 将/system/etc/ramdisk/build.prop拷贝到/second_stage_resources/system/etc/ramdisk下
    if (access(kBootImageRamdiskProp, F_OK) == 0) {
    
    
        // 获取/second_stage_resources/system/etc/ramdisk/build.prop
        // 当然此时这个目录没有创建,只是一个拼接出来的路径,后续会创建
        std::string dest = GetRamdiskPropForSecondStage();
         // 获取目录路径名称 : /second_stage_resources/system/etc/ramdisk
        std::string dir = android::base::Dirname(dest);
        std::error_code ec;
        if (!fs::create_directories(dir, ec) && !!ec) {
    
    
            LOG(FATAL) << "Can't mkdir " << dir << ": " << ec.message();
        }
        // 生成 /second_stage_resources/system/etc/ramdisk/build.prop
        if (!fs::copy_file(kBootImageRamdiskProp, dest, ec)) {
    
    
            LOG(FATAL) << "Can't copy " << kBootImageRamdiskProp << " to " << dest << ": "
                       << ec.message();
        }
        LOG(INFO) << "Copied ramdisk prop to " << dest;
    }
    // 如果存在“/force_debugable”,则第二阶段init将使用userdebug sepolicy并加载adb_debug.prop以允许adb root
    // /userdebug_plat_sepolicy.cil属于selinux策略里的规则
    // 如果设备unlocked(解锁了),则会修改selinux规则,放大用户权限
    if (access("/force_debuggable", F_OK) == 0) {
    
    
        constexpr const char adb_debug_prop_src[] = "/adb_debug.prop";
        constexpr const char userdebug_plat_sepolicy_cil_src[] = "/userdebug_plat_sepolicy.cil";
        ...
        setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
    }
    // setup 8
    // 如果内核命令行中存在 androidboot.force_normal_boot=1,则设备会正常启动,即正常启动时
    if (ForceNormalBoot(cmdline, bootconfig)) {
    
    
        // 创建第一阶段ramdisk目录 /first_stage_ramdisk
        mkdir("/first_stage_ramdisk", 0755);
        // 提前创建 "/system/bin/snapuserd" && "/first_stage_ramdisk/system/bin/snapuserd";
        PrepareSwitchRoot();
        if (mount("/first_stage_ramdisk", "/first_stage_ramdisk", nullptr, MS_BIND, nullptr) != 0) {
    
    
            PLOG(FATAL) << "Could not bind mount /first_stage_ramdisk to itself";
        }
        // 将根目录(/)切换为 /first_stage_ramdisk ,将根切换到 first_stage_ramdisk
        SwitchRoot("/first_stage_ramdisk");
    }
    
	// 挂载 system、vendor 、product等系统分区
    if (!DoFirstStageMount(!created_devices)) {
    
    
        LOG(FATAL) << "Failed to mount required partitions early ...";
    }
    // 此时new_root_info应该是 /first_stage_ramdisk,而old_root_info是 /root
    // 读取 /first_stage_ramdisk根目录信息,例如有多少个目录等
    struct stat new_root_info;
    if (stat("/", &new_root_info) != 0) {
    
    
        PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
        old_root_dir.reset();
    }
    // 根目录发生变化,则释放old ramdisk,用new ramdisk
    if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {
    
    
        FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);
    }
    // setup 8 主要内容:将根目录(/)切换为 /first_stage_ramdisk ,将根切换到 first_stage_ramdisk
    // setup 9
    //初始化安全框架 Android Verified Boot,用于防止系统文件本身被篡改、防止系统回滚,以免回滚系统利用以前的漏洞。
    // 包括Secure Boot, verified boot 和 dm-verity(会校验只读分区大小,若只读分区二进制改变则可能上被串改了,例如 user强制root),
    // 原理都是对二进制文件进行签名,在系统启动时进行认证,确保系统运行的是合法的二进制镜像文件。其中认证的范围涵盖:bootloader,boot.img,system.img。
    // 此处是在recovery模式下初始化avb的版本,不是recovery模式直接跳过
    SetInitAvbVersionInRecovery();
    // setup 10 --->主要开始进入下一个阶段了,即当期阶段结束了
    // init程序的二进制文件目录
    const char* path = "/system/bin/init";
    const char* args[] = {
    
    path, "selinux_setup", nullptr}; // 设置args为selinux_setup,又重新执行回到main.cpp中,执行SetupSelinux
    auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);// 打开日志处理分区
    // exec系列函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID,即重新回到main.cpp
    execv(path, const_cast<char**>(args));
}

具体信息可以看注释,这里我分为了11个步骤来分析,也就是对应的setup 1~10

2. FirstStageMain(int argc, char** argv)----->setup 1步骤

主要分析InstallRebootSignalHandlers();// setup1其关键作用:主要加载引导程序以及init阶段crash的进程处理,也就是异常处理

// 主要加载引导程序以及init阶段crash的进程处理
void InstallRebootSignalHandlers() {
    
    
    struct sigaction action;
    // memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作
    memset(&action, 0, sizeof(action));
    // sigfillset()用来将参数set信号集初始化,然后把所有的信号加入到此信号集里即将所有的信号标志位置为1。
    // 成功返回0,反之返回1
    sigfillset(&action.sa_mask);
    // 对于从init派生的进程(init进程对操作系统的意义在于,其他所有的用户进程都直接或者间接派生自init进程,例如解析init.rc出来的进程zygote)
    // 这些信号处理程序也会被捕获,但是我们不希望它们触发重新启动,所以我们在这里直接为子进程调用_exit()
    // 针对加载引导程序或init crash后加载引导程序,不应该restart,而是直接退出 再次启动系统时重新start
    action.sa_handler = [](int signal) {
    
    
        if (getpid() != 1) {
    
    
            _exit(signal);
        }
        // 由于init fatal || crash执行重启(所以有些机器故障会无限卡logo重启),这里执行了重启或是强制关机
        InitFatalReboot(signal); //1
    };
    action.sa_flags = SA_RESTART;
    // 设置信号处理器
    sigaction(SIGABRT, &action, nullptr);
    sigaction(SIGBUS, &action, nullptr);
    sigaction(SIGFPE, &action, nullptr);
    sigaction(SIGILL, &action, nullptr);
    sigaction(SIGSEGV, &action, nullptr);
#if defined(SIGSTKFLT)
    sigaction(SIGSTKFLT, &action, nullptr);
#endif
    sigaction(SIGSYS, &action, nullptr);
    sigaction(SIGTRAP, &action, nullptr);
}

以上代码块的逻辑大概就是收集进程信号集,将这些异常的信号集进行特殊处理(关机或重启 触发循环卡logo等),并把这些信号标志为1,针对init进程(Pid=1)派生出来的子进程(例如zygote)而是选择直接退出 不进一步处理,因为如果init进程重新启动后将会重新派生出原来的子进程。这里继续分析一下InitFatalReboot(signal); //1

2.1 InitFatalReboot(signal)

void __attribute__((noreturn)) InitFatalReboot(int signal_number) {
    
    
	// 派生出子进程,pid == -1则代表fork进程失败
    auto pid = fork();

    // Init 是Android OS中第一个user space process(用户态或用户空间pid = 1),守护进程(在后台运行的守护进程,其一作用于执行启动的系统任务)
    // Init 进程是所有用户进程的鼻祖,Init 进程会孵化(fork,派生)出 ueventd、logd、healthd、installd、adbd、lmkd 等用户守护进程
    if (pid == -1) {
    
     // 当pid==1时说明fork失败,立即执行bootloader重启,可能会重新执行到pid == 0
        // Couldn't fork, don't even try to backtrace, just reboot.
        RebootSystem(ANDROID_RB_RESTART2, init_fatal_reboot_target);
    } else if (pid == 0) {
    
     //fork成功,说明现在运行在子进程上,并且子进程需确保能重启
        sleep(5);
        RebootSystem(ANDROID_RB_RESTART2, init_fatal_reboot_target);
    }

    // 当关机时,尝试在父进程(pid = 1,即init)中获取back trace日志
    LOG(ERROR) << __FUNCTION__ << ": signal " << signal_number;
    std::unique_ptr<Backtrace> backtrace(
            Backtrace::Create(BACKTRACE_CURRENT_PROCESS, BACKTRACE_CURRENT_THREAD));
    if (!backtrace->Unwind(0)) {
    
    
        LOG(ERROR) << __FUNCTION__ << ": Failed to unwind callstack.";
    }
    for (size_t i = 0; i < backtrace->NumFrames(); i++) {
    
    
        LOG(ERROR) << backtrace->FormatFrameData(i);
    }
    // 判断init(主)进程是否被标记为异常(例如init主进程都退出了),决定是否需要重启bootLoader(正常步骤会执行到重启,异常则直接sysdump)
    // 启动内核时出现panic的几种情况:出现init进程被异常杀死,则直接触发崩溃,即进入sysdump
    if (init_fatal_panic) {
    
    
        LOG(ERROR) << __FUNCTION__ << ": Trigger crash";
        // 通过#define PROC_SYSRQ "/proc/sysrq-trigger" 写值触发系统崩溃
        // echo b > /proc/sysrq-trigger -->立即重启
        // echo o > /proc/sysrq-trigger -->立即关机
        // echo c > /proc/sysrq-trigger -->立即让系统崩溃
        // echo t > /proc/sysrq-trigger -->导出线程状态信息
        // echo u > /proc/sysrq-trigger -->立即重新挂载所有的文件系统为只读
        android::base::WriteStringToFile("c", PROC_SYSRQ);
        LOG(ERROR) << __FUNCTION__ << ": Sys-Rq failed to crash the system; fallback to exit().";
        _exit(signal_number);
    }
    // 在init进程上重启
    RebootSystem(ANDROID_RB_RESTART2, init_fatal_reboot_target);
}

以上代码块主要作用于确保子进程,如果fork成功则代表目前运行在子进程上,目的就是一个:确保子进程能重启,如果init进程(主进程)被标记为致命异常,则会立即执行android::base::WriteStringToFile("c", PROC_SYSRQ);,触发死机

指令 效果
echo b > /proc/sysrq-trigger 立即重启
echo o > /proc/sysrq-trigger 立即关机
echo c > /proc/sysrq-trigger 立即让系统崩溃
echo t > /proc/sysrq-trigger 导出线程状态信息
echo u > /proc/sysrq-trigger 立即重新挂载所有的文件系统为只读

至此setup 1就分析完了,总结一句话:针对异常的进程进行特殊的处理,例如让子进程确保能重启,检查init进程是否退出或产生异常,若主进程异常则直接对设备进行强制死机,进入sysdump

3. FirstStageMain(int argc, char** argv)----->setup 2步骤

setup 2主要就是初始化文件系统,我这里单独在贴一下代码:

	// setup 2
	//将/dev设置为tmpfs文件系统并挂载,设置0755权限(即用户具有读/写/执行权限,组用户和其它用户具有读写权限)
	// 后续会通过rc文件处理一些分区权限和进程,tmpfs是在内存上建立的文件系统(Filesystem)
	// tmpfs文件系统是一种基于内存的文件系统,可以运行在RAM上,所以/dev都属于tmpfs文件系统
 	CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755")); 
    CHECKCALL(mkdir("/dev/pts", 0755));//1
    // 关于/dev/socket目录
    // 0. tmpfs文件系统类型,例如zygote就保存到该位置(/dev/socket)
    // 1. zygote 开始循环监听 /dev/socket/zygote,system_server 与zygote 达成首次通信
    // 2. 之后AMS会且连接 /dev/socket/zygote 的socket,进行通信
    CHECKCALL(mkdir("/dev/socket", 0755));//2
    CHECKCALL(mkdir("/dev/dm-user", 0755));//3
    CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));

这里主要初始化了基本的文件系统tmpfs,既然基于RAM,那肯定有易失性,特点如下:

  • 基于内存的文件系统
  • 能够动态地使用虚拟内存
  • 不需要格式化文件系统
  • tmpfs 数据在重新启动之后不会保留,因为虚拟内存本质上就是易失的,其优点是读写速度很快,但存在掉电丢失的风险(ramfs与tmpfs有着对比性),这也许就是它叫tmpfs的原故
  • 由于tmpfs基于RAM,运行在内存上,因此它比硬盘的速度肯定要快,因此我们可以利用这个优点使用它来提升机器的性能(小伙伴们可以试着把tmpfs改为基于disk的文件系统试试),tmpfs 的另一个主要的好处是它闪电般的速度,因为典型的 tmpfs 文件系统会完全驻留在 RAM 中,读写几乎可以是瞬间的
  • tmpfs使用了虚拟内存的机制,它会进行swap,用例:达到空间上限时继续写入 结果:提示错误信息并终止,且tmpfs是有上限的,超过时会提示错误信息并终止 所以相比ramfs是比较安全的

tmpfs和ramfs有着对比性,tmpfs是相对安全的,因为 达到空间上限时仍继续写入数据,那么提示错误信息并终止
而ramfs没有空间上限,会持续写入尚未分配的空间(占用其他未分配的内存)。因此tmpfs是固定大小,ramfs不固定其大小。
可以通过命令来查看手机系统使用的是tmpfs还是ramfs,以及他们的信息
adb shell mount | grep -E "(tmpfs|ramfs)"
info可以看到该设备使用的是tmpfs文件系统,以及可以看到属于tmpfs文件系统的分区,大小信息等,还有一个命令也可以查看,比较简约
adb shell df -h | grep -E "(tmpfs|ramfs)"
info
可以看到使用的大小,/dev /mnt /apex都属于tmpfs文件系统,代码里提到过关于/dev/socket目录,是一个比较重要的知识点,具体看注释即可

4. FirstStageMain(int argc, char** argv)----->setup 3步骤

继续贴上FirstStageMain函数里面的setup 3的代码,

// 挂载proc文件系统(驻留在RAM中),Linux系统上的/proc目录是一种文件系统,即proc文件系统。与其它常见的文件系统不同的是,/proc是一种虚拟文件系统
CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));// 1
#undef MAKE_STR
    // 修改 「保存操作系统的启动参数」 的权限:0440,
    // 修改权限的目的是为了 不要将原始bootConfig暴露给非特权进程,部分文件系统只能是0440权限,如果修改权限则无法读取和操作
    // /proc/cmdline中保存bootloader 启动linux kernel 时 的参数
    CHECKCALL(chmod("/proc/cmdline", 0440));
    std::string cmdline;
    // 读取操作系统的启动参数
    android::base::ReadFileToString("/proc/cmdline", &cmdline);
    // 修改权限的目的是为了 不要将原始bootConfig暴露给非特权进程
    // 部分文件系统只能是0440权限,如果修改权限则无法读取和操作
    chmod("/proc/bootconfig", 0440);
    std::string bootconfig;
    // 读取系统启动参数配置
    android::base::ReadFileToString("/proc/bootconfig", &bootconfig);
    gid_t groups[] = {
    
    AID_READPROC};
    CHECKCALL(setgroups(arraysize(groups), groups));

这里主要解释一下注释的第一点

该目录下保存的并不是真正的文件和目录(虚拟文件系统),而是一些【运行时】的信息,如 CPU 信息、负载信息、系统内存信息、磁盘 IO 信息等。
存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系的硬件及当前【正在运行进程的信息】,甚至可以通过更改其中某些文件来改变内核的运行状态

/proc/cmdline         # 保存操作系统的启动参数,/proc/cmdline中保存bootloader 启动linux kernel 时 的参数
/proc/cpuinfo         # 保存CPU的相关信息。对应lscpu命令。
/proc/devices         # 系统已经加载的所有块设备和字符设备的信息。
/proc/diskstats       # 统计磁盘设备的I/O信息。
/proc/filesystems     # 保存当前系统支持的文件系统。
/proc/kcore	          # 物理内存的镜像。该文件大小是已使用的物理内存加上4K。
/proc/loadavg	      # 保存最近1分钟、5分钟、15分钟的系统平均负载。
/proc/meminfo	      # 保存当前内存使用情况。对应free命令
/proc/mounts -> self/mounts	# 系统中当前挂载的所有文件系统。mount命令。
                            # mounts文件是链接到self/mounts。
/proc/partitions      # 每个分区的主设备号(major)、次设备号(minor)、包含的块(block)数目。
/proc/uptime          # 系统自上次启动后的运行时间。
/proc/version         # 当前系统的内核版本号
/proc/vmstat          # 当前系统虚拟内存的统计数据

所以该步骤的代码块主要代码:CHECKCALL(chmod("/proc/cmdline", 0440));android::base::ReadFileToString("/proc/cmdline", &cmdline);,指定权限,若权限改变则无法正常读取,可以防止强行改变权限为a+x或777而带来的非法操作,然后读取cmdline,读取操作系统的启动参数

4. FirstStageMain(int argc, char** argv)----->setup 4步骤

	// setup 4
    // 挂载/sys内核,并设置为sysfs文件系统类型,sysfs是一个伪文件系统。
    // 不代表真实的物理设备,在linux内核中,sysfs文件系统将长期存在于RAM中
    // sysfs文件系统将每个设备抽象成文件,挂载sysfs文件系统在sys目录,用来访问内核信息
    CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
    // 挂载/sys/fs/selinux文件系统,也属于/sys
    // /sys/fs/selinux : selinux机制,也就是处理selinux权限机制文件存放的位置,判断是否开启严格模式等
    CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));
     // 创建处理日志的设备文件
    // Linux内核通过printk打印的log信息,这些log写入到了/dev/kmsg文件中,
    // 也就是kernel log,在展锐平台上可以通过python解析ylog来看到kernel log
    // 也可以在 Shell终端可以通过dmesg /dev/kmsg 命令查看这些log信息
    CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));
    // 如果是ud版本则继续创建kmsg_debug设备文件
    // WORLD_WRITABLE_KMSG = 1 则是ud,为0即user
    if constexpr (WORLD_WRITABLE_KMSG) {
    
    
        CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
    }
    // 文件系统:/dev/random和 /dev/urandom是 Linux 上的字符设备文件,它们是随机数生成器,为系统提供随机数
    // 随机数在计算中很重要。 TCP/IP 序列号、密码盐和 DNS 源端口号都依赖于随机数。
    // 在密码学中,随机性无处不在,从密钥的生成到加密系统,甚至密码系统受到攻击的方式。没有随机性,所有加密操作都是可预测的,因此不安全
    CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
    CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));

    // This is needed for log wrapper, which gets called before ueventd runs.
    // 创建日志系统的串口log(伪终端),这是日志包装器所需要的,它在ueventd运行之前被调用。
    CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
    CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));
  1. 上面的代码比较简单,主要就是创建随机数设备文件,所以上层可以调用random获取随机数,随机数在计算中很重要,TCP/IP 序列号、密码盐和 DNS 源端口号都依赖于随机数,也例如加密机制,都需要用到随机数。

  2. 同时也创建了/dev/null设备文件,dev/null 在Linux中是一个空设备文件,它于其他普通的设备文件不同,当往null写入数据时会被丢弃掉,它不能够被执行,所以不能使用管道符去操作它,只能通过文件重定向(>,>>),我们可以通过把命令的输出重定向到 /dev/null 来丢弃脚本的全部输出

  3. 也创建了/dev/kmsg设备文件,主要存储日志,Shell终端可以通过dmesg /dev/kmsg 命令查看这些log信息
    log

  4. 在介绍一下/sys文件系统,sysfs(常驻于RAM中)是一个伪文件系统,不占有任何磁盘空间的虚拟文件系统

/sys下存放的都是设备驱动,网络环境,偏硬件的文件
1./sys/firmware : 固件 文件目录
2./sys/kernel : 内核文件目录
3./sys/module : 内核驱动模块
4./sys/power : 电源相关模块
5./sys/bus : 驱动总线文件目录
6./sys/block : 块设备目录(映射的/sys/devices目录)
7./sys/devices : 设备目录(也有虚拟设备目录),例如:sys/devices/virtual/block/dm-28
8./sys/fs/selinux : selinux机制,也就是处理selinux权限机制文件存放的位置,判断是否开启严格模式等

5. FirstStageMain(int argc, char** argv)----->setup 5步骤

	// setup 5
    // 重要文件系统及分区在第一阶段挂载,其他可以在rc执行流程中挂载
    // 挂载/mnt/{vendor,product},这些相对比较重要,所以只挂载重要的,其余的动作会在第二阶段的解析rc文件中处理
    // tmpfs之前说过了,是运作在RAM的文件系统
    CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=1000"));
    CHECKCALL(mkdir("/mnt/vendor", 0755));
    CHECKCALL(mkdir("/mnt/product", 0755));
    CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));
    // 创建selinux驱动节点,类型属于:tmpfs
    CHECKCALL(mkdir("/dev/selinux", 0744));
    // 初始化kernel的日志,之前已经创建过了/dev/kmsg系统,用于处理日志的
    SetStdioToDevNull(argv);
    InitKernelLogging(argv);
    // log可以使用了,第一阶段正式开始 创建设备
    LOG(INFO) << "init first stage started!";

这里的代码也比较简单,主要创建比较重要的分区,注意这里只是创建并没有挂载,/mnt/{vendor,product},然后初始化kernel log,LOG日志可以正常使用和输出。

  • vendor: 用于存放odm供应商开发的文件,例如MTK自带节点 /mnt/vendor/protect_f/ 可存储恢复出厂+刷机不丢失数据
  • product:根据不同的项目,存放项目中不同的内容,例如APK位置等
  • mnt : 此目录主要是作为挂载点使用,例如挂载/mnt/sdcard

6. FirstStageMain(int argc, char** argv)----->setup 6步骤

   // setup 6
    // 打开根目录 / ,隶属ramdisk,就是上面挂载的基本文件系统
    auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{
    
    opendir("/"), closedir};
    // 用stat函数获取根目录的文件信息给(old_root_info),例如访问的时间,修改的时间,目录下的文件数量
    // 若!=0则是获取失败,提示未释放ramdisk,估计是基本文件系统还未处理完成
    if (stat("/", &old_root_info) != 0) {
    
    
        PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
        old_root_dir.reset();
    }
    // 根据ALLOW_FIRST_STAGE_CONSOLE(want_console)决定是否打开串口log(控制台log),
    // 并加载kernel模块且处于非正常的启动模式,根据want_console来决定是否打开串口日志
    if (!LoadKernelModules(IsRecoveryMode() && !ForceNormalBoot(cmdline, bootconfig), want_console,
                           want_parallel, module_count)) {
    
    
        if (want_console != FirstStageConsoleParam::DISABLED) {
    
    
            LOG(ERROR) << "Failed to load kernel modules, starting console";
        } else {
    
    
            LOG(FATAL) << "Failed to load kernel modules";
        }
    }
    // 继续根据是否打开串口日志来创建devices
    // want_console == 1
    if (want_console == FirstStageConsoleParam::CONSOLE_ON_FAILURE) {
    
    
    	 // 非恢复模式下进行,在 recovery 模式下不允许创建设备
        if (!IsRecoveryMode()) {
    
    
            // 在创建逻辑分区之前挂载 /metadata
            // metadata.img和userdata.img密切相连,metadata存放了一把Primary key,如果没有这个key,userdata数据不能访问
            // 在工作时可能需要单独刷分区来验证问题,所以有时候不能单独刷metadata分区
            created_devices = DoCreateDevices();// 1
            if (!created_devices) {
    
    
                LOG(ERROR) << "Failed to create device nodes early";
            }
        }
        StartConsole(cmdline);
    }

这里的代码主要是打开串口log(可选),在手机无法开机的情况下会抓串口log来分析问题。这里挂载了/metadata,metadata.img和userdata.img密切相连,metadata存放了一把Primary key,如果没有这个key,userdata数据不能访问,在文章开头也提到过metadata分区,这里会挂载和创建metadata里的设备文件,感兴趣的可以进一步去了解,挂载/metadata的前提条件是必须有根目录“/” 或 /system,否则,切换根目录后无法访问ramdisk(后续会把old ramdisk切换到new ramdisk,切换后的新根目录会还原默认权限)

7. FirstStageMain(int argc, char** argv)----->setup 7步骤

	// setup 7
	// 判断文件是否存在,并判断文件是否可写:/system/etc/ramdisk/build.prop
    // 当build.prop具有访问权限时,主要就是生成/second_stage_resources/system/etc/ramdisk/build.prop
    // 将/system/etc/ramdisk/build.prop拷贝到/second_stage_resources/system/etc/ramdisk下
    if (access(kBootImageRamdiskProp, F_OK) == 0) {
    
    
        // 获取/second_stage_resources/system/etc/ramdisk/build.prop
        // 当然此时这个目录没有创建,只是一个拼接出来的路径,后续会创建
        std::string dest = GetRamdiskPropForSecondStage();
        // 获取目录路径名称 : /second_stage_resources/system/etc/ramdisk
        std::string dir = android::base::Dirname(dest);
        std::error_code ec;
         // 创建/second_stage_resources/system/etc/ramdisk目录
        if (!fs::create_directories(dir, ec) && !!ec) {
    
    
            LOG(FATAL) << "Can't mkdir " << dir << ": " << ec.message();
        }
         // 拷贝 /system/etc/ramdisk/build.prop文件到 /second_stage_resources/system/etc/ramdisk
        // 生成 /second_stage_resources/system/etc/ramdisk/build.prop
        if (!fs::copy_file(kBootImageRamdiskProp, dest, ec)) {
    
    
            LOG(FATAL) << "Can't copy " << kBootImageRamdiskProp << " to " << dest << ": "
                       << ec.message();
        }
        LOG(INFO) << "Copied ramdisk prop to " << dest;
    }
    // 如果存在“/force_debugable”,则第二阶段init将使用userdebug sepolicy并加载adb_debug.prop以允许adb root
    // /userdebug_plat_sepolicy.cil属于selinux策略里的规则
    // 如果设备unlocked(解锁了),则会修改selinux规则,放大用户权限
    if (access("/force_debuggable", F_OK) == 0) {
    
    
        constexpr const char adb_debug_prop_src[] = "/adb_debug.prop";
        constexpr const char userdebug_plat_sepolicy_cil_src[] = "/userdebug_plat_sepolicy.cil";
        ...
        setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
    }

img
setup7的代码片段主要讲解了获取ramdis下的prop属性值,为了第二阶段做准备,一句话来说就是创建了second_stage_resources/system/etc/ramdisk目录文件,然后把/system/etc/ramdisk/build.prop复制到上面的目录下,得到:/second_stage_resources/system/etc/ramdisk/build.prop。
针对已经unlock了的设备加载特定的selinux规则,以放大权限,使得只读分区可以写入数据

8. FirstStageMain(int argc, char** argv)----->setup 8步骤

	// setup 8
    // 如果是正常启动模式则创建 /first_stage_ramdisk目录作为根目录,把之前的根目录切换掉
    // 因为之前都是基于根目录创建文件系统,放大了权限,将old根目录切换到first_stage_ramdisk后
    // 将会在first_stage_ramdisk根目录把权限恢复到默认,例如old根目录部分文件系统是777权限,切换到first_stage_ramdisk根目录后
    // 权限会放低,会给一个安全的权限,判断是否是正常启动可以通过该属性值判断:androidboot.force_normal_boot
    if (ForceNormalBoot(cmdline, bootconfig)) {
    
    
        // 创建第一阶段ramdisk目录 /first_stage_ramdisk
        mkdir("/first_stage_ramdisk", 0755);
        // 提前创建 "/system/bin/snapuserd" && "/first_stage_ramdisk/system/bin/snapuserd";
        PrepareSwitchRoot();
        // 挂载/first_stage_ramdisk目录
        if (mount("/first_stage_ramdisk", "/first_stage_ramdisk", nullptr, MS_BIND, nullptr) != 0) {
    
    
            PLOG(FATAL) << "Could not bind mount /first_stage_ramdisk to itself";
        }
        // 将根目录(/)切换为 (/first_stage_ramdisk) 
        // 将根切换到 first_stage_ramdisk
        SwitchRoot("/first_stage_ramdisk");
    }
    
	// 挂载 system、vendor 、product等系统分区,1
    if (!DoFirstStageMount(!created_devices)) {
    
    
        LOG(FATAL) << "Failed to mount required partitions early ...";
    }
    
    // 此时new_root_info应该是 /first_stage_ramdisk,而old_root_info是 /root
    // 读取 /first_stage_ramdisk根目录信息,例如有多少个目录等
    struct stat new_root_info;
    if (stat("/", &new_root_info) != 0) {
    
    
        PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
        old_root_dir.reset();
    }
    
    // 如果新的根目录已经创建,则释放old ramdisk,用new ramdisk
    if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {
    
    
        FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);
    }

如果是正常启动模式则创建 /first_stage_ramdisk目录作为根目录,把之前的根目录切换掉,因为之前都是基于根目录创建文件系统,放大了权限,将old根目录切换到first_stage_ramdisk后,将会把first_stage_ramdisk根目录把权限恢复到默认(相对较低的权限),假设old根目录部分文件系统是777权限,切换到/first_stage_ramdisk根目录后,权限会放低(0755),会给一个安全的权限,如果文件权限给太高,是非常危险的,此后会将system、vendor 、product等系统分区挂载到新根目录上。

必须执行的一步:如果新的根目录已经创建,则释放old ramdisk,用new ramdisk,再将system、vendor 、product等系统分区挂载到/first_stage_ramdisk上

这里再跟一下DoFirstStageMount函数

8.1 DoFirstStageMount

该方法位于/system/core/init/first_stage_mount.cpp#DoFirstStageMount

// 装载设备树中fstab指定的分区
bool DoFirstStageMount(bool create_devices) {
    
    
    // 如果处于恢复模式,则跳过第一阶段装载
    if (IsRecoveryMode()) {
    
    
        LOG(INFO) << "First stage mount skipped (recovery mode)";
        return true;
    }

    auto fsm = FirstStageMount::Create();// 1
    if (!fsm.ok()) {
    
    
        LOG(ERROR) << "Failed to create FirstStageMount " << fsm.error();
        return false;
    }

    if (create_devices) {
    
    
        if (!(*fsm)->DoCreateDevices()) return false;// 2
    }

    return (*fsm)->DoFirstStageMount();
}

先看第一点 auto fsm = FirstStageMount::Create();,该方法主要用于AVB校验,AVB校验可以去看看google文档

Result<std::unique_ptr<FirstStageMount>> FirstStageMount::Create() {
    
    
		// 读取fstab,file system table,里面包含了要挂载的逻辑分区
    auto fstab = ReadFirstStageFstab();
    if (!fstab.ok()) {
    
    
        return fstab.error();
    }
	// 判断device tree(fstabl)中是否有vbmeta/compatible结构,值是android,vbmeta
	// 创建FirstStageMountVBootV1或者FirstStageMountVBootV2实例,取决于	
	// IsDtVbmetaCompatible(fstab)的返回值,如果支持vbmeta,则使用FirstStageMountVBootV2,反之FirstStageMountVBootV1
    if (IsDtVbmetaCompatible(*fstab)) {
    
    
        return std::make_unique<FirstStageMountVBootV2>(std::move(*fstab));
    } else {
    
    
        return std::make_unique<FirstStageMountVBootV1>(std::move(*fstab));
    }
}

以上主要是创建V1或者V2版本的AVB校验,AVB校验主要是针对分区进行校验,对于要启动的 Android 版本中包含的所有可执行代码和数据,启动时验证均要求在使用前以加密形式对其进行验证。包括内核(从 boot 分区加载)、设备树(从 dtbo 分区加载)、system 分区和 vendor 分区等

  • 对于 boot 和 dtbo 这类仅读取一次的小分区,通常是通过将整个内容加载到内存中,然后计算其哈希值来进行验证
  • 内存装不下的较大分区(如文件系统)可能会使用哈希树;
  • 如果在某个时刻计算出的根哈希值与预期根哈希值不一致,系统便不会使用相应数据,无法启动Android
  • 在工作上遇到过开发者模式下OEM无法打开的情况,跟代码才发现开启OEM时会把persist分区文件里的指定位置上写入值(忘记值是多少了),通过解析该分区文件,写入的数据会以十六进制保持到指定位置上。但是瞬间就会把OEM的开关站位信息给擦除了,通过log看该分区文件是启用了RPMB分区安全保护机制

我们继续看第二点if (!(*fsm)->DoCreateDevices()) return false;

bool FirstStageMount::DoFirstStageMount() {
    
    
    // 如果fstab(设备树)为空则不执行挂载动作
    // Fstab是内核在启动时用来挂载文件系统的文件系统表
    if (!IsDmLinearEnabled() && fstab_.empty()) {
    
    
        // Nothing to mount.
        LOG(INFO) << "First stage mount skipped (missing/incompatible/empty fstab in device tree)";
        return true;
    }

    // 挂载分区详细步骤
    if (!MountPartitions()) return false;

    return true;
}

详细逻辑都在MountPartitions里面,继续跟着看看,该内容比较长

bool FirstStageMount::MountPartitions() {
    
    
    // 将system分区挂载到设备的“/”根目录
    if (!TrySwitchSystemAsRoot()) return false;
    
    if (!SkipMountingPartitions(&fstab_, true /* verbose */)) return false;
    
    // 读取fstab(设备树,product,vendor等逻辑分区),并把其中的逻辑分区进行挂载
    for (auto current = fstab_.begin(); current != fstab_.end();) {
    
    
        // 跳过/system,因为已经挂载上了
        if (current->mount_point == "/system") {
    
    
            ++current;
            continue;
        }
        // 跳过overlay,稍后后挂载它,OverlayFS是新引入的机制,感兴趣的自行了解
        // 当我们adb remount分区时,本来是可以直接把vendor改rw权限,但是引入overlayfs后仍然时只读权限
        // 因为分为了2层,能读写的始终在第一层,第二层仍然保留了原始的权限,保留其原子性
        if (current->fs_type == "overlay") {
    
    
            ++current;
            continue;
        }
        
		// flash相关,不太清楚
        if (current->fs_type == "emmc") {
    
    
            ++current;
            continue;
        }

        // 挂载fstab里的所有逻辑分区(例如:system,system_ext,vendor,product)
        Fstab::iterator end;
        if (!MountPartition(current, false /* erase_same_mounts */, &end)) {
    
    
            if (current->fs_mgr_flags.no_fail) {
    
    
                LOG(INFO) << "Failed to mount " << current->mount_point
                          << ", ignoring mount for no_fail partition";
            } else if (current->fs_mgr_flags.formattable) {
    
    
                LOG(INFO) << "Failed to mount " << current->mount_point
                          << ", ignoring mount for formattable partition";
            } else {
    
    
                PLOG(ERROR) << "Failed to mount " << current->mount_point;
                return false;
            }
        }
        current = end;
    }
    ...
    // 如果在fstab中没有看到/system或/,那么我们需要为overlayfs创建一个根目录/system或“/”
    if (!GetEntryForMountPoint(&fstab_, "/system") && !GetEntryForMountPoint(&fstab_, "/")) {
    
    
        FstabEntry root_entry;
        if (GetRootEntry(&root_entry)) {
    
    
            fstab_.emplace_back(std::move(root_entry));
        }
    }

    // 为overlayfs实例化设备文件,将vendor,product,data分区等实例化到该设备文件中
    // 例如/dev/block/dm-33设备文件上挂载的是/data,此时会有dm-verity来验证数据,比如root后更改了设备文件,那校验则会不通过,造成无法开机
    // dm-verity 是一个虚拟块设备,专门用于文件系统的校验,fs直接交互的设备是dm-verity,dm-verity调用真正的块驱动去读取对应的块(dm-verity读取dm-xxx)
    //   /dev/block/dm-3                94M  94M  300K 100% /vendor
    //   /dev/block/dm-2               1.5G 1.5G  4.8M 100% /product
    //   /dev/block/dm-1               166M 165M  508K 100% /system_ext
    //   /dev/block/dm-33              5.8G 609M  5.2G  11% /data
    auto init_devices = [this](std::set<std::string> devices) -> bool {
    
    
        for (auto iter = devices.begin(); iter != devices.end();) {
    
    
            if (android::base::StartsWith(*iter, "/dev/block/dm-")) {
    
    
                if (!block_dev_init_.InitDmDevice(*iter)) {
    
    
                    return false;
                }
                iter = devices.erase(iter);
            } else {
    
    
                iter++;
            }
        }
        return InitRequiredDevices(std::move(devices));
    };
    MapScratchPartitionIfNeeded(&fstab_, init_devices);

    // 再次挂载fstab_里面的全部逻辑分区,也就是overlayfs机制(adb remount也是靠这个机制实现的)
    // 再次声明将只读分区修改为可读写后,写入修改后的分区内容会保存到upperdir上,而不是直接修改其底层分区数据
    // 以保证overlayfs文件操作原子性,这里再次挂载了fstab里的所有逻辑分区,那岂不是重复了?其实这里再次挂载只是一个merged(合并)操作
    fs_mgr_overlayfs_mount_all(&fstab_);

    return true;
}

以上主要挂载了fstab(设备树)下的所有逻辑分区并实例化到对应的/dev/block/dm-xx设备文件上,例如挂载了:system,system_ext,vendor,product,还有挂载了overlay,overlay机制是用于保护分区原子性和分区安全而存在,具体可以自行了解一下,注释里也解释了一些,代码里两次挂载了fstab下的逻辑分区,第二次挂载是因为overlay机制的影响,是为了合并两个名称相同的分区,这里上一张图片来观察分区挂载的设备文件和位置
img可以观察到vendor.img镜像被挂载到了/vendor分区上,使用的设备文件是/dev/block/dm-3

9. FirstStageMain(int argc, char** argv)----->setup 9/10步骤

		// setup 9
    //初始化安全框架 Android Verified Boot,用于防止系统文件本身被篡改、防止系统回滚,以免回滚系统利用以前的漏洞。
    // 包括Secure Boot, verified boot 和 dm-verity(会校验只读分区大小,若只读分区二进制改变则可能上被串改了,例如 user强制root,则会被dm-verity检测到),
    // 原理都是对二进制文件进行签名,在系统启动时进行认证,确保系统运行的是合法的二进制镜像文件。其中认证的范围涵盖:bootloader,boot.img,system.img。
    // 此处是在recovery模式下初始化avb的版本,不是recovery模式直接跳过
    SetInitAvbVersionInRecovery();
    // setup 10 --->主要开始进入下一个阶段了,即当期阶段结束了
    // init程序的二进制文件目录
    const char* path = "/system/bin/init";
    const char* args[] = {
    
    path, "selinux_setup", nullptr}; // 设置args为selinux_setup,又重新执行回到main.cpp中,执行SetupSelinux
    auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);// 打开日志处理分区
    // exec系列函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID,即重新回到main.cpp
    execv(path, const_cast<char**>(args));

SetInitAvbVersionInRecovery里的步骤也是读取fstab,然后根据IsDtVbmetaCompatible是否支持AVB2.0,来进行处理,setup 10就是进入下一个阶段了(SetupSelinux)

三. FirstStageMain阶段总结

以上所有的代码和步骤:根据main.cpp来启用第一阶段FirstStageMain,挂载最基本的文件系统,该文件系统是运行于RAM上的,优点是相比disk磁盘来说运行速度快,不占存储空间,特点是易失性,断电即丢失,挂载上最基本的文件系统后会根据根目录"/"来挂载/mnt/{vendor,product}等重要的分区,其他不重要的文件挂载在第二阶段rc中处理,生成 /second_stage_resources/system/etc/ramdisk/build.prop,该文件会在第二阶段使用,在第一阶段并开启kernel log,挂载/first_stage_ramdisk新的根目录,根据设备树(fstab)来创建逻辑分区system,system_ext,vendor,product并挂载到/first_stage_ramdisk根目录上,然后将old 根目录切换到/first_stage_ramdisk 根目录,释放old根目录,/first_stage_ramdisk根目录将赋予较为安全的权限,创建AVB数据校验,启用overlayfs机制来保护分区原子性,初始化恢复模式下的AVB校验方案,然后调用"/system/bin/init"进入下一个阶段:selinux_setup
selinux_setup文章:基于Android13的系统启动流程分析(一)之SeLinux权限介绍

文章有点长,干货还是有的,工作上会用得到,往往一些细节决定了是否加班
文章内容若有不对的地方或者有疑惑的地方尽情评论和指教

猜你喜欢

转载自blog.csdn.net/q1210249579/article/details/128652348