Tiny Linux OSes with Go
Small disclaimer: This is much more fun than it is useful
For a while now, we’ve been seeing new “cloud” OSes crop up, like CoreOS and RancherOS. These two both simply marry docker+linux to create a magic docker runtime.
You may be familiar with “micro” Docker images built on top of Alpine Linux / BusyBox base images. These “images” general weigh in around 10-30mb. But that’s before including the ~300mb OS + actual Docker daemon that they have to run on - seems awfully bloated to me.
So, we need to ask ourselves, “who really needs docker when we can have an entire OS that is purpose built to run your application, and nothing else? Short answer, most people; fun answer, no one.
So let’s get started. Make sure you have a copy of
QEMU installed locally,
as we’re going to cheat and use its ability to boot a raw kernel bzImage
(the compressed, bootable kernel image) to avoid having to set up a real
bootloader (not too hard, but can be a real pain).
So, to get in the right mindset, we’re going to use the excellent termboy-go as our test application, an incredibly cool Gameboy Color emulator that runs exclusively in the Linux console. In the end, we’ll have a “single purpose OS” that just runs a Gameboy emulator.
First off, we’re going to need a Linux kernel, so grab mainline from kernel.org,
make menuconfig and then
make bzImage. The default config should work,
just ensure that EXT4 is compiled not as a module.
To boot our new kernel and make sure everything works, just use QEMU. You should
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
which makes complete sense, as we haven’t added any sort of root FS.
Using a tool within
virt-make-fs we can create a QEMU compatible
.qcow2 from a directory (mine is a directory named
null) and boot with it.
Looking good, but we got another kernel panic -
Kernel panic - not syncing: No
working init found. Try passing init= option to kernel. See Linux
Documentation/init.txt for guidance.. Luckily, this one’s easy to solve, as,
funnily enough, any binary can be used as the kernel
init - Go just so happens
to make wonderful, static binaries.
As such, we need a binary to be our
init. So let’s clone, then build a static
go build -a --ldflags="-s -X -linkmode external -extldflags -static",
the extra LDFLAGS are needed, as
termboy uses a bit of CGo to handle keyboard
input. You’ll probably need to slightly tweak termboy-go (hardcode in the ROM
path you want to use), but this isn’t particularly difficult.
termboy-go depends on having a copy of
setfont in $PATH. This
is part of how it manages to render pixel-perfect 2D graphics in text-mode consoles.
Luckily, you can find a copy prebuilt from minos.io,
or rip apart a copy with
ermine (convert dynamic binaries to static).
termboy-go into the directory you used
virt-make-fs on, along with
a Gameboy Color ROM, and the copy of
setfont you just obtained.
After this, you should be able to just boot your new gameboy-as-a-OS ->
Just about any Go program can be built into a static binary, making it the perfect choice for developing embedded systems, like this gameboy-emulator system we built today. This single-OS approach may work well for running high-performance applications in production, if you’re interested in doing so, drop me a line.
However, there are many, far more ridiculous things that can be done using Go to
tackle early-userland OS problems - I have a bit of a distributed device manager
laying around, that synchronizes
/dev/ across all devices running it… and that’s
just the beginning of the things that are possible here. Go is fantastic for
this use case.