Skip to content

FirstStageInit

逐段解析 FirstStageMain 的最小文件系统、模块加载、启动分流、跨阶段交接和旧 ramdisk 清理。

基于android-17.0.0_r1
AndroidinitFirstStageInitramdisk内核模块

FirstStageInit ​

本文面向已经理解文件描述符、mount、设备节点和 exec 基本语义的读者。请先读内核到init:上一篇已经证明内核如何执行 /init,以及 PID 1 如何最终进入 FirstStageMain();本文不再重复内核候选路径,而是把 system/core/init/first_stage_init.cpp 展开成一条可逐行追踪的资源生命周期。

本文解决的问题是:普通 Android 启动、recovery ramdisk、charger 和首阶段控制台共用同一个 FirstStageMain() 时,哪些对象由首阶段创建,哪些状态必须跨 exec 保留,哪些资源必须在交给 SetupSelinux() 前清理。本文只跟到 execv("/system/bin/init", {"selinux_setup"}),fstab、AVB、dm-verity 和逻辑分区算法留给后续的 FirstStageMount 专题。

读完后,你应能从一条首阶段日志定位到具体分支,解释 /dev、/proc、/sys、/mnt 和 /second_stage_resources 的消费者,并判断模块加载失败、recovery 分流或旧 ramdisk 清理分别会不会阻止启动。

1. 执行约束 ​

FirstStageMain() 运行在 Android 用户空间尚未建立的窗口。此时没有属性服务、没有 ueventd coldboot、没有 rc action,也不能假设 /system、/vendor 已经可访问。它只能依赖 first-stage 静态链接可执行文件、内核接口和 ramdisk 中的资源。

Android 17 的构建定义印证了这个边界。init_first_stage_defaults 把 first_stage_init.cpp、first_stage_mount.cpp、devices.cpp、switch_root.cpp 等编进 first-stage init,并链接静态库。首阶段需要的能力必须进入这个闭包,不能等待二阶段动态加载。

资源首阶段所有者直接消费者生命周期
/dev tmpfsFirstStageMain日志、设备发现、modprobe跨两个 exec 保留
/procFirstStageMaincmdline、bootconfig、进程接口持续到系统运行
/sysFirstStageMain模块、块设备、SELinux持续到系统运行
/mnt tmpfsFirstStageMainfirst-stage mount由后续挂载体系接管
旧 ramdisk内核解包,init 清理首阶段文件切根后释放
环境变量FirstStageMainSELinux/second stage读取后主动清除

首阶段的正确结束条件不是“所有 Android 服务都可用”,而是 /system/bin/init 已可执行、SELinux 所需输入已可见、跨阶段数据已转存,并且旧根中不再需要的内容可以回收。

2. 临时根 ​

2.1 环境清理 ​

入口先记录启动时间,清除 umask 和环境,再设置受控 PATH。这里使用 CHECKCALL 累积错误,而不是在第一次失败时立即退出:

源码文件:system/core/init/first_stage_init.cpp

相关函数/类型:FirstStageMain

cpp
int FirstStageMain(int argc, char** argv) {
    boot_clock::time_point start_time = boot_clock::now();

    std::vector<std::pair<std::string, int>> errors;
#define CHECKCALL(x) \
    if ((x) != 0) errors.emplace_back(#x " failed", errno);

    umask(0);
    CHECKCALL(clearenv());
    CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));

clearenv() 的结果是首阶段不信任内核执行 /init 时可能携带的普通环境;随后创建的 FIRST_STAGE_STARTED_AT、INIT_MODULE_DURATION_MS 和 INIT_FORCE_DEBUGGABLE 都是 init 自己定义的交接协议。因为 exec 保留环境,这些值能够被后续阶段消费。

2.2 内核接口 ​

首阶段创建最小文件系统,并在读取 cmdline 和 bootconfig 后收紧 /proc/bootconfig 权限:

源码文件:system/core/init/first_stage_init.cpp

