PumpkinOS, Busybox and Linux

PumpkinOS, in its current form, runs as a normal application on a host operating system, either Windows or a full GNU/Linux OS like Ubuntu. It is convenient because you can use the host OS as a development environment, where the build tool chain is installed. But I was wondering what it would take to run PumpkinOS on top of a (mostly) bare OS kernel, loosing all the unnecessary things than PumpkinOS does not need. The plan is to use QEMU to emulate a x86_64 guest system running a kernel, some basic utilities and PumpkinOS. To follow this article, some basic knowledge of Linux and Linux concepts (like filesystems, mount points, init system) are desirable.

Standalone PumpkinOS

For this test, I downloaded a recent Linux kernel (5.15.94) and compiled it from source on a Windows Subsystem for Linux host (why not a real Linux OS? I was already using WSL for other things, and did not want to switch environments). This article is not a tutorial on building Linux, but I will lay out some basic instructions so that anyone can (hopefully) reproduce the steps. After extracting the source, the first thing is to create a default configuration file:

cd linux-5.15.94/
make defconfig

This will create a “.config” file. The only thing I have changed in this file is to enable support for the “frame buffer”, so that an application can see the display as a bitmapped graphics display. For that I have used the text-based configuration tool called “menuconfig”:

make menuconfig

This screenshots below show the two options I have enabled: “Support for frame buffer devices” and “DRM support for bochs dispi vga interface”. I am in no way an expert on Linux kernel configuration, as I had to try a few different options before finding something that worked for me (sometimes the /dev/fb0 device did not show up on the guest machine, or the resolution was weird).

Configuring the Linux kernel
Configuring the Linux kernel

After saving and exiting, it is just a matter of issuing “make” and waiting for the kernel to build. If everything goes well, you will have a binary kernel image on “arch/x86_64/boot/bzImage” (since I am using WSL on a 64 bits host, this is kernel image path used).

The kernel alone can not do much. You will probably want a shell, some basic command line tools like ls, cat, etc. The BusyBox project provides a nice and compact solution for this. I have chosen the latest version 1.36.0 at the time of this writing. After extracting the source you have to issue a “make defconfig” to create a default configuration file. There is one important change you must make to the .config file: change CONFIG_STATIC to “y”, so that BusyBox is built as a static executable:

...
# Build Options
#
CONFIG_STATIC=y
# CONFIG_PIE is not set
# CONFIG_NOMMU is not set
# CONFIG_BUILD_LIBBUSYBOX is not set
# CONFIG_FEATURE_LIBBUSYBOX_STATIC is not set
# CONFIG_FEATURE_INDIVIDUAL is not set
...

If the build succeeds, you will have a busybox binary (mine is roughly 2.6 MB in size). Next I prepared a disk image to hold the real root filesystem, where PumpkinOS will be installed:

qemu-img create -f qcow2 disk.img 64M

This command creates a file named disk.img in a format recognized by QEMU. It is capable of storing 64 MB, which is enough for out purposes. Now comes the “fun” part of building a RAM filesystem for Linux. This filesystem contains just the minimum parts necessary for Linux to boot. From there, you can mount a proper disk-based filesystem (like disk.img created above), initialize the network, and anything else needed for your specific application. On a new directory named “initrd” on the WSL host, I have this:

-rwxr-xr-x 1 pmig96 pmig96 2599072 Feb 23 17:19 busybox*
drwxr-xr-x 2 pmig96 pmig96    4096 Feb 23 17:23 dev/
-rwxr-xr-x 1 pmig96 pmig96     383 Feb 23 17:40 init*
drwxr-xr-x 2 pmig96 pmig96    4096 Feb 23 17:23 proc/
drwxr-xr-x 2 pmig96 pmig96    4096 Feb 23 17:23 sys/
drwxr-xr-x 2 pmig96 pmig96    4096 Feb 23 17:23 work/

The busybox binary compiled earlier, an init script, three empty sub-directories (dev, proc and sys) and a “work” directory. The heart of the whole thing is the init script:

#!/busybox sh
/busybox mount -t sysfs sysfs /sys
/busybox mount -t proc proc /proc
/busybox mount -t devtmpfs udev /dev
/busybox mount /dev/sda /work
/busybox sysctl -w kernel.printk="2 4 1 7"
/busybox ifconfig lo 127.0.0.1
/busybox mount -o move /dev /work/dev
/busybox mount -o move /sys /work/sys
/busybox mount -o move /proc /work/proc
exec /busybox switch_root /work /sbin/init

This script is ran by the Linux kernel as the first process in the system and is responsible for setting things up. First it mounts the special Liunx filesystems /sys, /proc and /dev on our empty sub-directories acting as mount points. The /dev/sda device is the disk.img prepared earlier. With a proper command line option (shown later), QEMU will map it so that the Linux kernel sees it as /dev/sda. It is temporarily mounted on /work. The sysctl command is used to lower the amount of messages in the kernel log. I had to use the ifconfig command to setup the IP address of the loopback interface, otherwise it would be empty and PumpkinOS would not work. The next three mount commands transfer the special filesystems to /work. The last command switches the Linux root filesystem to /work and run /sbib/init from there. Since we used “exec” here, /sbin/init on the new root filesystem will still be process number 1. You probably noticed that all commands, even the shell shebang, are prefixed by “/busybox”. This is because BusyBox is a “super” command that implements all other commands as subroutines. The first argument defines the name of the subroutine to be called, which receives the rest of the arguments. The commands to create a RAM filesystem on the host machine are these:

