Last time, we covered how to set up a development environment with the latest kernel using QEMU. But after trying it for a while, I found it wasn’t very convenient.

More often than not, I just need to run a single program (like a unit test), and the full environment built in the previous article makes this process more complicated. This article introduces a simpler approach. A simple script is all it takes to build a complete runtime environment.

initrd

The previous article already mentioned using initrd to boot the operating system. An initrd is a read-only filesystem image that the kernel loads into memory at boot time. Programs can access it like a regular filesystem.

After loading the filesystem, the kernel looks for an available init program inside it. So all we need to do is write a proper init program and package it into an initrd.

busybox

In this from-scratch filesystem-building scenario, using dynamic libraries is too cumbersome. So we’ll use busybox from the host directly to provide basic utilities.

Preparation

First, create a WORK_DIR directory. Files under this path will be packaged into the filesystem image.

WORK_DIR=$(mktemp -d)
trap "rm -rf $WORK_DIR" EXIT

Then find busybox on the host and copy it to $WORK_DIR/bin/busybox.

mkdir -p "$WORK_DIR/bin"

# Copy busybox into the work directory
BUSYBOX_PATH=$(which busybox)
if [ -z "$BUSYBOX_PATH" ]; then
    echo "Error: busybox not found in PATH."
    exit 1
fi
cp "$BUSYBOX_PATH" "$WORK_DIR/bin/busybox"

Create the $WORK_DIR/init script and make it executable. The kernel will recognize and execute this script during boot.

# Create init script
cat << 'EOF' > "$WORK_DIR/init"
#!/bin/busybox sh

# ...

EOF
chmod +x "$WORK_DIR/init"

The init Script

Now let’s look at the contents of $WORK_DIR/init. First, declare that this script is executed by /bin/busybox sh.

#!/bin/busybox sh

Next, create the usual filesystem paths and mount the three special filesystems: proc, sys, and dev.

# Initialize minimal directories
busybox mkdir -p /etc /proc /root /sbin /sys /usr/bin /usr/sbin

# Mount necessary filesystems
busybox mount -t proc proc /proc
busybox mount -t sysfs sys /sys
busybox mdev -s

Install the tools provided by busybox. This creates symbolic links from tool names to busybox.

# Install busybox applets
busybox --install -s

Set up the user. root user, uid=0, gid=0, no password, home at /root, shell at /bin/sh.

# Create a minimal passwd file
echo root::0:0:root:/root:/bin/sh > /etc/passwd

Bring up the loopback interface.

# Set up loopback interface
hostname localhost
ip link set lo up

Reduce kernel log verbosity. Log in as root. On exit, power off.

# Reduce kernel printk verbosity
echo 5 > /proc/sys/kernel/printk

# Start an interactive shell
login root

# Power off
poweroff -f

Building the initrd Image

Next, build the initrd image.

$FILES defines a series of additional files to place into the image. Note: if you need to run a program, it should be statically linked.

# Copy user-specified files into /root
mkdir -p "$WORK_DIR/root"
for FILE in $FILES; do
    BASENAME=$(basename "$FILE")
    cp "$FILE" "$WORK_DIR/$DEST_DIR/$BASENAME"
done

Finally, use the cpio command to convert WORK_DIR into an initrd filesystem. All files are owned by root.

# Create the initrd image
OUTPUT_FILE_REAL=$(realpath "$OUTPUT_FILE")
cd "$WORK_DIR"
find . | cpio -o -H newc -R 0:0 | gzip -9 > "$OUTPUT_FILE_REAL"
echo "Created initrd image: $OUTPUT_FILE"

Running

Run with the following command:

qemu-system-x86_64 \
    -m 512M \
    -kernel /path/to/bzImage \
    -initrd /path/to/initrd \
    -append "console=ttyS0" \
    -nographic

One more plug for my new project Condy :)