Android9.0启动-inIt进程(First Stage)

2,415 阅读5分钟

基于9.0源码解析,代码的路径在第一行注释代码。本文为平时笔记的整理,难免有错误,欢迎大家指正,如有错误给您带来困惑和损失,也请见谅。

概述

init进程是Android系统的第一个用户进程,init进程由Linux内核启动,入口为/system/core/init/init.cpp中的main方法。它的执行主要包括两个部分: first_stage和second_stage.这两个阶段做了很多的初始化工作,接下来的两篇文章就探究一下这个过程。

init入口

/* system/core/init/init.cpp */
    /**
     * main函数:第一个参数传入参数的个数,包括可执行程序的名称。第二个参数为一个字符串数组,
     * 为具体的传入参数,其中args[0]为可执行程序的名称。
     */
int main(int argc, char** argv) {

    /**
     * 1.basename函数:当传入一个路径名参数时,将返回最后一个“/”后面的字符串,如果最后一个“/”为空,则返回倒数第二个“/”和倒数第一个"/"之前的字符串。例如:
     *   "/data/data/com/xray",结果返回为xray.
     * 2.strcmp函数:比较传入的两个字符串,如果返回值为0则相等,非0则为不等。但是在c语言中0为false,非0为true.
     */
    if (!strcmp(basename(argv[0]), "ueventd")) {//当argv[0]为ueventd,也就是程序名为ueventd,则进入ueventd_main,
                                                //ueventd为init进程创建的子进程,用于创建设备文件,同时接收uevent事件
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {//和ueventd一样,watchdogd(看门狗)进程的入口,用于程序出问题时重启系统。
        return watchdogd_main(argc, argv);
    }

    if (argc > 1 && !strcmp(argv[1], "subcontext")) {//subcontext是android9新出现的,它的出现就是为了隔离system和vendor
        InitKernelLogging(argv); //
        const BuiltinFunctionMap function_map;
        return SubcontextMain(argc, argv, &function_map);
    }
  if (REBOOT_BOOTLOADER_ON_PANIC) { //初始化重启信号的监听,当监听到重启信号时,重启系统
        InstallRebootSignalHandlers();
    }
    ...
    return 0;
}

ueventd启动

/* /system/core/rootdir/init.rc */
on early-init
  ...
    start ueventd
## Daemon processes to be run by init.
service ueventd /sbin/ueventd
    class core
    critical
    seclabel u:r:ueventd:s0
    shutdown critical

从rc文件我们知道ueventd进程依赖于/sbin/ueventd 这个二进制文件,但是它的代码实现在哪里了,在Android.mk文件中,发现了端倪,/sbin/ueventd只是init这个二进制文件的软链接,所以ueventd的真正入口就是我们上面的/system/core/init/init.cpp的main方法。

/* /system/core/init/Android.mk */
# Create symlinks.
LOCAL_POST_INSTALL_CMD := $(hide) mkdir -p $(TARGET_ROOT_OUT)/sbin; \
    ln -sf ../init $(TARGET_ROOT_OUT)/sbin/ueventd; \
    ln -sf ../init $(TARGET_ROOT_OUT)/sbin/watchdogd

下面看看ueventd的实现

/* /system/core/init/ueventd.cpp  */
int ueventd_main(int argc, char** argv) {
    /*
     * init sets the umask to 077 for forked processes. We need to
     * create files with exact permissions, without modification by
     * the umask.
     */
    umask(000);

    InitKernelLogging(argv);

    LOG(INFO) << "ueventd started!";

    SelinuxSetupKernelLogging();
    SelabelInitialize();

    DeviceHandler device_handler = CreateDeviceHandler();//解析rc文件
    UeventListener uevent_listener;

    if (access(COLDBOOT_DONE, F_OK) != 0) {
        ColdBoot cold_boot(uevent_listener, device_handler);
        cold_boot.Run();//遍历/sys目录, 将"add"写入uevent文件, 根据uevent文件创建设备节点
    }

    // We use waitpid() in ColdBoot, so we can't ignore SIGCHLD until now.
    signal(SIGCHLD, SIG_IGN);
    // Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN
    // for SIGCHLD above.
    while (waitpid(-1, nullptr, WNOHANG) > 0) {
    }

    uevent_listener.Poll([&device_handler](const Uevent& uevent) {//热插拔,获取内核uevent事件,动态的创建设备节点
        HandleFirmwareEvent(uevent);
        device_handler.HandleDeviceEvent(uevent);
        return ListenerAction::kContinue;
    });

    return 0;
}

ueventd的启动分为两个部分:

  • 冷启动(Cold Boot) 它会遍历所有在/sys注册的设备并将'add'写入它找到的每个'uevent'文件,这会导致内核生成并重新发送所有当前注册设备的uevent消息。这样做是因为当这些设备注册到sysfs时,ueventd根本还没有开始运行,没能接收他们的uevent消息并妥善处理,这样Android系统也是无法正常运行的,所以需要重新生成其uevent以便ueventd对其做处理.简单概括这一过程就是创建静态的设备文件。
  • 热插拔(Hot Plug) 当有新的设备连接时,比如tv上插入一个u盘,ueventd就会接受到内核发送的uevent事件,并且为设备动态的创建设备文件。

watchdog启动

和ueventd一样, watchdogd也是init的另一面. 这时, 它被用作硬件watchdog定时器(timer) (/dev/watchdog,如果有的话)在用户态中的接口. 设置一个超时((timeout)变量, 每隔一段时 间就发送一个keepalive信号(一个""字节)。如果超过了超时变量规定的时间,watchdogd 还没能及时发送keepalive信号,硬件watchdog定时器就会发出一个中断,要求内核重启,下面看看其实现:

/* system/core/init/watchdogd.cpp */
int watchdogd_main(int argc, char **argv) {
    InitKernelLogging(argv);

    int interval = 10;
    if (argc >= 2) interval = atoi(argv[1]);//atoi函数:将一个字符串转化为整型

    int margin = 10;
    if (argc >= 3) margin = atoi(argv[2]);

    LOG(INFO) << "watchdogd started (interval " << interval << ", margin " << margin << ")!";

    int fd = open(DEV_NAME, O_RDWR|O_CLOEXEC);
    if (fd == -1) {
        PLOG(ERROR) << "Failed to open " << DEV_NAME;
        return 1;
    }

    int timeout = interval + margin;
    int ret = ioctl(fd, WDIOC_SETTIMEOUT, &timeout);//ioctl 是设备驱动程序中对设备的I/O通道进行管理的函数,
                                                    //这一个参数就是open函数返回的文件描述符,
                                                    //第二个参数是用户程序对设备的控制命令(这里是设置超时时间),
                                                    第三个参数是超时的值
    if (ret) {
        PLOG(ERROR) << "Failed to set timeout to " << timeout;
        ret = ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
        if (ret) {
            PLOG(ERROR) << "Failed to get timeout";
        } else {
            if (timeout > margin) {
                interval = timeout - margin;
            } else {
                interval = 1;
            }
            LOG(WARNING) << "Adjusted interval to timeout returned by driver: "
                         << "timeout " << timeout
                         << ", interval " << interval
                         << ", margin " << margin;
        }
    }

    while (true) {
        write(fd, "", 1);//写入“”字符串,告诉watchdog定时器(/dev/watchdog)一切正常,如果系统发生故障了,
                         //watchdogd 还没能及时发送这个""字符串,硬件watchdog定时器就会发出一个中断,要求内核重启
        sleep(interval);
    }
}

## subcontext
init 进程具有几乎不受限制的权限,并可使用系统分区和verdor分区(供应商分区)中的输入脚本在启动过程中初始化系统。该访问权限会导致 Treble 系统/供应商拆分中出现巨大漏洞,因为供应商脚本可能会指示 init 访问不属于稳定系统-供应商 ABI(应用二进制接口)的文件、属性等。
供应商 init 已设计为使用单独的安全增强型 Linux (SELinux) 域 vendor_init,以利用供应商专属权限来运行 /vendor 中的命令,从而填补此漏洞. 所以如果想要在init中运行脚本需要将其添加到vendor中

InstallRebootSignalHandlers

当内部通过sigaction注册信号,当监听到信号时,重启bootloader.

/* /system/core/init/init.cpp */
static void InstallRebootSignalHandlers() {
    // Instead of panic'ing the kernel as is the default behavior when init crashes,
    // we prefer to reboot to bootloader on development builds, as this will prevent
    // boot looping bad configurations and allow both developers and test farms to easily
    // recover.

    //当init crashes时重启bootloader 而不是内核panic这种默认的行为。因为这样能防止由于错误配置导致的循环启动
    struct sigaction action;
    memset(&action, 0, sizeof(action));
    sigfillset(&action.sa_mask);
    action.sa_handler = [](int signal) {
        // These signal handlers are also caught for processes forked from init, however we do not
        // want them to trigger reboot, so we directly call _exit() for children processes here.
        if (getpid() != 1) {//退出子进程
            _exit(signal);
        }

        // Calling DoReboot() or LOG(FATAL) is not a good option as this is a signal handler.
        // RebootSystem uses syscall() which isn't actually async-signal-safe, but our only option
        // and probably good enough given this is already an error case and only enabled for
        // development builds.
        RebootSystem(ANDROID_RB_RESTART2, "bootloader");
    };
    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);
}

first stage

init进程的执行主要涉及到两个阶段first stage和second stage,下面看看first stage

int main(int argc, char** argv) {
    ...
  bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);//从环境变量中获取是否执行过第一阶段的标记

    if (is_first_stage) {//如果没有执行过第一阶段
        boot_clock::time_point start_time = boot_clock::now();

        // Clear the umask.
        umask(0);// 这个函数的作用是设置用户创建文件和目录时的默认权限,如果umask为0,用户创建的目录权限就是:目录在各位上的最大权限减去umaks值
                 //目录的最大权限是666(因为目录不需要执行权限),因为umask为0,所以这里创建的目录的默认的权限就是666,如果umask值为0022,那么目录的默认权限就是644

        clearenv();
        setenv("PATH", _PATH_DEFPATH, 1);//设置环境变量/sbin:/system/sbin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin
        // Get the basic filesystem setup we need put together in the initramdisk
        // on / and then we'll let the rc file figure out the rest.
        mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");//将临时文件系统tmpfs挂载到/dev目录
        mkdir("/dev/pts", 0755);//创建目录
        mkdir("/dev/socket", 0755);
        mount("devpts", "/dev/pts", "devpts", 0, NULL);
        #define MAKE_STR(x) __STRING(x)
        mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
        // Don't expose the raw commandline to unprivileged processes.
        chmod("/proc/cmdline", 0440);
        gid_t groups[] = { AID_READPROC };
        setgroups(arraysize(groups), groups);
        mount("sysfs", "/sys", "sysfs", 0, NULL);
        mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);

        mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));//mknod 创建一个特殊的设备文件,/dev/kmsg 输出内核日志

        if constexpr (WORLD_WRITABLE_KMSG) {
            mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11));
        }

        mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
        mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));

        // Mount staging areas for devices managed by vold
        // See storage config details at http://source.android.com/devices/storage/
        mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
              "mode=0755,uid=0,gid=1000");
        // /mnt/vendor is used to mount vendor-specific partitions that can not be
        // part of the vendor partition, e.g. because they are mounted read-write.
        mkdir("/mnt/vendor", 0755);

        // Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
        // talk to the outside world...
        //tmpfs已经挂载到/dev 并且有了/dev/kmsg文件,这个时候就可以初始化内核日志系统了,通过/dev/kmsg.就可以将内核日志输出到外界了
        InitKernelLogging(argv);

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

        if (!DoFirstStageMount()) {
            LOG(FATAL) << "Failed to mount required partitions early ...";
        }
 /**
        * 初始化验证启动,它会确保所有已经执行的代码均来自可信来源,以防止受到攻击和损坏。它可以建立一条从受硬件保护的
        *信任根到引导加载程序,再到启动分区和其已验证分区(包括system、 vendor)的完整信任链,在设备启动的过程中,都会在
        * 进入下一个阶段前验证下一个阶段的完整行和真实性。
        */
        SetInitAvbVersionInRecovery();

        // Enable seccomp if global boot option was passed (otherwise it is enabled in zygote).
        //为了防止一些恶意的程序直接的调用system call, Android 新出了seccomp,它的作用就是过滤一些System call,防止应用层的app直接调用。
        global_seccomp();

        // Set up SELinux, loading the SELinux policy.
        SelinuxSetupKernelLogging();
        SelinuxInitialize();//启动SELinux,加载SELinux策略

        // We're in the kernel domain, so re-exec init to transition to the init domain now
        // that the SELinux policy has been loaded.
        if (selinux_android_restorecon("/init", 0) == -1) {
            PLOG(FATAL) << "restorecon failed of /init failed";
        }

        setenv("INIT_SECOND_STAGE", "true", 1);//设置环境变量,标记已经完成了第一阶段

        static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
        uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
        setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1);

        char* path = argv[0];
        char* args[] = { path, nullptr };
        execv(path, args);//这里的path其实就是/system/bin/init 相当于再次调用main方法,执行second stage逻辑

        // execv() only returns if an error happened, in which case we
        // panic and never fall through this conditional.
        PLOG(FATAL) << "execv(\"" << path << "\") failed";
    }

 ...

    return 0;
}