cpp
CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
CHECKCALL(mkdir("/dev/pts", 0755));
CHECKCALL(mkdir("/dev/socket", 0755));
CHECKCALL(mkdir("/dev/dm-user", 0755));
CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
#define MAKE_STR(x) __STRING(x)
CHECKCALL(mount("proc", "/proc", "proc", 0,
                "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR

std::string cmdline;
android::base::ReadFileToString("/proc/cmdline", &cmdline);
chmod("/proc/bootconfig", 0440);
std::string bootconfig;
android::base::ReadFileToString("/proc/bootconfig", &bootconfig);
gid_t groups[] = {AID_READPROC};
CHECKCALL(setgroups(arraysize(groups), groups));
CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));

读取发生在 chmod 之后并不矛盾:PID 1 以 root 身份运行,而且紧接着把自己加入 AID_READPROC。hidepid=2 和 bootconfig 的 0440 是后续普通进程的可见性边界,不是为了阻止 init 自己读取。

cmdline 与 bootconfig 是多个分支的共同输入:console 参数、charger 模式、force-normal-boot、模块并行方式和休眠恢复都从这里解析。它们由内核提供,由首阶段读取,但真正的语义所有者分散在各个解析函数中。

2.3 基础节点 ​

ueventd 尚未运行,所以首阶段手工创建自己立刻需要的字符设备:

源码文件:system/core/init/first_stage_init.cpp

cpp
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)));
}

CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));
CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));

/dev/kmsg 是 KernelLogger 的输出端;/dev/null 用来固定 0、1、2 三个标准文件描述符;/dev/ptmx 是早于 ueventd 运行的 log wrapper 所需入口。这里不能套用“所有 /dev 都由 ueventd 创建”的常规结论,首阶段正是这个结论的例外窗口。

2.4 暂存目录 ​

接着创建 first-stage mount 和跨阶段数据使用的 tmpfs:

源码文件:system/core/init/first_stage_init.cpp

cpp
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"));
CHECKCALL(mount("tmpfs", kSecondStageRes, "tmpfs",
                MS_NOEXEC | MS_NOSUID | MS_NODEV,
                "mode=0755,uid=0,gid=0"));

MS_NOEXEC | MS_NOSUID | MS_NODEV 表明这些目录是数据和挂载暂存区,不是可执行代码来源。/debug_ramdisk 保存调试属性和 policy 输入,/second_stage_resources 保存普通 ramdisk 属性;二者消费者不同,不能合并理解。

Microdroid 且 OpenDice 变更启用时还会创建 /microdroid_resources。这是编译与运行条件共同决定的分支,普通 Android 设备不应把它列为固定启动依赖。

3. 日志边界 ​

3.1 延迟报错 ​

所有 CHECKCALL 完成后,init 才建立日志并统一处理错误:

源码文件:system/core/init/first_stage_init.cpp

cpp
#undef CHECKCALL

SetStdioToDevNull(argv);
InitKernelLogging(argv);

if (!errors.empty()) {
    for (const auto& [error_string, error_errno] : errors) {
        LOG(ERROR) << error_string << " " << strerror(error_errno);
    }
    LOG(FATAL) << "Init encountered errors starting first stage, aborting";
}

LOG(INFO) << "init first stage started!";

这样设计的原因不是忽略错误,而是在 /dev/kmsg 尚未创建前尽量完成基础操作,然后一次输出完整失败集合。最终 LOG(FATAL) 仍会终止启动。排查时不要只截取最后一行 fatal;它只说明集合非空,真正的失败调用位于此前逐条 LOG(ERROR)。

3.2 标准描述符 ​

SetStdioToDevNull() 的实现先打开 /dev/null,再覆盖 0、1、2:

源码文件:system/core/init/first_stage_console.cpp

