Diagram showing QEMU emulating a machine running a Linux kernel that launches a Go program as PID 1.
AI/ML

Booting Linux in QEMU and Writing PID 1 in Go

Codemurf Team

Codemurf Team

AI Content Generator

Dec 11, 2025
5 min read
0 views
Back to Blog

Learn how to boot a custom Linux kernel in QEMU and write the first userspace process (PID 1) in Go. A hands-on guide to understanding OS fundamentals.

Booting Linux in QEMU and Writing PID 1 in Go

For many developers, the Linux kernel is a black box—a complex piece of software that orchestrates the system from a distance. But what if we could strip away the layers and interact with it directly? By booting a minimal Linux kernel in the QEMU emulator and replacing the traditional init system with a simple program written in Go, we can demystify the kernel's role. This exercise reveals a powerful truth: the kernel is essentially a program that provides services, and PID 1 is its first and most privileged client. Let's explore this hands-on.

Setting the Stage: QEMU and a Minimal Kernel

The first step is creating a controlled, isolated environment. QEMU is a perfect tool for this, as it emulates a full computer system (CPU, memory, devices) without needing dedicated hardware. We'll pair it with a tiny, custom-built Linux kernel and a minimal root filesystem.

You'll need a kernel image (like bzImage) built with essential drivers (especially for the VirtIO devices QEMU uses). The root filesystem can be created using BusyBox or built from scratch. The key is the kernel command line: we pass root=/dev/vda console=ttyS0 to tell the kernel where to find its root filesystem (a virtual disk) and to use the serial console for output. Booting is a simple QEMU command:

qemu-system-x86_64 \
  -kernel bzImage \
  -drive file=rootfs.img,format=raw \
  -append "root=/dev/vda console=ttyS0" \
  -nographic

If successful, you'll be greeted by a kernel panic—specifically, one that says it can't find an init process. This is expected and good! The kernel has loaded, mounted the root filesystem, and now seeks to execute the program specified as init (by default /sbin/init). Our job is to provide it.

Crafting PID 1: A Minimal Init in Go

PID 1 is special. It's the first process spawned by the kernel after the initial ramdisk, and it traditionally handles system initialization, reaping orphaned child processes, and graceful shutdowns. We can write a drastically simplified version in Go. The core requirement is that it must run indefinitely, as its exit will cause another kernel panic.

We must compile our Go program statically and for the Linux target architecture (e.g., GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o init ./cmd/init). This eliminates dependencies on system libraries that won't exist in our minimal rootfs. Here's a skeleton:

package main

import (
    "fmt"
    "os"
    "os/exec"
    "time"
)

func main() {
    fmt.Println("My Go Init: PID 1 is alive!")

    // Example: launch a shell if available
    if _, err := os.Stat("/bin/sh"); err == nil {
        cmd := exec.Command("/bin/sh")
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        go cmd.Run()
    }

    // Keep PID 1 running
    for {
        time.Sleep(10 * time.Second)
        fmt.Println("PID 1 still here...")
    }
}

Place the compiled binary as /init in your root filesystem and adjust the kernel command line with init=/init. Upon reboot, QEMU will show your Go program's output. You've just created a working, if basic, userland.

Key Takeaways: The Kernel as a Service Provider

This experiment illuminates several foundational concepts:

  • The Kernel is a Program: It's not magic. It sets up hardware, manages memory and processes, and provides a syscall interface. Its primary job after boot is to load and execute your init program.
  • Abstraction Through Syscalls: Your Go init, like all programs, interacts with the world via syscalls (write for console output, fork/exec to run shells). The kernel is the sole provider of these services.
  • Modularity and Isolation: Using QEMU and a custom init demonstrates the clean contract between kernel and userspace. You can swap out any component—kernel, init, rootfs—independently.

By booting Linux in QEMU and writing PID 1 in Go, we move from abstract theory to tangible practice. You've built a microscopic, functional operating system. This foundational knowledge demystifies larger systems: whether it's systemd, Docker containers (which often have custom init processes), or unikernels, they all build upon this simple kernel-userspace handshake. The next time you run a program, you'll have a clearer mental model of the journey from BIOS to your code's execution.

Codemurf Team

Written by

Codemurf Team

AI Content Generator

Sharing insights on technology, development, and the future of AI-powered tools. Follow for more articles on cutting-edge tech.