cd initrd
find . | cpio -o -H newc > ../initrd.img

First switch to the “initrd” directory, where the files were created. Then pack everything into a initrd.img file using cpio.

One important note: we have not initialized disk.img yet. It was created empty and does not even have a filesystem, and certainly not a /sbin/init. There are many ways to create disk.img, but I have chosen a shortcut. I have used a modified init script that does not mount /work and does not switch the root filesystem. Instead, it just runs an interactive shell, so I a get a prompt on the guest machine once the boot process finishes. From there I used the BusyBox command mkfs.ext2 to initialize /dev/sda and manually mount it to /work:

/busybox mkfs.ext2 /dev/sda
/busybox mount /dev/sda /work

The only options BusyBox offered for the filesystem type were ext2, minix or vfat, so I went with ext2. For this first run, I have also included in the initial RAM filesystem all the files needed to run PumpkinOS. I then copied everything to /work and unmounted it. The next time I booted the system, the original init script shown here was used.

This is the command line used to run QEMU (which I settled on after much experimentation. It must be read as a single line):

qemu-system-x86_64 -kernel bzImage -append "vt.global_cursor_default=0" -initrd initrd.img -hda disk.img -usb -usbdevice tablet

Here bzImage is the Linux kernel image compiled in the first step. The “global_cursor_default” is a kernel parameter to disable a blinking cursor. The initrd parameter specifies the RAM filesystem we prepared, and the hda parameter maps disk.img to the primary disk on the guest machine (which Linux sees as /dev/sda). The usb and usbdevice parameters were necessary to make QEMU report mouse positions as absolute, instead of relative. PumpkinOS can handle relative positions, but without a reference to where the real mouse pointer is inside the QEMU window, there is no way to synchronize the real mouse position with the “pen” position within PumpkinOS.

Now we will see with more details what exactly lies within disk.img.

drwxr-xr-x  6 pmig96 pmig96  4096 Feb 23 20:30 PumpkinOS/
drwxr-xr-x  2 pmig96 pmig96 12288 Feb 23 20:40 bin/
drwxr-xr-x  2 pmig96 pmig96  4096 Feb 23 21:38 dev/
drwxr-xr-x  2 pmig96 pmig96  4096 Feb 23 20:41 etc/
drwxr-xr-x  3 pmig96 pmig96  4096 Feb 23 22:31 lib/
drwxr-xr-x  2 pmig96 pmig96  4096 Feb 23 22:32 lib64/
drwxr-xr-x  2 pmig96 pmig96  4096 Feb 23 21:38 proc/
drwxr-xr-x  2 pmig96 pmig96  4096 Feb 23 21:38 sbin/
drwxr-xr-x  2 pmig96 pmig96  4096 Feb 23 21:38 sys/

We already talked about dev, proc and sys. bin is where busybox is installed. There is a nice trick that allows us to use the command name directly (just use “ls” instead of “busybox ls”, for example). This is accomplished with links, like in this excerpt of the contents of the bin directory:

lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  bootchartd -> /bin/busybox*
lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  brctl -> /bin/busybox*
lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  bunzip2 -> /bin/busybox*
-rwxr-xr-x  1 pmig96 pmig96 2599072 Feb 23 07:35  busybox*
lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  bzcat -> /bin/busybox*
lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  bzip2 -> /bin/busybox*
lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  cal -> /bin/busybox*
lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  cat -> /bin/busybox*
lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  chat -> /bin/busybox*
lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  chattr -> /bin/busybox*

Directories lib and lib64 contain dynamic libraries dependencies for PumpkinOS. Just three of them are necessary:

/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libm.so.6
/lib64/ld-linux-x86-64.so.2

Directories /etc and /sbin are part of the init system of BusyBox. sbin contains only one link:

lrwxrwxrwx  1 pmig96 pmig96      12 Feb 21 21:38  init -> /bin/busybox*

Remember the switch_root command calling /sbin/init on the new root filesystem? That is just a link to the init implementation inside BusyBox. This init looks for a /etc/inittab file for its configuration. Currently it looks like:

::sysinit:/etc/sysinit
::respawn:/bin/pumpkin
::shutdown:/bin/umount -a -r

Here sysinit is a script containing further commands to execute during initialization. Is is currently empty. shutdown specifies the command to run during shutdown. And respawn is where PumpkinOS is started. The /bin/pumpkin is just a simple shell script:

#!/bin/sh
cd /PumpkinOS
PATH=./bin:$PATH
export LD_LIBRARY_PATH=./bin
./pumpkin -d 1 -f pumpkin.log -s libscriptlua.so ./script/pumpkin_linux.lua
poweroff -f

This is regular way to start PumpkinOS on Linux. The main difference is the last line. If PumpkinOS exits for any reason, the poweroff command will execute, causing QEMU to finish. Because of this, PumpkinOS will not be re-spawned as instructed in /etc/inittab.

All of this is pretty much a proof of concept. There are many edges to trim (while mouse works, keyboard still does not), and performance could improve a bit. It started as an answer to the question: is PumpkinOS a full OS? In its current form, the answer is obviously ‘no’. But when you take a bare OS kernel and put PumpkinOS right on top of it, it is starting to get there.

One thought on “PumpkinOS, Busybox and Linux

  1. I would run pumpkin os on stm or raspbery pi 2040
    and cc1101 or lora sx modems 😉

    Like

Leave a comment