文件系统

文件系统描述
tmpfs种虚拟内存文件系统,它会将所有的文件存储在虚拟内存中,如果你将tmpfs文件系统卸载后,那么其下的所有的内容将不复存在,断电后内容也不复存在
devpts为伪终端提供了一个标准接口,它的标准挂接点是/dev/ pts。只要pty的主复合设备/dev/ptmx被打开,就会在/dev/pts下动态的创建一个新的pty设备文件
proc一个非常重要的虚拟文件系统,它可以看作是内核内部数据结构的接口,通过它我们可以获得系统的信息,同时也能够在运行时修改特定的内核参数
sysfs与proc文件系统类似,也是一个不占有任何磁盘空间的虚拟文件系统。它通常被挂接在/sys目录下。sysfs文件系统是Linux2.6内核引入的,它把连接在系统上的设备和总线组织成为一个分级的文件,使得它们可以在用户空间存取

InitKernelLogging

InitKernelLogging的作用是将标准输入,标准输出,标准错误重定向到/sys/fs/selinux/null

/* /system/core/init/log.cpp */
void InitKernelLogging(char* argv[]) {
    // Make stdin/stdout/stderr all point to /dev/null.
    int fd = open("/sys/fs/selinux/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 /sys/fs/selinux/null";
    }

    /**
    *dup2(int fd1, int fd2) 将fd1复制到fd2,也就是i标准输入,标准输出,标准错误,都指向了/sys/fs/selinux/null
    */
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    if (fd > 2) close(fd);

    android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);
}

