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.
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.
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
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
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.
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.
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
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.