cpp
void SetStdioToDevNull(char** argv) {
    int fd = open("/dev/null", O_RDWR);
    if (fd == -1) {
        int saved_errno = errno;
        android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);
        errno = saved_errno;
        PLOG(FATAL) << "Couldn't open /dev/null";
    }
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    if (fd > STDERR_FILENO) close(fd);
}

这一步同时处理两种内核状态:内核可能已把标准描述符接到 /dev/console,也可能根本没有打开它们。前者会在 SELinux re-exec 时留下访问不匹配,后者可能让未来第一个 open() 意外占用 fd 0。因此首阶段主动固定三个编号,日志再经 KernelLogger 写 /dev/kmsg。

二阶段还会再次调用 SetStdioToDevNull()。原因是首阶段仍处在 kernel SELinux context,它打开的 fd 不一定允许二阶段 context 继续访问;“exec 会保留 fd”是机制事实,“保留的 fd 一定可用”却不是。

4. 模块装载 ​

4.1 模式清单 ​

启动模式决定默认模块清单:

源码文件:system/core/init/first_stage_init.cpp

cpp
std::string GetModuleLoadList(BootMode boot_mode,
                              const std::string& dir_path) {
    std::string module_load_file;
    switch (boot_mode) {
        case BootMode::NORMAL_MODE:
            module_load_file = "modules.load";
            break;
        case BootMode::RECOVERY_MODE:
            module_load_file = "modules.load.recovery";
            break;
        case BootMode::CHARGER_MODE:
            module_load_file = "modules.load.charger";
            break;
    }

    if (module_load_file != "modules.load") {
        struct stat fileStat{};
        std::string load_path = dir_path + "/" + module_load_file;
        if (stat(load_path.c_str(), &fileStat)) {
            module_load_file = "modules.load";
        }
    }
    return module_load_file;
}

recovery 或 charger 专用清单不存在时回退 modules.load。因此看到设备只提供通用清单并不等于专用模式无法启动;真正要检查的是回退后的模块是否覆盖该模式所需驱动。

GetBootMode() 先判断 charger,再判断 recovery;recovery 文件存在但 force_normal_boot 生效时返回 normal:

源码文件:system/core/init/first_stage_init.cpp

cpp
static BootMode GetBootMode(const std::string& cmdline,
                            const std::string& bootconfig) {
    if (IsChargerMode(cmdline, bootconfig))
        return BootMode::CHARGER_MODE;
    else if (IsRecoveryMode() && !ForceNormalBoot(cmdline, bootconfig))
        return BootMode::RECOVERY_MODE;

    return BootMode::NORMAL_MODE;
}

4.2 目录匹配 ​

LoadKernelModules() 读取 uname().release,从 /lib/modules 选择目录。Android 17 不只比较完整 release,还处理 16K/64K 页大小后缀和主次版本回退:

源码文件:system/core/init/first_stage_init.cpp

cpp
struct utsname uts{};
if (uname(&uts)) {
    LOG(FATAL) << "Failed to get kernel version.";
}
int major = 0, minor = 0;
if (sscanf(uts.release, "%d.%d", &major, &minor) != 2) {
    LOG(FATAL) << "Failed to parse kernel version " << uts.release;
}

std::unique_ptr<DIR, decltype(&closedir)> base_dir(
        opendir(MODULE_BASE_DIR), closedir);
if (!base_dir) {
    LOG(INFO) << "Unable to open /lib/modules, skipping module loading.";
    return true;
}

/lib/modules 不存在被视为“没有模块需要加载”,返回成功;uname 失败或 release 无法解析则 fatal。遍历目录时若找到 uts.release + page_size_suffix 的精确目录,就清空候选并禁止回退;否则收集主次版本匹配且页大小兼容的目录,排序后依次尝试。

这个顺序解释了一个常见现象:同一 ramdisk 中放了多个 GKI 模块目录时,精确 release 目录拥有最高优先级,而不是按目录顺序碰运气。

4.3 并行模式 ​

