Building Barebones Linux

I do not intend to make this particular post a tutorial, demonstration, or any kind of reference or guideline, though it may end up looking like one.


Here I will be exploring the assembly of Linux from zero to functioning system. By the end of this, in an ideal world, I’d like to be able to do some bare minimum things such as multiuser, SSH, a webserver, networking, disk access, NFS, etc.

For my first experiments, I will be using QEMU to test due to its convenient bootloader functionality, and the fact I don’t want to be wasting time booting a physical machine to a broken OS every time I do a tweak.

Observations and Explanations

This section will be where I stick my observations, and where applicable, explanations in a Q&A style.

O1: When booting the kernel, /dev contains one node, /dev/console without any intervention. Mounting a tmpfs to /dev removes /dev/console .

E: Because of the mknod major and minor numbers, it seems the kernel already knows what the devices are called (seemingly from their driver’s name) and console is just populated by default, sort of like /root . A tmpfs is pretty useless, but if we mount a devtmpfs to /dev (as long as it is supported by the kernel) the kernel appears to populate /dev with all devices it knows exists already.

O2: It appears /root is created automatically?

E: None yet

O3: Kernel cmdline lets you specify ttyS0 when that node isn’t even created at /dev/ttyS0 as we aren’t even at init yet. Then when init is complete, /dev/ttyS0 doesn’t exist unless init creates it, we also haven’t chosen the name to be ttyS0 either!

E: See O1‘s explanation

Building the Kernel

I am building the Linux kernel from source code using x86_64_defconfig. My procedure for preparing and proceeding with a kernel build is as follows:

#Install compilation dependencies
sudo apt-get install git make gcc libssl-dev libelf-dev libncurses-dev flex bison -y

#Clean directory to work in
mkdir linuxdev
cd linuxdev

#Download and expand kernel, then enter its directory
tar -xf linux-5.14.10.tar.xz
cd linux-5.14.10

#Set up default config and then begin compilation
make x86_64_defconfig

Setting up Directories and Scripts

I will now set up the directory in which I will build my linux system, and I will start my creating some directories and scripts to make repetitive things quicker:

#Assuming we just finished the kernel build, so are still in the kernel folder
cd ..

#Make our system directory and enter it
mkdir linuxsystem
cd linuxsystem

#Make our initrd directory
mkdir myrd

Lets make a simple script to package a directory as our initrd called with the following contents:

cd myrd
find . | cpio -v -H newc -o -F ../myrd.cpio
cd ..
cat myrd.cpio | xz --check=crc32 -9 > myrd.cpio.xz

Lets also make another script called which just defines our qemu startup parameters:


qemu-system-x86_64 -kernel my.knl -initrd myrd.cpio.xz -nographic -no-reboot -m 128 -append "panic=1 console=ttyS0 noapic"

The contents of the launch script will likely change throughout this. If I use different parameters, I will specify the full startup line.

We also need to actually install qemu and also make these scripts executable, install the execution environment and copy our compiled kernel locally:

#Install QEMU
sudo apt-get install qemu qemu-kvm -y

#Make scripts executable
chmod +x
chmod +x

#Copy kernel
cp ../linux-5.14.10/arch/x86_64/boot/bzImage ./my.knl

Building Busybox

As the basis of this system, I’d like to use Busybox. For this, I will also compile it myself. Lets clone it:

#Assuming we just finished creating the two scripts, so are still in our system's folder
cd ..

#Clone Busybox
git clone

Now we have busybox cloned, let’s set up a default config. We will also need to do a quick tweak to produce a statically linked binary, as dependency libraries will not be included in our Linux system:

#Go into Busybox's cloned directory
cd busybox

#Make a default config
make defconfig

#Enable static linking
make menuconfig

Now you are in the menuconfig, you need to go to Settings -> Build Options -> Build static binary (no shared libs) and make sure it is switched on. Then exit menuconfig and choose Yes to saving. Now we can run the compile:

#Run the compiler

Laying out the initrd

Go into the myrd directory we created inside of the linuxsystem directory earlier:

#Assuming we just finished making Busybox, head down to linuxsystem then down to myrd
cd ../linuxsystem/myrd

We will now lay out some very basic folders and set up Busybox and init.

Create some important directories familiar in Linux:

mkdir usr
mkdir bin
mkdir sbin
mkdir usr/bin
mkdir usr/sbin
mkdir etc
mkdir etc/init.d

We will now copy the Busybox binary we created earlier into the initrd and link it as the init also:

#Copy Buxybox in
cp ../../busybox/busybox bin/busybox

#Make it also the init executable by symlinking it. Remember, symlinks are relative to the mounted rootfs
ln -s /bin/busybox init

We will now make the script which will initialise the system. Busybox, as it is linked to /init , will initialise the system and then run the initialisation script we create. Open the init script with nano etc/init.d/rcS and fill it with the following contents:

#!/bin/busybox sh

#Set the PATH environment variable

#Let Busybox set itself up to make all executables it contains available nativelt
/bin/busybox --install

#Mount the kernel's virtual filesystems
mkdir /proc
mkdir /sys
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev

Save and exit, then mark the script as executable with chmod +x rcS .

We now need to set the file permissions correct for the initrd. Change the owner of all files to root with chown -R root:root ./* .

Build and Launch Your System

Finally, pack your initrd using the script we created earlier, then launch your QEMU VM to demo your system:

#Assuming you're still in mrd whilst we were setting it up
cd ..

#Pack your initrd

#Launch your QEMU VM

To be continued

0 0 votes
Article Rating
Inline Feedbacks
View all comments