[Android basics] - Android 8.1 boot process analysis (1)

1. Overview of the startup process

                                                Figure: android_boot_process

 

Learning any hardware and software system, studying the system startup process is a very effective way to start. The picture above can help the group understand the startup process of the Android system.

1. Boot ROM stage

After the Android device is powered on, it will first start execution from the boot code of the ROM on the processor, and the on-chip ROM will find the code of the BootLoader and load it into the memory. This step is designed and implemented by the "chip manufacturer".

2. BootLoader stage

BootLoader, also known as the boot program, is a program that runs before the operating system runs. It mainly has functions such as checking RAM and initializing the hardware parameters of the system. Then it finds the Linux kernel code, sets the startup parameters, and finally loads it into the memory. U-boot is a universal boot program.

3. Kernel stage

The Linux kernel starts to boot, initializes various hardware and software environments, loads drivers, mounts the root file system, and executes the init program, thus opening the world of Android.

Startup file path: source/kernel/init/main.c

4. The init process stage

From this step, you have truly entered the world of Android. The init process is the emperor process of the Android world. All other processes are forks directly or indirectly by the init process.

The init process is responsible for starting the most critical core daemen processes (Zygote, ServiceManager, etc.) of the system. The Zygote process creates the dalvik virtual machine, which is the foundation of the Java world. In addition, other functions such as property service are also provided.

Startup file path: source/system/core/init/init.cpp

This note mainly records the startup process of the Android world, that is, the process from the init process to the main interface lighting.

Two, the init process

2.1. The kernel code starts the init process

After the kernel is running, it will execute the start_kernel function, which is responsible for the initialization of various functions before the formal operation of the kernel. At the end of the start_kernel function, the reset_init function is called to start three processes (idle, kernel_init, kthreadd) to process the formal operation of the operating system .

  • Idle is the idle process of the operating system. When the cpu is idle, go back and run it;
  • The kernel_init function is started as a process, but then it will read the init program under the root file system. This operation will complete the transition from kernel mode to user mode. This init process is the parent process of all user mode processes, and it generates a large number of The child process of the init process, so the init process will always exist, and its PID is 1;
  • kthreadd is the kernel daemon, its PID is 2;

The following code is the specific startup logic of the init process:

File path: kernel/init/main.c

static int __ref kernel_init(void *unused)
{
    // ramdisk_execute_command 这个值为 "./init"
    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }

    /*
     * We try each of these until one succeeds.
     *
     * The Bourne shell can be used instead of init if we are
     * trying to recover a really broken machine.
     */
    if (execute_command) {
        ret = run_init_process(execute_command);
        if (!ret)
            return 0;
        panic("Requested init %s failed (error %d).",
              execute_command, ret);
    }
    if (!try_to_run_init_process("/sbin/init") ||
        !try_to_run_init_process("/etc/init") ||
        !try_to_run_init_process("/bin/init") ||
        !try_to_run_init_process("/bin/sh"))
        return 0;

    panic("No working init found.  Try passing init= option to kernel. "
          "See Linux Documentation/init.txt for guidance.");  
}

The following note is the code that actually executes the init program. Through the definition of LOCAL_MODULE_PATH under system/core/init/Android.mk, you can know that the installation path of the final init executable file is in the root file system.

LOCAL_MODULE_PATH := $(TRAGET_ROOT_OUT)

2.2, ueventd/watchdogd jump and environment variable setting

int main(int argc, char** argv) {
    // basename 是 C 库中的一个函数,得到特定的路径中的最后一个'/'后面的内容
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {
        return watchdogd_main(argc, argv);
    }

    if (REBOOT_BOOTLOADER_ON_PANIC) {
        InstallRebootSignalHandlers();
    }

    add_environment("PATH", _PATH_DEFPATH);

    bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
}

++ Program code description++:

1. The main function in C++ has two parameters, the first parameter argc represents the number of parameters, and the second parameter is the parameter list;

2. If the program runs without other parameters, argc = 1, argv[0] = the path of the execution process;

3. The init process has two other entrances, ueventd (enter ueventd_main) and watchdogd (enter watchdogd_main);

rk3288:/sbin # ls -al
lrwxrwxrwx  1 root root       7 1969-12-31 19:00 ueventd -> ../init
lrwxrwxrwx  1 root root       7 1969-12-31 19:00 watchdogd -> ../init

You can see that watchdog and ueventd are a soft link, directly linked to the init program,
so when executing /sbin/ueventd or /sbin/watchdogd, it will enter the corresponding ueventd_main and watchdogd_main entry points.

1. ueventd is mainly responsible for a series of tasks such as the creation of equipment nodes and permission setting;

2. Watchdogd, commonly known as watchdog, is used to restart the system when the system goes wrong;

2.2.1、ueventd_main

The file is defined in source/system/core/init/ueventd.cpp.

Android and Linux use device drivers to access hardware devices, and the device node file is the logical file of the device driver. However, the "/dev" directory does not exist in the image of the Android root file system. This directory is dynamically created after the init process is started.

Therefore, the important task of creating node files for Android devices is also in the init process, which is the job of ueventd.