我们看看最后一行代码,这行代码的路径在system/core/base/logging.cpp, 它主要做的事情是设置日志级别等。我们着重看看第二个参数android::base::KernelLogger,它的代码实现如下:

/* system/core/base/logging.cpp */
void KernelLogger(android::base::LogId, android::base::LogSeverity severity,
                  const char* tag, const char*, unsigned int, const char* msg) {
  // clang-format off
  static constexpr int kLogSeverityToKernelLogLevel[] = {
      [android::base::VERBOSE] = 7,              // KERN_DEBUG (there is no verbose kernel log
                                                 //             level)
      [android::base::DEBUG] = 7,                // KERN_DEBUG
      [android::base::INFO] = 6,                 // KERN_INFO
      [android::base::WARNING] = 4,              // KERN_WARNING
      [android::base::ERROR] = 3,                // KERN_ERROR
      [android::base::FATAL_WITHOUT_ABORT] = 2,  // KERN_CRIT
      [android::base::FATAL] = 2,                // KERN_CRIT
  };
  // clang-format on
  static_assert(arraysize(kLogSeverityToKernelLogLevel) == android::base::FATAL + 1,
                "Mismatch in size of kLogSeverityToKernelLogLevel and values in LogSeverity");

  static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC));
  if (klog_fd == -1) return;

  int level = kLogSeverityToKernelLogLevel[severity];

  // The kernel's printk buffer is only 1024 bytes.
  // TODO: should we automatically break up long lines into multiple lines?
  // Or we could log but with something like "..." at the end?
  char buf[1024];
  size_t size = snprintf(buf, sizeof(buf), "<%d>%s: %s\n", level, tag, msg);
  if (size > sizeof(buf)) {
    size = snprintf(buf, sizeof(buf), "<%d>%s: %zu-byte message too long for printk\n",
                    level, tag, size);
  }

  iovec iov[1];
  iov[0].iov_base = buf;
  iov[0].iov_len = size;
  TEMP_FAILURE_RETRY(writev(klog_fd, iov, 1));//将日志写入/dev/kmsg中
}

所以我们想查看内核日志的话可以通过:

cat /dev/kmsg

另外还有两种查看内核日志方法:

cat /proc/kmsg
dmesg

总结

上面把init中第一阶段的工作介绍完了,如果把每个细节都弄明白确实很困难,有时一个c函数就的研究半天,但是我相信这个是值得的。下一篇文中会开始第二个阶段,包括rc文件的解析,SELinux相关的问题(这个问题在Android P中非常的让人头疼)等。好了,这篇就就写到这里,我们下篇见。

参考资料

关于我

  • 公众号: CodingDev qrcode_for_gh_0e16b0c63d2d_258.jpg