Rémy Mathieu

Build a Linux kernel running only a Go binary

Oct 13, 20195 minutes read


I’ve recently thought that it’s been long since I’ve booted a Linux kernel that I’ve myself compiled. For some reasons, I also thought about the fact that it would be very easy to boot a Linux running only a Go binary, as they’re statically built if you don’t use external C or anything exotic.

This article is going through the Linux compilation process and a tiny initramfs creation to be able to boot a Go binary.

Compile Linux


First stop: https://kernel.org

At the time of writing, Linux 5.3.6 is the stable version so, let’s go for it.

  $ curl https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.3.6.tar.xz| tar Jx
  $ cd linux-5.3.6

Before compiling Linux, you’ll want to configure what you actually want shipped in the kernel. Linux comes with a menu to help you configure that.

$ make menuconfig

At this point, it’s up to you. If you want to run the image on your machine without virtualization and you’re doing this using Linux, you can use the command make localmodconfig which configures the kernel with what’s available in the running one.

If the plan is to run it in qemu (or something else), I would advise you to remove everything that will be unused to speed up the build:

  • bluetooth
  • drm / intel drivers
  • sound card support
  • pcmcia
  • even network drivers could be removed…

The Linux menu to configure the build


When you are happy with your configuration, build the kernel:

$ make -j4

Use -j with the value which fits the best your CPU to indicate to make how many jobs it can run simultaneously.

Here’s the result for my compilation configuration:

Setup is 16764 bytes (padded to 16896 bytes).
System is 6493 kB
CRC 6e1285d1
Kernel: arch/x86/boot/bzImage is ready  (#1)

Where we can see that a 6493 kB kernel has been built.


In order to test the kernel you have just built, there is a convenient option in qemu allowing you to directly boot a bzImage file:

$ qemu-system-x86_64 --enable-kvm -kernel linux-5.3.6/arch/x86/boot/bzImage

First run of this kernel

Looks like it is booting! But something is wrong: it can’t mount a rootfs, which makes total sense since we didn’t provide one.

If you prefer to run it directly in your terminal:

$ qemu-system-x86_64 --enable-kvm -kernel linux-5.3.6/arch/x86/boot/bzImage -nographic -append "console=ttyS0"

Running it in the terminal

Use C-a x to exit qemu.

Use initramfs as a rootfs

Since Linux 2.6.13, the kernel is capable of booting directly on a cpio archive, without requiring a file system or a block driver. Basically, everything will run from the RAM and you won’t need to mount anything. All you have to do to get started is to use the command cpio to create the archive that will be loaded in RAM and used as the rootfs.

What you want to put inside is up to you (binaries, libraries, etc.).

Create an initramfs containing busybox

If for example you want ls, sh, cat and other useful binaries, you can compile busybox and add these binaries into your initramfs. Make sure to compile busybox statically though!

$ wget https://busybox.net/downloads/busybox-1.31.0.tar.bz2|tar xJ
$ cd busybox-1.31.0
$ make menuconfig # select the option to build static binaries
$ make -j4
$ make install

Now that busybox is compiled, let’s fill a directory with the content of our future initramfs:

$ mkdir -p initramfs-busybox/{bin,sbin,etc,proc,sys,usr/bin,usr/sbin}
$ cp -a busybox-1.31.0/_install/* initramfs-busybox/

What we also want is an init binary available in the root of our initramfs, because Linux will have the good idea of starting this binary just after it has booted!

As we have sh available, we can create an executable script calling… sh to boot on a shell interpreter.

$ echo '#!/bin/sh\necho "Welcome to your custom Linux."\n/bin/sh' > initramfs-busybox/init
$ chmod +x initramfs-busybox/init

Finally, create the initramfs archive with the content of the directory:

$ cd initramfs-busybox
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
$ cd ../

And voilà, initramfs.cpio.gz is our compressed initramfs! In order to boot with it:

$ qemu-system-x86_64 --enable-kvm -kernel linux-5.3.6/arch/x86/boot/bzImage -nographic -append "console=ttyS0" -initrd initramfs.cpio.gz

If this doesn’t work, make sure you’ve compiled the kernel with the initramfs support.

A minimal Linux kernel with busybox binaries.

Create a minimal initramfs with only a Go binary

Now that we know how to create an initramfs and do something useful with it, back to our initial goal: let’s boot on a Go binary.

From there, it will actually be very easy: we just have to build our binary, make sure it is static, send it to an empty directory named as init and create an initramfs with the content of this directory!

Let’s say we have this Go file in a directory called source:

package main

import (

func main() {
	fmt.Println("Hello from your custom built Linux kernel.")
	fmt.Println("Booting on a Go binary!")
    for {} // never stop!

Let’s compile it and build an initramfs with only this binary:

$ cd source
$ go build -o init
$ cd ../
$ mkdir initramfs-go
$ mv source/init initramfs-go/init
$ cd initramfs-go
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz

And… that’s pretty much it. We can now boot on this initramfs:

$ qemu-system-x86_64 --enable-kvm -kernel linux-5.3.6/arch/x86/boot/bzImage -nographic -append "console=ttyS0" -initrd initramfs.cpio.gz

A minimal Linux kernel running only a Go binary.


Since the Go binaries are static, it’s very easy to boot them as the init process of a Linux kernel. We have to keep in mind that “it just works” because I’m building everything on the same CPU architecture: we would have to involve cross-compilation if we wanted to do this for another architecture.

However, starting from here, there are many things that would be very interesting to develop such as an actual init system!

If you’ve reached this point and liked this article, thank you and don’t hesitate to ping me on Twitter.

Some links I’ve used:

Back to posts