To debug a problem with my self-built kernel and initramfs, I took a look at the relevant kernel code. It is not a big surprise that understanding the process was not that easy. Hence, I made notes while digging through the code and wrote the following article afterwards with links to the great free-electrons website which greatly simplifies following the code online.

One of the earliest functions executed by the kernel is init/main.c:start_kernel(). Afterwards, init/main.c:rest_init() is called which starts a new kernel thread that executes init/main.c:kernel_init() in line init/main.c:397. Before we return to this function, init/main.c:kernel_init_freeable() is called that, among other things, calls do_basic_setup() in init/main.c:1008 which, after some more intermediate functions, calls init/initramfs.c:populate_rootfs() that is responsible for loading an initramfs included in the kernel or from a separate memory location, e.g., set up by a bootloader.

After initializing the virtual root filesystem with the initramfs, init/main.c:kernel_init_freeable() tries to open the console device in line init/main.c:1011:

if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
   pr_err("Warning: unable to open an initial console.\n");

Hence, the initial initramfs should contain this device, that can be created using:

mknod /dev/console c 5 1

Afterwards, kernel_init_freeable() checks if ramdisk_execute_command is set and sets it to /init if not in line init/main.c:102:

if (!ramdisk_execute_command)
 ramdisk_execute_command = "/init";

ramdisk_execute_command is initially set by the function init/main.c:rd_init_setup() which is called by __setup("rdinit=", rdinit_setup); if the rdinit parameter is set in the kernel commandline. Afterwards, the kernel checks if this script exists and if not, unsets ramdisk_execute_command and calls init/do_mounts.c:prepare_namespace() which tries to mount the actual root filesystem in line init/main.c:1024:

if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
   ramdisk_execute_command = NULL;
   prepare_namespace();
}

If the script exists or if the kernel was able to mount the root filesystem, the function returns to kernel_init() and executes the command in the initramfs, if it is available, in line init/main.c:947:

ret = run_init_process(ramdisk_execute_command);

If ramdisk_execute_command is not set or if its execution fails, the kernel tries to start userspace from the root filesystem mounted by init/do_mounts.c:prepare_namespace(). It either executes the script given in execute_command that, similar to ramdisk_execute_command, is initialized with the optional init= parameter, or it tries to execute a few predefined scripts (e.g. a shell) as it can be seen in the code from line init/main.c:954 and following:

/*
* 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.");

To make the whole process a bit clearer, I created a small diagram:

Other interesting articles: