When the Linux kernel finishes booting, it needs to know what to run next. What process will be the “manager” of all of the other userland processes? This process, called the “init process” or “PID 1” is commonly part of a larger “init system”. There are several. Mostly only SysV Init and systemd are worth mentioning, though.

The first order of business is to do something that I can’t believe the kernel doesn’t do for you: Mounting /proc, /sys, and /dev. (The kernel does most of the work, in fact, you just get to push the button that says “go”).

Here’s a minimal example of an init process which mounts these three directories/pseudo-directories:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <unistd.h>
 
void mount_proc();
void mount_sys();
void mount_dev();
void run_terminal();
 
void mount_proc() {
    printf("Mounting /proc...");
    if (0 != mount("none", "/proc", "proc", 0, "")) {
        printf("\nFailed to mount /proc!\n");
        exit(1);
    }
    printf("Done!\n");
}
 
void mount_sys() {
    printf("Mounting /sys...");
    if (0 != mount("none", "/sys", "sysfs", 0, "")) {
        printf("\nFailed to mount /proc!\n");
        exit(1);
    }
    printf("Done!\n");
}
 
void mount_dev() {
    printf("Mounting /dev...");
    if (0 != mount("none", "/dev", "devtmpfs", 0, "")) {
        int errno2 = errno;
        // Error code 16 ("device or resource busy") is spurrious and doesn't really matter. Ignore it.
        if (errno2 != 16) {
            printf("\nFailed to mount /dev! errno=%d strerror=%s\n", errno, strerror(errno));
            exit(1);
        }
    }
    printf("Done!\n");
}
 
void run_terminal() {
    execl("busybox-x86_64", "busybox-x86_64", "ash", NULL);
}
 
int main(int argc, char *argv[]) {
    printf("Hello from the init system!\n");
 
    mount_proc();
    mount_sys();
    mount_dev();
 
    run_terminal();
 
    return 0;
}

To compile it, be sure to use -static so gcc won’t try to use any shared libraries:

1
gcc -static main.c -o ./boot_disk/init

You can put this file on a drive image and load it into QEMU, and specify it as the init= part of the Linux kernel parameters (in GRUB2 or directly through QEMU). It’s also handy to have busybox on the drive, and I left an example of how to invoke it in the code above. That way you can drop into the busybox shell and see the mounted filesystem: