← Back to Linux Inside Out
Part 1 of 1 in Linux Inside Out

The Linux kernel is just a program

Mon Dec 01 2025

Most books and courses introduce Linux through shell commands, leaving the kernel as a mysterious black box doing magic behind the scenes.

In this post, we will run some experiments to demystify it: the Linux kernel is just a binary that you can build and run.

The experiments are designed so you can follow along if you have a Linux PC. But this is completely optional, the goal is to build a mental model about how Linux works, seeing components of the system fit together.

But first let’s talk a few sentences about what is a kernel.

What is a kernel?

Computers are built from CPUs, memory, and other devices, like video cards, network cards, keyboards, displays, and a lot of other stuff.

These devices can be manufactured by different companies, have different capabilities, and can be programmed differently.

An operating system kernel provides a unified interface to use these devices conveniently and securely. Without one, if we wrote a program it’s likely that it wouldn’t work on other computers, we couldn’t run multiple programs at the same time, or have multiple users that can use the same computer.

A kernel

The closest analogy from the software development world is that the kernel is a runtime for our computer.

Where is the kernel?

On most Linux distributions we will find the kernel under the /boot directory. Let’s enter the directory and list its contents:

~$ cd /boot
/boot$ ls -1
System.map-6.12.43+deb13-amd64
System.map-6.12.48+deb13-amd64
config-6.12.43+deb13-amd64
config-6.12.48+deb13-amd64
efi
grub
initrd.img-6.12.43+deb13-amd64
initrd.img-6.12.48+deb13-amd64
vmlinuz-6.12.43+deb13-amd64
vmlinuz-6.12.48+deb13-amd64

We see a few files here, but the one we are looking for is vmlinuz-6.12.48+deb13-amd64. This single file is the kernel.

If you ever wondered what this name means:

Let’s start the kernel

In our first experiment we will copy this kernel into another directory and run it.

First, let’s create a directory and copy the kernel there:

/boot$ cd
~$ mkdir linux-inside-out
~$ cd linux-inside-out/
~/linux-inside-out$ cp /boot/vmlinuz-6.12.48+deb13-amd64 .
~/linux-inside-out$ ls -lh
total 12M
-rw-r--r-- 1 zsoltkacsandi zsoltkacsandi 12M Dec  1 09:44 vmlinuz-6.12.48+deb13-amd64

Then install some tools that are needed for this experiment. We will use QEMU, a virtual machine emulator, because our kernel needs something that works like a computer, and because we do not want to mess up our original operating system.

~$ sudo apt update
~$ sudo apt install -y qemu-system-x86 qemu-utils

Then start a virtual machine with our kernel:

~/linux-inside-out$ qemu-system-x86_64 \
  -m 256M \
  -kernel vmlinuz-6.12.48+deb13-amd64 \
  -append "console=ttyS0" \
  -nographic

The output should be something like this:

SeaBIOS (version 1.16.3-debian-1.16.3-2)
iPXE (https://ipxe.org) 00:03.0 CA00 PCI2.10 PnP PMM+06FC6D30+06F06D30 CA00
Booting from ROM...
Probing EDD (edd=off to disable)... o
[    0.000000] Linux version 6.12.48+deb13-amd64 ([email protected]) (x86_64-linux-gnu-gcc-14 (Debian 14.2.0-19) 14.2.0, )
[    0.000000] Command line: console=ttyS0
...
[    2.055627] RAS: Correctable Errors collector initialized.
[    2.161843] clk: Disabling unused clocks
[    2.162218] PM: genpd: Disabling unused power domains
[    2.179652] /dev/root: Can't open blockdev
[    2.180871] VFS: Cannot open root device "" or unknown-block(0,0): error -6
[    2.181038] Please append a correct "root=" boot option; here are the available partitions:
[    2.181368] List of all bdev filesystems:
[    2.181477]  fuseblk
[    2.181516]
[    2.181875] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
[    2.182495] CPU: 0 UID: 0 PID: 1 Comm: swapper/0 Not tainted 6.12.48+deb13-amd64 #1  Debian 6.12.48-1
[    2.182802] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
...
[    2.186426] Kernel Offset: 0x30e00000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[    2.186949] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---

You can exit with pressing Ctrl + A then X.

So, we’ve just started the same kernel that is running on our computer. It took 2 seconds, it printed a lot of log messages, then panicked.

This panic is not a bug, actually this is expected - once our kernel initializes itself, it tries to mount the root filesystem, and hand over the control to a program called init.

So let’s give one to it.

We will write a simple program in Golang that we will use as an init program.

~/linux-inside-out$ sudo apt -y install golang
~/linux-inside-out$ mkdir init
~/linux-inside-out$ cd init
~/linux-inside-out/init$
~/linux-inside-out/init$ go mod init init
go: creating new go.mod: module init
go: to add module requirements and sums:
go mod tidy

The code of the program:

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    fmt.Println("Hello from Go init!")
    fmt.Println("PID:", os.Getpid()) // printing the PID (process ID)

    for i := 0; ; i++ { // every two seconds printing the text "tick {tick number}"
        fmt.Println("tick", i)
        time.Sleep(2 * time.Second)
    }
}

Let’s build the program and run it:

~/linux-inside-out/init$ CGO_ENABLED=0 go build -o init .
~/linux-inside-out/init$ ./init
Hello from Go init!
PID: 3086
tick 0
tick 1

As you can see, this is a regular program that got the PID 3086 and prints some texts to the output. There is nothing special about it.

Now we create a simple initramfs filesystem. When the kernel starts it does not have all of the parts loaded that are needed to access the disks in the computer, so it needs a filesystem loaded into the memory called initramfs (Init RAM filesystem).

~/linux-inside-out$ mkdir -p rootfs/{proc,sys,dev}
~/linux-inside-out$ cp ./init/init rootfs/init
~/linux-inside-out$ sudo mknod rootfs/dev/console c 5 1
~/linux-inside-out$ sudo mknod rootfs/dev/null c 1 3

The cp and mkdir commands might be familiar, the mknod command creates special files that programs use to communicate with hardware devices.

Our root filesystem directory structure looks like this:

|-- dev             # dev (devices) directory
|   |-- console     # console device
|   `-- null        # null device
|-- init            # our Golang program
|-- proc            # a directory called proc
`-- sys             # a directory called sys

Now let’s package the files into an archive file, called initramfs.img.

( cd rootfs && find . | cpio -H newc -o ) > initramfs.img

Then start a virtual machine again, with the kernel and initramfs:

qemu-system-x86_64 \
  -m 256M \
  -kernel vmlinuz-6.12.48+deb13-amd64 \
  -initrd initramfs.img \
  -append "console=ttyS0 rdinit=/init" \
  -nographic

SeaBIOS (version 1.16.3-debian-1.16.3-2)
iPXE (https://ipxe.org) 00:03.0 CA00 PCI2.10 PnP PMM+0EFC6D30+0EF06D30 CA00
Booting from ROM...
Probing EDD (edd=off to disable)... o
[    0.000000] Linux version 6.12.48+deb13-amd64 ([email protected]) (x86_64-linux-gnu-gcc-14 (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44) #1 SMP PR)
[    0.000000] Command line: console=ttyS0 rdinit=/init
...
[    1.922229] RAS: Correctable Errors collector initialized.
[    2.158525] clk: Disabling unused clocks
[    2.158865] PM: genpd: Disabling unused power domains
[    2.264545] Freeing unused decrypted memory: 2028K
[    2.327128] Freeing unused kernel image (initmem) memory: 4148K
[    2.406015] Write protecting the kernel read-only data: 28672k
[    2.407968] Freeing unused kernel image (rodata/data gap) memory: 488K
[    2.555150] x86/mm: Checked W+X mappings: passed, no W+X pages found.
[    2.557822] tsc: Refined TSC clocksource calibration: 2903.977 MHz
[    2.558399] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x29dbf0142be, max_idle_ns: 440795300983 ns
[    2.565700] clocksource: Switched to clocksource tsc
[    2.672446] Run /init as init process
Hello from Go init!
PID: 1
tick 0
tick 1
tick 2

Our kernel booted normally, then it started our Go program, the init process. A program that is running is called process.

There are a few important points to note here:

What we have learnt so far

We have already learnt quite a few important concepts that are essential to understand Linux systems: