building and booting my own kernel on arch linux - part two
what do grown men wish for? wallpapers? drugs? rock-music (well, a little) idk. but the correct answer is building and booting your very own minimal linux distro.
if you follow the given guide below for word by word, you will end up with a minimal linux distro that boots using your kernel. here's obligatory screenshot of babyos (bro, tell me i am not good at naming things) running in qemu:
.
well, part one was a sick learning experience. if you haven't read it yet and somehow ended up here, i recommend checking it out first: building and booting my own kernel on arch linux.
i usually ssh into my arch linux machine from my main laptop, because well it's cold winters. i don't want to get out of my warm blankets, so the only terminal i use is kitty from my laptop, but today i decided to sit on the chair and use my arch machine directly and baby, i have lost the ability to type on a complete black screen. i love terminal and no that's not what i am talking about. i need a wallpaper now.
i am right now using the cosmic de (been testing it for a while now, hopefully a blog about it too some day), but the default cosmic terminal has a black background. i can change the background color, i think. let me see... no i can't. that too is not implemented yet. nor do i think it has a schema support for changing basic things. i might have contributed to it, but well, rust. it has support for themes. my bad.
after understanding about the kernel configuration and how to build a basic kernel and boot it using qemu in part one. the question was what now? well, i wanted to build my own minimal distro that boots using my custom kernel, so let's get started for it.
(jokes apart, i have always wanted something like bharatOS/indiaOS but well, i don't know how that will work out. there already exists something that is called boss linux and it is funded by the government of india, but well, i don't know how that is going. i even mailed them once asking for contributing, but they have never replied back. sad! nor do they have a public repo. i mean i get it, it's just a debian fork, but well, open source is about collaboration and sharing knowledge. not just making a distro and keeping it to yourself. the iso files are available for download on their website, but that's it.)
building babyos: let's make a minimal distro
i'd love the chance to revise a few things first. busybox is a single binary that acts like multiple core unix commands. it is often used in embedded systems and minimal linux distributions. one of the few distro that includes/uses busybox is puppy linux (though i remember reading, there's a catch to that too).
also another thing is initramfs. initramfs is a temporary root filesystem that is loaded into memory during the boot process. it contains the necessary files and drivers needed to mount the root filesystem and start the init process. in layman's terms, it solves the chicken and egg problem of needing drivers to access the root filesystem, but needing the root filesystem to load the drivers.
now, let's get started with building babyos. i assume that you have already built your custom kernel as explained in part one. if not, please do so before proceeding. i am open to any questions about anything/regarding the post. contact me via email given in the contact section or ping me on twttr @dhrm1k.
step 1: verify you have everything
by default the kernel build process creates a bzimage file in the yourfolder/arch/x86/boot/bzImage. this is the kernel image that we will use.
confirm that you have the other necessary packages installed too:
which qemu-system-x86_64 || echo "qemu missing"
which busybox || echo "busybox missing"
busybox --help | head -n 3
now, create a folder for babyos and navigate into it:
mkdir -p ~/babyOS/rootfs
cd ~/babyOS/rootfs
step 2: create the rootfs skeleton
what is rootfs? rootfs is the root filesystem that will be used by the kernel during boot. we will create a minimal rootfs and give it utilities using busybox.
mkdir -p bin sbin lib lib64 usr/bin usr/sbin usr/lib
mkdir -p etc dev proc sys tmp var home root
chmod 755 tmp
step 3: populate rootfs with busybox
now, we will copy busybox binary to the rootfs and create necessary symlinks for the commands provided by busybox (under the assumption that you have already installed it).
cp /usr/bin/busybox bin/
cd bin
for i in $(./busybox --list); do ln -s busybox $i 2>/dev/null; done
cd ../..
i'd like to focus more on why we create this symlink and how it works. essentially, these symlinks tell the kernel that whenever a command is called (ls, cat, etc.), it should execute the busybox binary with the name of the command as an argument. busybox then interprets this argument and executes the corresponding functionality.
fun fact: the os for routers which is quite popular, openwrt, also uses busybox for providing basic unix commands.
step 4: setting up config files (passwd, group, inittab)
here we will give our operating system configuration files like the user database, group database and inittab.
the password database file (/etc/passwd) contains information about the user accounts on the system. we will create a minimal passwd file with a room for the root user. it tells the system every time it boots up that there is a root user with a uid of 0 and a home directory of /root.
another fun fact: recently i was working on a jail shell. what a jail shell is that it restricts a particular user to a specific directory and prevents them from accessing files outside that directory. while doing that, i had to edit the /etc/passwd file to add a new user for the jail shell. it was quite a fun experience. the problem with not doing it as a jail shell and customly parsing the commands that the user enters is that the tab autocomplete and other shell features won't work properly, so i had to create a proper user in the passwd file.
back to here to creating the passwd file:
cat > etc/passwd <<EOF
root::0:0:root:/root:/bin/sh
EOF
the format here for creating the user is as follows:
username:password:uid:gid:comment/user_description:home_directory:shell
the uid of the root user is always 0. the password field right now is left empty for simplicity, but in a real-world scenario, you should set a proper password. the gid is also 0 for the root user.
now, let's create the group database file (/etc/group). the file contains information about the groups on the system. understanding user sounds intuitive consider most operating systems have it, but you must be wondering what groups are. groups are a way to organize users and manage permissions. for example, you can have a group for administrators, another for regular users. each user can belong to one or more groups. this helps in managing permissions and access control.
let's create a minimal group file with a room for the root group:
cat > etc/group <<EOF
root::0:
EOF
the format here for creating the group is as follows:
group_name:password:gid:user_list
one of the thing that's on my mind though is that if we need the group file at all, considering that we have only one user (root) and one group (root). if a system boots up without a group file. well, i guess we will find out later.
added later: well, it does seem to boot up without any issues even if the group file is not present.
now, let's create the inittab file (/etc/inittab). inittab is a configuration file that tells the init process what to do during the boot process. it defines the runlevels and the processes that should be started.
in the previous part, we discussed that initramfs is a temporary root filesystem that is loaded into memory during boot. initramfs is loaded into ram and is not persistent. once the kernel has booted and the real root file system is mounted, the initramfs is no longer needed and is discarded, whereas the rootfs we are creating is the actual root filesystem that will be used by kernel after booting.
let's create a minimal inittab file that starts a shell on boot:
cat > etc/inittab <<EOF
::sysinit:/etc/init.d/rcS
ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100
EOF
the format here for creating the inittab entries is as follows:
id:runlevels:action:process, so ::sysinit:/etc/init.d/rcS means that run /etc/init.d/rcS one time at boot before anything else.
also, line 2 is the login terminal configuration. it tells the init process to start a getty process on ttyS0 (the first serial terminal) with a baud rate (speed. think of it as without it being exact, the letters on screen will not be formatted proper) of 115200 and using the vt100 (a terminal emulation type).
create the rcS init script
mkdir -p etc/init.d
cat > etc/init.d/rcS <<'EOF'
#!/bin/sh
# rcS - minimal startup script for BabyOS v0.1
echo "=== BabyOS rcS: starting ==="
# mount virtual filesystems the kernel exposes
mount -t proc none /proc || echo "mount /proc failed"
mount -t sysfs none /sys || echo "mount /sys failed"
# dev: prefer devtmpfs (kernel-managed), fall back to mdev if not available
mount -t devtmpfs none /dev 2>/dev/null || {
echo "devtmpfs not available, falling back to mdev"
/bin/mdev -s
}
# create basic device nodes if kernel didn't (safe-guard)
[ -e /dev/console ] || /bin/mknod -m 600 /dev/console c 5 1
[ -e /dev/null ] || /bin/mknod -m 666 /dev/null c 1 3
[ -e /dev/tty0 ] || /bin/mknod -m 666 /dev/tty0 c 4 0
# bring up loopback (important for some network stacks)
ifconfig lo up
# optional: set hostname (you can edit this later)
echo "baby-os" > /proc/sys/kernel/hostname
# tune kernel logging (reduce spam)
dmesg -n 1
# run any other one-shot startup tasks you want to add
# e.g. mount extra filesystems, set timezone, start network later
# /bin/mount -a
echo "=== BabyOS rcS: initialization complete ==="
EOF
chmod +x etc/init.d/rcS
this script is executed during the boot process and performs several important tasks to set up the system properly. what it does:
-
it mounts the virtual filesystem like
/procand/sys, which are essential for the kernel to expose information about processes and system hardware. -
mount -t devtmpfs none /dev. modern kernels create device nodes automatically via devtmpfs. if the kernel doesn't support/allow it, we fallback to runningmdev -s, which creates/devnodes from uevents. -
mknod lines ensure crucial device nodes exist in case neither devtmpfs nor mdev created them.
-
(this is not important right now, this only exists because i was trying something else earlier. and noted it down here.)ifconfig lo upbrings the loopback interface up; important for some programs and for DHCP tests later. -
echo "baby-os" > /proc/sys/kernel/hostnamesets a hostname quickly (optional). -
dmesg -n 1limits kernel log level printed to console (optional).
create nodes in /dev
now, we will create necessary device nodes in the /dev directory. device nodes are special files that represent hardware devices. they allow user-space programs to interact with hardware devices.
sudo mknod -m 600 rootfs/dev/console c 5 1
sudo mknod -m 666 rootfs/dev/null c 1 3
sudo mknod -m 666 rootfs/dev/tty c 5 0
sudo chown root:root rootfs/dev/* || true
more on device nodes:
device nodes are not real files. they are handles to the kernel to access hardware devices.
for example, /dev/console is the device node for the system console. without it, the early boot messages and login prompt won't work.
/dev/null is a special device that discards all data written to it. /dev/tty represents the current terminal.
c in the mknod command stands for character device. the number 5 1 for console means major number 5 and minor number 1. it's more complex than what i can comprehend or explain right now. i too will read more about it later and update this post or something.
creating rootfs.ext4 image
we need a real disk image for qemu to boot from. we will create an ext4 filesystem image and copy our rootfs into it.
dd if=/dev/zero of=rootfs.ext4 bs=1M count=128
mkfs.ext4 rootfs.ext4
line 1 of the following command creates a blank 128MB file filled with zeros. line 2 formats that file as an ext4 filesystem. i myself had very little idea about filesystems before starting this post. also how helpful this dd utility is.
mount and populate the disk image
cd ~/babyOS
mkdir -p mnt
sudo mount rootfs.ext4 mnt
sudo cp -a rootfs/* mnt/
sync
sudo umount mnt
boot babyos with qemu
qemu-system-x86_64 \
-kernel ~/src/linux/arch/x86/boot/bzImage \
-drive file=rootfs.ext4,format=raw \
-append "root=/dev/sda console=ttyS0" \
-nographic
with this setup the os will boot up and you will be presented with a login prompt on the serial console. you can log in as root. if you had the same config, then you won't have to set up any password.
once logged in, you can try out some basic commands provided by busybox.
you won't be able to write to the root filesystem right now because it is mounted as read-only. to remount it as read-write, you can use the following command:
mount -o remount, rw / || echo "remount / rw failed
also, later your qemu run command has to be modified to include rw in the kernel parameters like so:
qemu-system-x86_64 \
-kernel ~/src/linux/arch/x86/boot/bzImage \
-drive file=rootfs.ext4,format=raw \
-append "root=/dev/sda rw console=ttyS0" \
-nographic
tip: i had to google it on how to exit qemu. it's ctrl + a, then x.
this will remount the root filesystem in read-write mode during boot.
ascii art
can anything be over without ascii art? no. so here it is:
from what i read, after busybox login, /etc/motd is displayed. motd stands for message of the day. so let's create one:
(the ascii art didn't work from etc/motd. it is probably because the tty is not configured properly. i will fix it later. for now, i am putting it in /etc/init.d/rcS to display it on every boot.)
cat > /etc/motd << "EOF"
o o
O O
O O
o o
OoOo. .oOoO' OoOo. O o .oOo. .oOo
O o O o O o o O O o `Ooo.
o O o O o O O o o O O
`OoO' `OoO'o `OoO' `OoOO `OoO' `OoO'
o
OoO'
EOF
i changed the ascii art in the screenshot because i had no energy to deal with formatting it properly.
i enjoyed writing this post. it was an experience.