ueventd creates device node files in two ways:

1. The first method corresponds to "Cold Plug"; that is, based on the pre-defined device information, when ueventd is started, the device node file is created uniformly. This type of device node file is also called a static node file.

2. The second method corresponds to "Hot Plug"; that is, when a device is inserted into the USB port during system operation, ueventd will receive this time and dynamically create a device node file for the inserted device. This type of device node file is also called a dynamic node file.

File path: source/system/core/init/ueventd.cpp

int ueventd_main(int argc, char** argv) {
    // 创建新建文件的权限默认值
    // 与 chmod 相反,这里相当于新建文件后权限为 666 
    umask(000);

    // 初始化日志输出
    InitKernelLogging(argv);

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

    // 注册 selinux 相关的用于打印 log 的回调函数
    selinux_callback cb;
    cb.func_log = selinux_klog_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);


    DeviceHandler device_handler = CreateDeviceHandler();
    // 创建 socket,用于监听 uevent 事件
    UeventListener uevent_listener;

    // 通过 access 判断文件 /dev/.coldboot_done 是否存在
    // 若已经存在则表明已经进行过冷插拔了
    if (access(COLDBOOT_DONE, F_OK) != 0) {
        ColdBoot cold_boot(uevent_listener, device_handler);
        cold_boot.Run();
    }

    // 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) {
        HandleFirmwareEvent(uevent);
        device_handler.HandleDeviceEvent(uevent);
        return ListenerAction::kContinue;
    });

    return 0;
}

 

2.2.2、watchdogd_main

The "watchdog" itself is a timer circuit, which will keep timing (or counting) operations inside. The computer system and the "watchdog" are connected to two pins, which will pass every certain period of time during normal operation. One of the pins wants to "watchdog" to send a signal. After the "watchdog" receives the signal, the timer will be cleared and restarted.

Once the system has a problem, enters an infinite loop or any blocking state, it cannot send a signal in time to clear the "watchdog" timer. When the timing is over, the "watchdog" will send a "watchdog" to the system through another pin. Reset signal" to restart the system.

watchdog_main is mainly used as a timer, and DEV_NAME is that pin. The main operation is to "feed the dog" and write a data reset signal to DEV_NAME.

File path: source/system/core/init/watchdogd.cpp

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

...

while (true) {
    write(fd, "", 1);
    sleep(interval);
}

2.2.3、install_reboot_signal_handlers

File path: source/system/core/init/init.cpp

if (REBOOT_BOOTLOADER_ON_PANIC) {
    install_reboot_signal_handlers(); 
}

REBOOT_BOOTLOADER_ON_PANIC is defined in the mk file of the top-level init module. Userdebug and eng version firmware will enable this option.

The main function is: when the init process crashes, restart the BootLoader to make it easier for users to locate the problem.

The install_reboot_signal_handlers function sets the behavior of various semaphores, such as SIGABRT, SIGBUS, etc. to SA_RESTART, and restarts the system once these signals are monitored.

2.3, the first stage of the init process

In the init code, execute the two-way code according to the environment variable INIT_SECOND_STAGE. After the first execution is completed, set the value of INIT_SECOND_STAGE to true, and then execute the init program again and take the code of the second branch. In the following records, the first stage of init execution is called kernel mode execution, and the second stage of execution is called user mode execution.

2.3.1, mount the basic file system and create a directory

File path: File path: source/system/core/init/init.cpp.

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,清空访问权限屏蔽码

    // 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");
    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/random", S_IFCHR | 0666, makedev(1, 8));
    mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));

    ...
}
...

is_first_stage

The main function of init will be executed twice, controlled by the is_first_stage environment variable.

Mount the basic file system

During the execution of the android init process in the kernel mode, the basic file system needs to be mounted.

Introductory description of the file system-related functions can be viewed here

Among them, the /dev/ partition is a temporary file system tmpfs, which uses RAM to store all files in virtual memory. It is mainly used to create and store device file nodes. This partition can be dynamically adjusted as needed.

The /sys/ partition uses the sysfs file system to organize the devices and buses connected to the system into a hierarchical file so that they can be accessed in user space.

The /proc/ partition uses the proc file system. The proc file system is a very important virtual file system, which can be seen as an interface to the internal data structure of the kernel. Through it we can obtain system information, and can also modify specific kernel parameters at runtime.

selinuxfs is a virtual file system, usually mounted in /sys/fs/selinux, used to store SELinux security policy files.

2.3.2, initial log output

File path: File path: source/system/core/init/init.cpp

// Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
// talk to the outside world...
InitKernelLogging(argv);

The purpose of this sentence is to use the KernelLogger function as a log processing function. The main function of KernelLogger is to format the output log and write it to the /dev/kmsg device.

2.3.3. Mount system partitions such as system and vendor (DoFirstStageMount)

File path: File path: source/system/core/init/init_first_stage.cpp.

bool DoFirstStageMount() {
    // Skips first stage mount if we're in recovery mode.
    if (IsRecoveryMode()) {
        LOG(INFO) << "First stage mount skipped (recovery mode)";
        return true;
    }

    // Firstly checks if device tree fstab entries are compatible.
    if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) {
        LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
        return true;
    }

    std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();
    if (!handle) {
        LOG(ERROR) << "Failed to create FirstStageMount";
        return false;
    }
    return handle->DoFirstStageMount();
}

The Android8.1 system transplants the mounting functions of the system and vendor partitions to the kernel device-tree.

In the dts file of the kernel, the following firmware partition mounting nodes need to be included. The partition mounting information recorded in the device-tree will be checked and read during the execution of the DoFirstStageMount function.

firmware {
    android {
        compatible = "android,firmware";
        fstab {
            compatible = "android,fstab";
            system {
                compatible = "android,system";
                dev = "/dev/block/by-name/system";
                type = "ext4";
                mnt_flags = "ro,barrier=1,inode_readahead_blks=8";
                fsmgr_flags = "wait";                                                                                       
            };  
            vendor {
                compatible = "android,vendor";
                dev = "/dev/block/by-name/vendor";
                type = "ext4";
                mnt_flags = "ro,barrier=1,inode_readahead_blks=8";
                fsmgr_flags = "wait";
            };  
        };  
    };  
};

is_android_dt_value_expected

// Firstly checks if device tree fstab entries are compatible.
if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) {
    LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
    return true;
}

The android device-tree directory is under /proc/device-tree/firmware/android by default. If the kernel startup parameter /proc/cmdline contains the setting of the androidboot.android_dt_dir value, use it directly.

First confirm whether the attribute value of the fstab/compatible directory under the android device-tree directory is "android,fstab".

The organization form of the dts compatible node is ,. android, fstab represents the execution function of fstab partition mounting.

handle->DoFirstStageMount

File path: File path: source/system/core/init/init_first_stage.cpp.

FirstStageMount::FirstStageMount()
    : need_dm_verity_(false), device_tree_fstab_(fs_mgr_read_fstab_dt(), fs_mgr_free_fstab) {
    if (!device_tree_fstab_) {
        LOG(ERROR) << "Failed to read fstab from device tree";
        return;
    }
    // Stores device_tree_fstab_->recs[] into mount_fstab_recs_ (vector<fstab_rec*>)
    // for easier manipulation later, e.g., range-base for loop.
    for (int i = 0; i < device_tree_fstab_->num_entries; i++) {
        mount_fstab_recs_.push_back(&device_tree_fstab_->recs[i]);
    }
}

bool FirstStageMount::DoFirstStageMount() {
    // Nothing to mount.
    if (mount_fstab_recs_.empty()) return true;

    if (!InitDevices()) return false;

    if (!MountPartitions()) return false;

    return true;
}

In the constructor of FirstStageMount, the fs_mgr_read_fstab_dt function is used to read the distribution mounting information in the /proc/device-tree/firmware/android/fstab directory, and finally the statistics are made into a vector array of fstab_rec type;

struct fstab_rec {
    char* blk_device;
    char* mount_point;
    char* fs_type;
    unsigned long flags;
    char* fs_options;
    int fs_mgr_flags;
    char* key_loc;
    char* key_dir;
    char* verity_loc;
    long long length;
    char* label;
    int partnum;
    int swap_prio;
    int max_comp_streams;
    unsigned int zram_size;
    uint64_t reserved_size;
    unsigned int file_contents_mode;
    unsigned int file_names_mode;
    unsigned int erase_blk_size;
    unsigned int logical_blk_size;
};

The MountPartitions() function traverses the fstab_rec array, finds mount_source and mount_target, and uses the mount function to mount the system, vendor or oem partition.

The successfully mounted Log prints as follows:

[    1.608773] init: [libfs_mgr]__mount(source=/dev/block/by-name/system,target=/system,type=ext4)=0: Success
[    1.611679] init: [libfs_mgr]__mount(source=/dev/block/by-name/vendor,target=/vendor,type=ext4)=0: Success

2.3.4, start SELinux security policy

SELinux, short for [Security-Enhanced Linux], is an extended mandatory access control security module of Linux developed by the US National Security Agency and SCC (Secure Computing Corporation). Under the restriction of this access control system, a process can only access the files needed in its tasks.

The specific application of SELinux in Android can be clicked on Android 8.1 Security Mechanism-SEAndroid & SELinux

For specific source code analysis, see the introduction in section 4.2.

2.3.5, start the second phase of user mode execution preparation

The main thing here is to set some variables such as INIT_SECOND_STAGE, INIT_STARTED_AT to prepare for the second stage, and then call the main function of init again to start the user-mode init process.

if (is_first_stage) {
    ...

    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", StringPrintf("%" PRIu64, start_ms).c_str(), 1);//记录第二阶段开始时间戳

    char* path = argv[0];
    char* args[] = { path, nullptr };
    execv(path, args); //重新执行main方法,进入第二阶段

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

summary

The main work done in the first stage of the init process is:

  1. Mount the partition dev, system, vendor;
  2. Create device node and device node hot plug time monitoring processing (ueventd);
  3. Create some key directories and initialize the log output system;
  4. Enable SELinux security policy.

 

Guess you like

Origin blog.csdn.net/u014674293/article/details/105860356