Easily Creating A Linux Distribution

Posted on by Archie.

Hey everyone! Archie here. Today I decided to start a little project I have always wanted to do: create my own Linux distribution.

You don't actually have to know programming or stuff to create awesome stuff like this, though you learn a lot about operating system internals in the way, so it's really fun.

Now that I have decent hardware where I can work with stuff like this, it's time to get started!

I'm using Gentoo Linux as my host system (I mention this because of the compiler version string), and I'm gonna build the kernel from the official sources for version 6.1.177 LTS.

Compiling The Linux Kernel

First, let's start by compiling the Linux kernel. Open up the terminal emulator and create a new directory to store the components of our system:

mkdir -p ~/linux-sys
mkdir -p ~/linux-sys/kern

Enter the directory:

cd ~/linux-sys/kern

Now, we're gonna download the source code of any Linux kernel version using the GNU wget program. In this case, I randomly chose version 6.1.177.

wget -c -t0 https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.177.tar.xz

We need to extract the archive using tar xpvf in the same directory, and then remove the compressed file:

tar xpvf linux-6.1.177.tar.xz -C .
mv linux-6.1.177/ ~/linux-sys/kern
rm -rf linux-6.1.177/; rm linux-6.1.177.tar.xz

We'll create a default .config kernel configuration for 64-bit systems:

make x86_64_defconfig

We can customize our kernel version string by editing the Makefile file and modifying the EXTRAVERSION string, for example:

EXTRAVERSION = -archie

This will make our kernel version appear as:

Linux 6.1.177-archie

Open the kernel configuration menu to verify that everything is correct:

make menuconfig

Press save, confirm with Enter, and exit.

Compile the kernel:

make -j$(nproc)

That process will take some time. The kernel image will be located at the following path:

arch/x86/boot/bzImage

We can go ahead and test the newly compiled kernel using the Qemu processor emulator:

qemu-system-x86 -enable-kvm -display sdl -kernel arch/x86/boot/bzImage

If everything goes well, we should see our newly compiled kernel boot just fine.

However, it will fail when trying to mount the root filesystem, because we do not have an init system, or even userspace.

That's exactly what we are going to add now.

Configuring The Init System And Userspace

Now that we have the kernel and know that it boots correctly, we are gonna add something that can actually execute. That is called userspace, because userspace runs outside ring 0 (kernel level). Without the userspace, the kernel cannot do much aside from booting and managing our computer's resources (if it even finds the rootfs, which we saw that it doesn't, at first).

We're using BusyBox here, as it's minimal and simple, and that's the main goal of our Linux distribution.

BusyBox is a lightweight userspace tool that runs entirely outside the privileged kernel level. It uses symlinks mapped back to the main executable, providing constrained environments the functions of a fully featured Unix-like operating system.

Create a folder for it, download, and extract it:

mkdir -p ~/linux-sys/busybox
cd ~/linux-sys/busybox
wget -c -t0 https://busybox.net/downloads/busybox-1.38.0.tar.bz2
tar xpvf busybox-1.38.0.tar.bz2 -C .
rm busybox-1.38.0.tar.bz2

Create a default configuration:

make defconfig

Open the configuration menu:

make menuconfig

Go to Settings and enable:

Build static binary (no shared libs)

This is useful, because we do not need to depend on glibc yet.

Then go to Init Utilities and enable:

init (10 kb)

This enables a minimal init system to run, which we will configure later.

Go to Linux System Utilities and enable:

support gpt disklabels

This provides UEFI support. If we configure the kernel with GPT partition table support, this will allow our system to boot on modern computers.

Next up, we configure the command interpreter, which will be the Almquist shell, better known as ash.

Go to Shells and enable:

ash (80 kb)

This enables a minimal shell for interacting with the operating system.

Then, we're gonna enable the base for networking, so we go to Networking Utilities, disable tc.

Why disable tc? tc is a network traffic tool that depends on the kernel's networking extensions. It's useful for firewalls and stuff, but we don't actually need it for our system, and disabling it makes sure that there are no compile issues.

After disabling tc, enable udhcpd to provide networking support.

Now, scroll down to the bottom, and select:

save configuration to an alternate file

Press Enter twice there.

Then press Escape twice. When asked if you want to save the configuration, confirm with Return and choose Exit.

Now, we're gonna compile BusyBox:

make -j$(nproc)

We should now have a binary called busybox in the current directory. Let's verify that it is statically linked:

file busybox

It should return something similar to:

busybox: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=7c139ade4c115286e418703a162a58f8b69c6a3a, for GNU/Linux 3.2.0, stripped

Configuring The Init System

Create the root filesystem directory:

mkdir -p ~/linux-sys/rootfs

Install BusyBox:

make CONFIG_PREFIX=../rootfs install

Go to the root filesystem directory and create the required directories:

cd ../rootfs
mkdir -p dev proc sys etc tmp root

Now we're gonna configure the init. Create a script with the editor of your choice with the init name:

vim init

Add the following content to it:

#!/bin/sh

mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev

echo "Linux-6.1.177-archie."

exec /bin/sh

Add exec permissions:

chmod +x init

Now that we have successfully configured the init script, we will create the initramfs (system boot files and stuff).

cd ~/linux-sys/rootfs

Testing Our System

Go to the kernel directory:

cd ../kern/

Make sure the kernel image exists.

The make command should have already created it, but it is worth checking:

ls arch/x86/boot/bzImage

Now, we can test our system:

qemu-system-x86_64 \
-enable-kvm \
-display sdl \
-kernel arch/x86/boot/bzImage \
-initrd ../initramfs.cpio.gz

If everything went well, we should see the kernel booting, finding our rootfs, and starting the ash shell prompt.

Pressing Ctrl+D or typing exit will kill the init process and cause an instant kernel panic. Of course you can just add a while loop that spawns bin/sh indefinitely, but I haven't looked into it.

Also, we need to configure our kernel with virtual console control support (CONFIG_VT and CONFIG_VT_CONSOLE) for Ctrl-C to function properly.

Integrating Musl Libc

Now that we have a base, we're gonna integrate a C library into our operating system.

For the virtual machine kernel to link and execute binaries dynamically compiled against musl, it is mandatory to provide the physical path to the loader and its corresponding symlink.

We'll create a directory, enter it and download musl:

mkdir musl; cd musl
wget -c -t0 https://musl.libc.org/releases/musl-1.2.6.tar.gz

Then we're gonna extract it:

tar xzvf musl-1.2.6.tar.gz
mv musl-1.2.6/ ~/linux-sys/musl
rm -rf musl-1.2.6; rm musl-1.2.6.tar.gz

We're gonna create the lib directory:

mkdir ~/linux-sys/rootfs/lib
ln -sfv /lib/libc.so lib/ld-musl-x86_64.so.1

That's it, we installed and linked musl libc to our Linux distribution.

Installing The Alpine Package Keeper (APK) Package Manager

Now we're gonna add the package manager to actually install some stuff.

Download apk-tools-static:

wget https://dl-cdn.alpinelinux.org/alpine/edge/main/x86_64/apk-tools-static-3.0.6-r0.apk

Extract it:

tar xvf apk-tools-static-3.0.6-r0.apk

We're gonna see a "sbin" folder. Copy the binary to sbin/apk.static on our rootfs:

cp -v sbin/apk.static ~/linux-sys/rootfs/usr/bin/apk

Go to the rootfs directory, and recompile the initramfs:

cd ~/linux-sys/rootfs
find . -print0 | cpio --null -ov --format=newc -v | gzip -9 > ../initramfs.cpio.gz

Fire up Qemu:

qemu-system-x86_64 \
-enable-kvm \
-display sdl \
-kernel arch/x86/boot/bzImage \
-initrd ../initramfs.cpio.gz

The VM should boot, and now, apk is installed. But close Qemu for now, we're gonna configure apk:

Create the APK configuration directories:

cd ~/linux-sys/rootfs; mkdir -p lib/apk/db var/cache/apk etc/apk

Create the repositories file:

cd ~/linux-sys/rootfs
printf "http://alpinelinux.org/alpine/edge/main\n" > etc/apk/repositories
printf "https://dl-cdn.alpinelinux.org/alpine/edge/community\n" >> etc/apk/repositories

Create the installed and world package file:

printf "" > lib/apk/db/installed
printf "" > etc/apk/world

Initialize the databases:

cd usr/bin; ./apk --root . --update-cache update

Recompile the initramfs once again:

find . -print0 | cpio --null -ov --format=newc -v | gzip -9 > ../initramfs.cpio.gz

Fire up Qemu once again:

qemu-system-x86_64 \
-enable-kvm \
-display sdl \
-kernel arch/x86/boot/bzImage \
-initrd ../initramfs.cpio.gz

Networking Configuration In Qemu

Now, we need to configure the network. Open up the init script, and add:

ifconfig lo 127.0.0.1 up
ifconfig eth0 10.0.2.15 netmask 255.255.255.0 up
route add default gw 10.0.2.2 eth0
mkdir -p /etc
printf "nameserver 1.1.1.1\n" > /etc/resolv.conf

Add the following to the QEMU command line:

-netdev user,id=net0,hostname=linux-sys -device e1000,netdev=net0

Now we have working network inside the VM. Test it:

ping -c4 1.1.1.1

We should see a reply.

We're gonna update the package databases:

apk --allow-untrusted update

This should work without any errors.

That's a good starting point, we're running the initramfs from RAM, and the system is working, but it's extremely minimal. Any changes you make will be lost on the next boot (because we don't have any kinda bootloader yet), but from here we could go and take over the world haha, maybe we could add a login prompt, a tiny compiler, device management, a more complete init such as OpenRC or runit, and all the awesome bits and bytes and the little stuff that makes a system.

That's it for this post, hope it has been useful, love yaa!! <3

Go back to the blog's main index.