bootconfig 可以选择 NORMAL、PERFORMANCE 或 CONSERVATIVE 并行加载,也可打开并行测试。没有配置时走 LoadListedModules():

源码文件:system/core/init/first_stage_init.cpp

cpp
auto want_parallel_mode = Modprobe::LoadParallelMode::NONE;
auto want_parallel_test = false;
if (bootconfig.find("androidboot.load_modules_parallel = \"true\"") !=
    std::string::npos)
    want_parallel_mode = Modprobe::LoadParallelMode::NORMAL;
else if (bootconfig.find(
             "androidboot.load_modules_parallel = \"performance\"") !=
         std::string::npos)
    want_parallel_mode = Modprobe::LoadParallelMode::PERFORMANCE;
else if (bootconfig.find(
             "androidboot.load_modules_parallel = \"conservative\"") !=
         std::string::npos)
    want_parallel_mode = Modprobe::LoadParallelMode::CONSERVATIVE;

最终消费者是 Modprobe:

源码文件:system/core/init/first_stage_init.cpp

cpp
Modprobe m({MODULE_BASE_DIR}, GetModuleLoadList(boot_mode, MODULE_BASE_DIR));
bool retval = (want_parallel_mode != Modprobe::LoadParallelMode::NONE)
        ? m.LoadModulesParallel(std::thread::hardware_concurrency(),
                                want_parallel_mode, want_parallel_test)
        : m.LoadListedModules(!want_console);
modules_loaded = m.GetModuleCount();

首阶段把模块耗时写入 INIT_MODULE_DURATION_MS,二阶段 RecordStageBoottimes() 再发布为 ro.boottime.init.modules 并 unsetenv()。这是一条完整的 owner 到 consumer 链,而不只是一个日志数字。

4.4 失败降级 ​

源码文件:system/core/init/first_stage_init.cpp

cpp
if (!LoadKernelModules(boot_mode, want_console, want_parallel_mode,
                       want_parallel_test, module_count)) {
    if (want_console != FirstStageConsoleParam::DISABLED) {
        LOG(ERROR) << "Failed to load kernel modules, starting console";
    } else {
        LOG(FATAL) << "Failed to load kernel modules";
    }
}

控制台关闭时,模块失败阻止启动;控制台已请求时,错误降为 LOG(ERROR),让读者进入首阶段 shell 收集证据。这个分支改变的是故障恢复能力,不是把缺失驱动变成成功:后续挂载仍可能因为块设备不存在而 fatal。

5. 启动分流 ​

5.1 控制台 ​

FirstStageConsole() 从 bootconfig 或 cmdline 读取 androidboot.first_stage_console,并限制值域。只有构建时 ALLOW_FIRST_STAGE_CONSOLE 允许,运行参数才会生效。

当值为 CONSOLE_ON_FAILURE 时,init 会先尝试创建 first-stage mount 所需设备,再调用 StartConsole():

源码文件:system/core/init/first_stage_init.cpp

cpp
bool created_devices = false;
if (want_console == FirstStageConsoleParam::CONSOLE_ON_FAILURE) {
    if (!IsRecoveryMode()) {
        fsm = CreateFirstStageMount(cmdline);
        if (fsm) {
            created_devices = fsm->DoCreateDevices();
            if (!created_devices) {
                LOG(ERROR) << "Failed to create device nodes early";
            }
        }
    }
    StartConsole(cmdline);
}

StartConsole() fork 子进程,尝试执行 /first_stage.sh,有 kernel console 时再执行 /system/bin/sh;父进程等待 console 子进程退出后继续。created_devices 防止正常路径重复创建同一批设备。这里的取消路径是用户退出 shell,恢复路径是父进程回到挂载流程,而不是另起一个 init。

5.2 Recovery ​

recovery 判断不是读取属性,而是检查 /system/bin/recovery:

源码文件:system/core/init/first_stage_init.cpp

相关函数/类型:IsRecoveryMode

cpp
bool IsRecoveryMode() {
    return access("/system/bin/recovery", F_OK) == 0;
}

正常 recovery 模式跳过 DoFirstStageMount();但共享 recovery ramdisk 可以通过 androidboot.force_normal_boot=1 请求普通 Android 启动。此时首阶段先把需要保留的 snapuserd 复制到 /first_stage_ramdisk/system/bin,再切换根:

源码文件:system/core/init/first_stage_init.cpp

相关函数/类型:FirstStageMain

cpp
if (ForceNormalBoot(cmdline, bootconfig)) {
    mkdir("/first_stage_ramdisk", 0755);
    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";
    }
    SwitchRoot("/first_stage_ramdisk");
}

bind mount 自身是因为 SwitchRoot() 要求目标必须是 mount point。PrepareSwitchRoot() 优先选择 generic ramdisk 的 snapuserd_ramdisk,否则使用 vendor ramdisk 的 snapuserd,通过硬链接放到新根中的固定路径。这个准备动作只在 force-normal-boot 分支发生。

5.3 普通挂载 ​

源码文件:system/core/init/first_stage_init.cpp

cpp
if (IsRecoveryMode()) {
    LOG(INFO) << "First stage mount skipped (recovery mode)";
} else {
    if (!fsm) {
        fsm = CreateFirstStageMount(cmdline);
    }
    if (!fsm) {
        LOG(FATAL) << "FirstStageMount not available";
    }

    if (!created_devices && !fsm->DoCreateDevices()) {
        LOG(FATAL) << "Failed to create devices required for first stage mount";
    } else if (REBOOT_BOOTLOADER_ON_PANIC && !AttemptingToBootNewSlot()) {
        InstallRebootSignalHandlers();
    }

    if (!fsm->DoFirstStageMount()) {
        LOG(FATAL) << "Failed to mount required partitions early ...";
    }
}

对象创建、设备创建和分区挂载是三个独立失败点。REBOOT_BOOTLOADER_ON_PANIC 的 handler 只在设备创建成功之后、且当前不是尝试新 slot 时安装,防止新 slot 启动失败被直接转成 bootloader 重启而破坏回退语义。

6. 跨阶段交接 ​

6.1 Ramdisk属性 ​

首阶段可能在 ramdisk 中看到 /system/etc/ramdisk/build.prop。它不能假设切根后这个路径仍指向同一文件,因此复制到 tmpfs:

源码文件:system/core/init/first_stage_init.cpp

相关函数/类型:PrepareSecondStageResources

cpp
if (access(kBootImageRamdiskProp, F_OK) == 0) {
    std::string dest = GetRamdiskPropForSecondStage();
    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();
    }
    if (!fs::copy_file(kBootImageRamdiskProp, dest, ec)) {
        LOG(FATAL) << "Can't copy " << kBootImageRamdiskProp << " to "
                   << dest << ": " << ec.message();
    }
}

GetRamdiskPropForSecondStage() 生成 /second_stage_resources/system/etc/ramdisk/build.prop。二阶段 property service 的 LoadPropertiesFromSecondStageRes() 读取它;不存在只接受 ENOENT,存在却不可读则触发检查,解析失败记录 warning。这里的复制失败是首阶段 fatal,消费失败则按二阶段错误策略处理。

6.2 调试资源 ​

/force_debuggable 存在时,首阶段把 adb_debug.prop 和 userdebug_plat_sepolicy.cil 分别复制到 /debug_ramdisk,并设置 INIT_FORCE_DEBUGGABLE=true:

源码文件:system/core/init/first_stage_init.cpp

相关函数/类型:PrepareSecondStageResources

