About The Project
I love Doom. It’s one of my favorite video game franchises, and the community around Doom that ports this game to run on bizarre hardware is nothing short of genius. The process of porting Doom to run on something it wasn’t designed for is quintessential hacking. You’re reverse engineering hardware and software, building custom loaders, writing file format parsers, debugging weird machines, and everything in between. The ports are an art unto itself. They include running Doom on a heat pump, a Smart Planter, and even the Flipper Zero. This blog post walks through building a tiny Linux kernel whose only job is to boot Doom. While I fully admit this is not as nearly as impressive as having rats run doom, it’s the start of Arch Cloud Labs running Doom on weird devices.

Building A Tiny Kernel
I am leveraging kernel 6.18.1 for this blog post, and it can be obtained from kernel.org.
The Linux kernel’s Makefile contains numerous build targets to quickly get up and running with the latest build.
These options can be seen when executing make help from the root directory of the Kernel source code. Of interest to us for “Kernel of Doom” are tinyconfig and kvm_guest.config. As the descriptions show below, tinyconfig will build the tiniest kernel possible and kvm_guest.config will enable specific QEMU necessary configurations to boot our kernel images with QEMU. This combined with a minimal initramfs will allow for the Linux kernel to kick off our init process which will be doom-ascii.
➜ linux-6.18.1 make help
Cleaning targets:
clean - Remove most generated files but keep the config and
enough build support to build external modules
mrproper - Remove all generated files + config + various backup files
distclean - mrproper + remove editor backup and patch files
Configuration targets:
config - Update current config utilising a line-oriented program
nconfig - Update current config utilising a ncurses menu based program
menuconfig - Update current config utilising a menu based program
xconfig - Update current config utilising a Qt based front-end
gconfig - Update current config utilising a GTK+ based front-end
oldconfig - Update current config utilising a provided .config as base
localmodconfig - Update current config disabling modules not loaded
except those preserved by LMC_KEEP environment variable
localyesconfig - Update current config converting local mods to core
except those preserved by LMC_KEEP environment variable
defconfig - New config with default from ARCH supplied defconfig
savedefconfig - Save current config as ./defconfig (minimal config)
allnoconfig - New config where all options are answered with no
allyesconfig - New config where all options are accepted with yes
allmodconfig - New config selecting modules when possible
alldefconfig - New config with all symbols set to default
randconfig - New config with random answer to all options
yes2modconfig - Change answers from yes to mod if possible
mod2yesconfig - Change answers from mod to yes if possible
mod2noconfig - Change answers from mod to no if possible
listnewconfig - List new options
helpnewconfig - List new options and help text
olddefconfig - Same as oldconfig but sets new symbols to their
default value without prompting
tinyconfig - Configure the tiniest possible kernel
testconfig - Run Kconfig unit tests (requires python3 and pytest)
Configuration topic targets:
debug.config - Debugging for CI systems and finding regressions
hardening.config - Basic kernel hardening options
kvm_guest.config - Bootable as a KVM guest
nopm.config - Disable Power Management
rust.config - Enable Rust
x86_debug.config - Debugging options for tip tree testing
xen.config - Bootable as a Xen guest
...truncated....
An issue with tinyconfig is that it does not enable CONFIG_PRINTK which means no output is written to the QEMU console.
It’s essential for doom-ascii that this is enabled. Also, tinyconfig enables a networking stack and other features that are not particularly necessary for the goals of Kernel of Doom. Ultimately the two Makefile targets give an excellent basis to start configuration modifications are, but they still include features that are not necessary for building the tiniest kernel image possible. Removing subsystems that are not necessary for the goal of booting Doom can greatly reduce the overall size of the kernel. To reduce size, I disabled CONFIG_NET to remove all networking functionality and modified the initramfs support to exclude compression algorithms I wasn’t using.
[*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
() Initramfs source file(s)
[*] Support initial ramdisk/ramfs compressed using gzip
[*] Support initial ramdisk/ramfs compressed using bzip2
[*] Support initial ramdisk/ramfs compressed usiining LZMA
[ ] Support initial ramdisk/ramfs compressed using XZ
[ ] Support initial ramdisk/ramfs compressed using LZO
[ ] Support initial ramdisk/ramfs compressed using LZ4
[ ] Support initial ramdisk/ramfs compressed using ZSTD
After applying the previously mentioned changes, a bzip compressed kernel image comes in at a whopping 1.1 megabytes! With the kernel at a reasonably small size, the next object to tackle is the initramfs.
➜ kernel-of-doom ls -lah linux-6.18.1/arch/x86/boot/bzImage
-rw-r--r-- 1 dllcoolj dllcoolj 1.1M Dec 20 21:04 linux-6.18.1/arch/x86/boot/bzImage
Building The Initramfs
Per the Linux kernel documentation, the Linux kernel will execute a binary called init after unpacking the initramfs. For Kernel of Doom, this binary can just be doom-ascii.
Doom-ASCII is a port of Doom that runs entirely in the terminal.
Only one small changes was necessary to the Makefile to generate a static binary to run as a standalone utility for the initramfs.
diff --git a/Makefile b/Makefile
index d1dbb72..af4dbc1 100644
--- a/Makefile
+++ b/Makefile
@@ -59,7 +59,7 @@ APPDIR = $(OBJDIR)/io.github.wojciech_graj.doom_ascii.AppDir
OUTDIR = game
APPOUTDIR = appimage
-CFLAGS += -O3 -flto -Wall -D_DEFAULT_SOURCE -DVERSION=$(VERSION) -std=c99 #-DSNDSERV -DUSEASM
+CFLAGS += -static -O3 -flto -Wall -D_DEFAULT_SOURCE -DVERSION=$(VERSION) -std=c99 #-DSNDSERV -DUSEASM
For the custom initramfs it is nothing more than that a directory with the doom-ascii binary in a file called init, and a Doom game file (WAD). The WAD file provides game data to actually run Doom. This was obtained by purchasing “Doom II” on Steam and copying the “WAD” file from the game installation. This minimal initramfs file system looks as follows:
➜ kernel-of-doom ls -lah initramfs
total 16M
drwxr-xr-x 1 dllcoolj dllcoolj 62 Dec 20 21:13 .
drwxr-xr-x 1 dllcoolj dllcoolj 242 Dec 20 20:47 ..
-rwxr-xr-x 1 dllcoolj dllcoolj 92 Dec 15 20:15 build-initramfs.sh
-rwxr-xr-x 1 dllcoolj dllcoolj 14M Dec 18 21:05 doom2.wad
-rwxr-xr-x 1 dllcoolj dllcoolj 1.5M Dec 18 21:21 init
The build-initramfs.sh file is simply a helper file to rapidly build a new initramfs for testing.
The contents are as follows:
$> find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
The compressed initramfs comes in at 6.6 megabytes, the largest file being the Doom2.wad at 14 megabytes.
The Doom Wiki states that a WAD file is:
WAD (which, according to the Doom Bible, is an acronym for “Where’s All the Data?"[1]) is the file format used by Doom and all Doom-engine-based games for storing data
The WAD files are necessary for Doom to get up and running, and doom-ascii requires this be passed via a command line argument or be in the same directory doom-ascii is executing in. To further reduce this size, a modification to doom-ascii to take a compressed WAD file via command line would be ideal. This is left as an exercise to the reader should they want to re-implement this.
Booting into Doom
With the kernel and initramfs built, the last step is to boot the QEMU environment. The command line below shows how to boot a custom x86_64 kernel with an initramfs and have the console redirect to serial out.
$> qemu-system-x86_64 -kernel ./arch/x86_64/boot/bzImage \
-initrd ./initramfs.cpio.gz \
-nographic -append "console=ttyS0"
Behold! a successful minimal kernel that boots directly into Doom! Is it hard to read? Yes. Is it awesome? Also, yes.

The total size, to include uncompressed files is as follows:
- vmlinux: 11M
- bzImage: 1.1M
- initramfs.cpio.gz: 6.6M
- initramfs uncompressed: 15.5M (Doom WAD & Doom-ASCII)
Beyond The Project
The total size of the compressed kernel and initramfs with the necessary Doom artifacts was less than 8 megabytes in size. I’m sure there are ways to optimize beyond what I’ve shown here and I will leave that as a friendly challenge to the reader of this blog post. I would imagine a ARM kernel would be smaller, and the ability to read in a compressed WAD file or even a reduced WAD in size would result in something even smaller. I hope you enjoyed reading this, and I hope this inspires you to do something similar!