cpp
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";
    std::error_code ec;
    if (access(adb_debug_prop_src, F_OK) == 0 &&
        !fs::copy_file(adb_debug_prop_src, kDebugRamdiskProp, ec)) {
        LOG(WARNING) << "Can't copy " << adb_debug_prop_src << " to "
                     << kDebugRamdiskProp << ": " << ec.message();
    }
    if (access(userdebug_plat_sepolicy_cil_src, F_OK) == 0 &&
        !fs::copy_file(userdebug_plat_sepolicy_cil_src,
                       kDebugRamdiskSEPolicy, ec)) {
        LOG(WARNING) << "Can't copy " << userdebug_plat_sepolicy_cil_src;
    }
    setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
}

这段代码本身不直接授予 adb root。SELinux 阶段消费调试 policy,property service 消费 adb_debug.prop,而且源码注释明确把设备 unlocked 作为最终启用条件的一部分。首阶段只是搬运输入并设置标志。

6.3 启动计时 ​

进入 SELinux setup 前,首阶段记录开始时间:

源码文件:system/core/init/selinux.cpp

cpp
setenv(kEnvFirstStageStartedAt,
       std::to_string(start_time.time_since_epoch().count()).c_str(), 1);

二阶段 RecordStageBoottimes() 读取 FIRST_STAGE_STARTED_AT 和 SELinux 阶段时间,发布:

源码文件:system/core/init/init.cpp

cpp
SetProperty("ro.boottime.init.first_stage",
            std::to_string(selinux_start_time_ns - first_stage_start_time_ns));
SetProperty("ro.boottime.init.selinux",
            std::to_string(second_stage_start_time.time_since_epoch().count() -
                           selinux_start_time_ns));

读取后使用 unsetenv() 清除交接变量,避免它们继续污染 init 将来启动的服务环境。这是显式清理路径。

7. 旧根清理 ​

首阶段在挂载前保存旧根目录句柄和设备号,挂载后再次读取新根设备号:

源码文件:system/core/init/first_stage_init.cpp

cpp
auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{
        opendir("/"), closedir};
struct stat old_root_info{};
if (stat("/", &old_root_info) != 0) {
    PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
    old_root_dir.reset();
}

struct stat new_root_info{};
if (stat("/", &new_root_info) != 0) {
    PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
    old_root_dir.reset();
}

if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {
    FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);
}

只有根目录设备号发生变化才递归释放旧 ramdisk。FreeRamdisk() 使用已打开的旧根 fd,通过 readdir()、fstatat()、openat() 和 unlinkat() 遍历;遇到属于其他设备的挂载点会跳过,避免删除新挂载内容:

源码文件:system/core/init/first_stage_init.cpp

cpp
if (fstatat(dfd, de->d_name, &info, AT_SYMLINK_NOFOLLOW) != 0) {
    continue;
}
if (info.st_dev != dev) {
    continue;
}

正在承担首阶段 snapshot 合并的 snapuserd 也会被保留。清理失败多数被设计为跳过而不是 fatal,因为此时更重要的目标是继续策略切换;代价是旧 ramdisk 页可能暂时不能回收。

最后把 stdout/stderr 指向 /dev/kmsg,并执行 SELinux 模式:

源码文件:system/core/init/first_stage_init.cpp

相关函数/类型:FirstStageMain

cpp
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
execv(path, const_cast<char**>(args));

PLOG(FATAL) << "execv(\"" << path << "\") failed";

成功 exec 后首阶段程序映像消失,PID 1、环境和未设置 CLOEXEC 的 fd 按 exec 规则保留。这里要区分原始 fd 和 dup2() 的目标:open() 返回的 fd 带 O_CLOEXEC,会在 exec 时关闭;复制到 stdout/stderr 的 fd 不继承 FD_CLOEXEC,会跨 exec 保留。SetupSelinux() 随后再次调用 SetStdioToDevNull() 和 InitKernelLogging(),重新建立适合新 SELinux context 的标准描述符与日志。

8. 故障矩阵 ​

输入或状态失败位置行为恢复入口
基础 mount/mknoderrors 集合汇总后 fatal最早期 kernel log
uname 无法解析模块目录选择立即 fatal核对 kernel release
/lib/modules 不存在模块加载视为无模块并继续确认设备是否内建驱动
模块依赖失败,无 consoleLoadKernelModulesfatal修复模块清单或依赖
模块依赖失败,有 console同上记录错误并进入 shell首阶段 shell 检查设备
休眠恢复节点失败MaybeResumeFromHibernation记录 error 并继续冷启动路径检查 /sys/power/resume
FirstStageMount 创建失败CreateFirstStageMount随后 fatal检查 fstab/设备树输入
设备节点失败DoCreateDevicesfatal检查 uevent 和块设备
必要分区失败DoFirstStageMountfatal检查 AVB、dm、fstab
旧根无法 stat清理阶段跳过释放并继续内存占用与早期日志
/system/bin/init exec 失败交接末端fatal检查挂载、权限、格式

这个矩阵体现了首阶段的错误策略:建立不可替代的执行前提时 fatal;诊断能力开启时允许部分模块错误降级;清理和休眠恢复失败多记录后继续。不能把所有 LOG(ERROR) 都解释为启动终止,也不能把所有 fallback 都解释为启动成功。

9. 测试闭环 ​

FirstStageMain 本身依赖真实 mount、mknod、内核模块和切根,没有覆盖整条函数的普通 host 单元测试。可用的局部证明来自 libmodprobe:libmodprobe_test.cpp 构造临时的 modules.dep、modules.softdep、modules.load 和 blocklist,断言 LoadListedModules() 的加载顺序、数量和卸载结果;module_dependency_graph_test.cpp 的 SimpleDependency 逐次断言只有依赖满足的模块进入 ready 集合,SimpleDependencyFailed 则证明依赖失败会阻止下游模块就绪。

这些测试能证明 Modprobe 的依赖图和清单消费,不证明某台设备的 /lib/modules 目录选择、bootconfig、块设备和分区挂载。完整启动仍需设备证据。

在 Android 源码根目录可执行以下源码核对:

bash
rg -n "int FirstStageMain|LoadKernelModules|ForceNormalBoot|FreeRamdisk" \
  system/core/init/first_stage_init.cpp
rg -n "LoadPropertiesFromSecondStageRes|kDebugRamdiskProp" \
  system/core/init/property_service.cpp
rg -n "RecordStageBoottimes|FIRST_STAGE_STARTED_AT" \
  system/core/init/init.cpp

有测试环境时运行局部测试目标:

bash
atest libmodprobe_tests

设备侧先采集输入、分支和消费者,避免只看最终 boot completed:

bash
adb shell 'cat /proc/bootconfig 2>/dev/null | grep -E "first_stage|load_modules|force_normal|mode"'
adb shell 'getprop ro.boottime.init.first_stage'
adb shell 'getprop ro.boottime.init.modules'
adb shell 'dmesg | grep -E "init first stage|Loaded .* modules|First stage mount|freeing ramdisk|first_stage_console"'

验证结果应形成闭环:bootconfig 是输入,首阶段日志显示选择的分支,ro.boottime.init.* 是二阶段消费后的状态。如果只有属性值而没有早期日志,它只能证明计时变量被消费,不能证明每个 mount 或模块都走了预期路径。

10. 设备边界 ​

本文已经从 Android 17 真实源码证明:首阶段如何建立最小文件系统与标准描述符,如何根据 boot mode 和页大小选择模块目录,如何在 console、recovery 和 normal boot 之间分流,如何把属性、调试资源与计时跨 exec 交给消费者,以及如何按设备号清理旧 ramdisk。

本文没有展开 FirstStageMount::Create() 如何选择实现、DoCreateDevices() 如何完成 coldboot、MountPartitions() 如何处理 fstab/AVB/dm-verity,也没有证明任意厂商设备都包含相同模块和 bootconfig。下一篇将以 FirstStageMount 为 owner,从 fstab 输入追到每个 early-mount 分区的生效与失败位置。